Files
buzz/desktop/tests/e2e/sidebar-relay-card.spec.ts
T
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

335 lines
11 KiB
TypeScript

import { expect, type Page, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
const CONNECT_ERROR = "relay unreachable: could not connect to relay";
const PROXY_ERROR =
"relay unreachable: relay returned an unexpected HTML page (network sign-in?)";
const ACCESS_ERROR = "relay unreachable: 403 Forbidden";
const RELAY_AUTH_ERROR = "Relay authentication rejected.";
type RelayConnectionState =
| "connected"
| "connecting"
| "disconnected"
| "idle"
| "reconnecting"
| "stalled";
async function setChannelsReadError(page: Page, error: string | null) {
await page.evaluate((nextError) => {
const testWindow = window as Window & {
__BUZZ_E2E__?: { mock?: { channelsReadError?: string } };
};
if (!testWindow.__BUZZ_E2E__?.mock) {
throw new Error("Mock bridge config is not installed.");
}
if (nextError === null) {
delete testWindow.__BUZZ_E2E__.mock.channelsReadError;
return;
}
testWindow.__BUZZ_E2E__.mock.channelsReadError = nextError;
}, error);
}
async function setRelayConnectionState(
page: Page,
state: RelayConnectionState,
) {
if (state !== "connected") {
// When driving to a degraded state, wait for the relay to reach "connected"
// first. Without this guard, the mock relay's async auth handshake (started
// on page load) can race the override and set state back to "connected"
// after we've driven it to a degraded value.
await page.waitForFunction(() => {
const win = window as Window & {
__BUZZ_E2E_GET_RELAY_CONNECTION_STATE__?: () => string;
__BUZZ_E2E_SET_RELAY_CONNECTION_STATE__?: unknown;
};
return (
typeof win.__BUZZ_E2E_SET_RELAY_CONNECTION_STATE__ === "function" &&
typeof win.__BUZZ_E2E_GET_RELAY_CONNECTION_STATE__ === "function" &&
win.__BUZZ_E2E_GET_RELAY_CONNECTION_STATE__() === "connected"
);
});
} else {
// When driving to "connected", just wait for the seam to be installed —
// no need to gate on the current state since we're overriding it directly.
await page.waitForFunction(
() =>
typeof (
window as Window & {
__BUZZ_E2E_SET_RELAY_CONNECTION_STATE__?: unknown;
}
).__BUZZ_E2E_SET_RELAY_CONNECTION_STATE__ === "function",
);
}
await page.evaluate((nextState) => {
const testWindow = window as Window & {
__BUZZ_E2E_SET_RELAY_CONNECTION_STATE__?: (
state: RelayConnectionState,
) => void;
};
const setConnectionState =
testWindow.__BUZZ_E2E_SET_RELAY_CONNECTION_STATE__;
if (!setConnectionState) {
throw new Error("Mock relay connection state helper is not installed.");
}
setConnectionState(nextState);
}, state);
}
async function emitRelayConnectionState(
page: Page,
state: RelayConnectionState,
) {
await page.evaluate((nextState) => {
const setConnectionState = (
window as Window & {
__BUZZ_E2E_SET_RELAY_CONNECTION_STATE__?: (
state: RelayConnectionState,
) => void;
}
).__BUZZ_E2E_SET_RELAY_CONNECTION_STATE__;
if (!setConnectionState) {
throw new Error("Mock relay connection state helper is not installed.");
}
setConnectionState(nextState);
}, state);
}
async function expectGenericReconnectCard(page: Page) {
const card = page.getByTestId("sidebar-relay-unreachable");
await expect(card).toBeVisible();
await expect(card).toContainText("Can't reach the relay");
await expect(card).toContainText("Click to connect");
await expect(page.getByTestId("sidebar-reconnect")).toBeVisible();
return card;
}
test("sidebar generic relay failures use the reconnect card", async ({
page,
}) => {
await installMockBridge(page, { channelsReadError: CONNECT_ERROR });
await page.goto("/");
// The card is gated on !isRelayConnectionConnected — a stale channelsReadError
// alone (with state still "connected") no longer pins the card. Drive the relay
// into a disconnected state so the card appears immediately (disconnected is
// reported without debounce, unlike the 2-second delay for reconnecting/stalled).
await setRelayConnectionState(page, "disconnected");
await expectGenericReconnectCard(page);
});
test("relay outage notification stays dismissed through retries and re-arms after recovery", async ({
page,
}) => {
await installMockBridge(page, { channelsReadError: CONNECT_ERROR });
await page.goto("/");
await setRelayConnectionState(page, "disconnected");
const card = await expectGenericReconnectCard(page);
await card
.getByRole("button", { name: "Dismiss relay notification" })
.click({ force: true });
await expect(card).toBeHidden();
// Retry churn is still the same outage: no successful connection occurred.
await emitRelayConnectionState(page, "connecting");
await emitRelayConnectionState(page, "disconnected");
await emitRelayConnectionState(page, "reconnecting");
await page.waitForTimeout(2_100);
await expect(card).toBeHidden();
// A successful connection ends the episode and re-arms the next outage.
await setChannelsReadError(page, null);
await emitRelayConnectionState(page, "connected");
await setChannelsReadError(page, CONNECT_ERROR);
await emitRelayConnectionState(page, "disconnected");
await expectGenericReconnectCard(page);
});
test("relay outage notification re-arms after same-URL lifecycle teardown", async ({
page,
}) => {
await installMockBridge(page, { channelsReadError: CONNECT_ERROR });
await page.goto("/");
await setRelayConnectionState(page, "disconnected");
const card = await expectGenericReconnectCard(page);
await card
.getByRole("button", { name: "Dismiss relay notification" })
.click({ force: true });
await expect(card).toBeHidden();
// Community switches and reconnectCommunity() tear down the singleton to
// idle before applying the next lifecycle. The next lifecycle may reuse the
// same relay URL, so URL identity alone must not preserve the old dismissal.
await emitRelayConnectionState(page, "idle");
await emitRelayConnectionState(page, "disconnected");
await expectGenericReconnectCard(page);
});
test("sidebar proxy sign-in failures use the reconnect card", async ({
page,
}) => {
await installMockBridge(page, { channelsReadError: PROXY_ERROR });
await page.goto("/");
// Drive disconnected so the card appears immediately (connected is now authoritative).
await setRelayConnectionState(page, "disconnected");
await expectGenericReconnectCard(page);
});
test("sidebar access failures use the reconnect card", async ({ page }) => {
await installMockBridge(page, { channelsReadError: ACCESS_ERROR });
await page.goto("/");
// Drive disconnected so the card appears immediately (connected is now authoritative).
await setRelayConnectionState(page, "disconnected");
await expectGenericReconnectCard(page);
});
test("collapsed sidebar still shows the reconnect card", async ({ page }) => {
await installMockBridge(page, { channelsReadError: CONNECT_ERROR });
await page.goto("/");
// Drive degraded state so the card appears.
await setRelayConnectionState(page, "disconnected");
await expectGenericReconnectCard(page);
await page
.getByRole("button", { exact: true, name: "Toggle Sidebar" })
.click();
await expect(
page.locator('[data-state="collapsed"][data-collapsible="offcanvas"]'),
).toHaveCount(1);
// The card remains visible via the fixed overlay even with sidebar collapsed.
const card = page.getByTestId("sidebar-relay-unreachable");
await expect(card).toBeVisible();
await expect(card).toContainText("Can't reach the relay");
await setChannelsReadError(page, null);
await page.getByTestId("sidebar-reconnect").click();
// Drive connected so the card shows success and auto-dismisses.
await setRelayConnectionState(page, "connected");
await expect(card).toContainText("Connected");
await expect(card).toBeHidden({ timeout: 10_000 });
});
test("sidebar stalled relay state uses the reconnect card", async ({
page,
}) => {
await installMockBridge(page);
await page.goto("/");
await expect(page.getByTestId("channel-general")).toBeVisible();
await setRelayConnectionState(page, "stalled");
await expectGenericReconnectCard(page);
});
test("sidebar application auth disconnects stay on the error path", async ({
page,
}) => {
await installMockBridge(page, { channelsReadError: RELAY_AUTH_ERROR });
await page.goto("/");
await setRelayConnectionState(page, "disconnected");
await expect(page.getByText(RELAY_AUTH_ERROR)).toBeVisible();
await expect(page.getByTestId("sidebar-relay-unreachable")).toHaveCount(0);
});
test("sidebar reconnect action shows connected before hiding", async ({
page,
}) => {
await installMockBridge(page, { channelsReadError: CONNECT_ERROR });
await page.goto("/");
// Drive disconnected state so the card appears immediately.
await setRelayConnectionState(page, "disconnected");
const card = await expectGenericReconnectCard(page);
await setChannelsReadError(page, null);
await page.getByTestId("sidebar-reconnect").click();
// Drive connected to simulate the relay recovering after the reconnect action.
await setRelayConnectionState(page, "connected");
await expect(card).toContainText("Connected");
await expect(card).not.toContainText("Click to connect");
await page.waitForTimeout(3_000);
await expect(card).toContainText("Connected");
await expect(card).toBeHidden({ timeout: 5_000 });
});
test("sidebar reconnect action suppresses stale refresh errors after success", async ({
page,
}) => {
await installMockBridge(page, { channelsReadError: CONNECT_ERROR });
await page.goto("/");
// Drive disconnected state so the card appears immediately.
await setRelayConnectionState(page, "disconnected");
const card = await expectGenericReconnectCard(page);
await page.getByTestId("sidebar-reconnect").click();
// Drive connected to simulate the relay recovering after the reconnect action.
await setRelayConnectionState(page, "connected");
await page.waitForTimeout(500);
await expect(card).toBeVisible();
await expect(card).toContainText("Connected");
await expect(card).not.toContainText("Can't reach the relay");
await page.waitForTimeout(6_500);
await expect(card).toBeHidden();
});
test("sidebar connected success clears when relay degrades again", async ({
page,
}) => {
await installMockBridge(page, { channelsReadError: CONNECT_ERROR });
await page.goto("/");
// Drive disconnected state so the card appears immediately.
await setRelayConnectionState(page, "disconnected");
const card = await expectGenericReconnectCard(page);
await setChannelsReadError(page, null);
await page.getByTestId("sidebar-reconnect").click();
// Drive connected to simulate the relay recovering after the reconnect action.
await setRelayConnectionState(page, "connected");
await expect(card).toContainText("Connected");
await setRelayConnectionState(page, "stalled");
await expect(card).toContainText("Can't reach the relay", {
timeout: 10_000,
});
await expect(card).toContainText("Click to connect");
await expect(card).not.toContainText("Connected");
});