Files
buzz/desktop/tests/e2e/channel-browser.spec.ts
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

553 lines
19 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { installMockBridge, openChannelBrowser } from "../helpers/bridge";
const MOCK_PUBKEY = "deadbeef".repeat(8);
const CUSTOM_SECTION = { id: "sec-projects", name: "Projects", order: 0 };
async function seedCustomSection(page: Page) {
await page.addInitScript(
({ pubkey, section }) => {
window.localStorage.setItem(
`buzz-channel-sections.v1:${pubkey}`,
JSON.stringify({ version: 1, sections: [section], assignments: {} }),
);
},
{ pubkey: MOCK_PUBKEY, section: CUSTOM_SECTION },
);
}
test.beforeEach(async ({ page }, testInfo) => {
await installMockBridge(
page,
testInfo.title.includes("failed section create")
? { createChannelErrors: ["Create failed"] }
: undefined,
);
});
test("keyboard shortcut opens the channel browser dialog", async ({ page }) => {
await page.goto("/");
await expect(page.getByTestId("app-sidebar")).toBeVisible();
const isMacBrowser = await page.evaluate(() =>
/mac|iphone|ipad|ipod/i.test(navigator.platform),
);
if (isMacBrowser) {
await page.evaluate(() => {
window.dispatchEvent(
new KeyboardEvent("keydown", {
bubbles: true,
cancelable: true,
key: "O",
metaKey: true,
shiftKey: true,
}),
);
});
} else {
await page.keyboard.press("Control+Shift+O");
}
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
});
test("channel browser shows channels not yet joined", async ({ page }) => {
await page.goto("/");
await openChannelBrowser(page);
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
await expect(
page.getByRole("heading", { name: "Browse channels" }),
).toBeVisible();
// "design" and "sales" are open channels the mock user is NOT a member of
await expect(page.getByTestId("browse-channel-design")).toBeVisible();
await expect(page.getByTestId("browse-channel-sales")).toBeVisible();
// "general" is a channel the mock user IS a member of — shown in "Joined" section
await expect(page.getByTestId("browse-channel-general")).toBeVisible();
});
test("channel browser sorts alphabetically or by member count", async ({
page,
}) => {
await page.goto("/");
await openChannelBrowser(page);
const rows = page.locator('[data-testid^="browse-channel-"]');
await expect(rows).toHaveText([
/#agents/,
/#all-replies/,
/#deep-history/,
/#design/,
/#engineering/,
/#general/,
/#random/,
/#sales/,
/#secret-projects/,
/#welcome-everyone/,
]);
await page.getByTestId("channel-browser-sort").click();
await page.getByTestId("channel-browser-sort-members").click();
await expect(rows).toHaveText([
/#general/,
/#agents/,
/#engineering/,
/#random/,
/#all-replies/,
/#deep-history/,
/#design/,
/#sales/,
/#secret-projects/,
/#welcome-everyone/,
]);
await expect(page.getByTestId("channel-browser-sort")).toHaveAttribute(
"aria-label",
"Sort channels: Most members",
);
});
test("channel browser sorts by recent activity", async ({ page }) => {
await page.goto("/");
await openChannelBrowser(page);
const rows = page.locator('[data-testid^="browse-channel-"]');
await page.getByTestId("channel-browser-sort").click();
await page.getByTestId("channel-browser-sort-recent").click();
await expect(rows).toHaveText([
/#all-replies/,
/#deep-history/,
/#general/,
/#agents/,
/#sales/,
/#engineering/,
/#design/,
/#random/,
/#secret-projects/,
/#welcome-everyone/,
]);
await expect(page.getByTestId("channel-browser-sort")).toHaveAttribute(
"aria-label",
"Sort channels: Recent",
);
});
test("channel browser search filters by name", async ({ page }) => {
await page.goto("/");
await openChannelBrowser(page);
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
await page.getByTestId("channel-browser-search").fill("design");
await expect(page.getByTestId("browse-channel-design")).toBeVisible();
await expect(page.getByTestId("browse-channel-sales")).toHaveCount(0);
await expect(page.getByTestId("browse-channel-general")).toHaveCount(0);
});
test("channel browser search filters by description", async ({ page }) => {
await page.goto("/");
await openChannelBrowser(page);
await page.getByTestId("channel-browser-search").fill("pipeline");
// "sales" has "pipeline" in its description
await expect(page.getByTestId("browse-channel-sales")).toBeVisible();
await expect(page.getByTestId("browse-channel-design")).toHaveCount(0);
});
test("channel browser shows no results for unmatched search", async ({
page,
}) => {
await page.goto("/");
await openChannelBrowser(page);
await page.getByTestId("channel-browser-search").fill("zzz-nonexistent");
await expect(page.getByText("No channels match your search")).toBeVisible();
});
test("channel browser fuzzy-matches a subsequence", async ({ page }) => {
await page.goto("/");
await openChannelBrowser(page);
// "engr" is not a substring of "engineering", but it is an in-order
// subsequence — plain includes() would miss it, fuzzy matching finds it.
await page.getByTestId("channel-browser-search").fill("engr");
await expect(page.getByTestId("browse-channel-engineering")).toBeVisible();
await expect(page.getByTestId("browse-channel-general")).toHaveCount(0);
});
test("channel browser matches a scattered subsequence", async ({ page }) => {
await page.goto("/");
await openChannelBrowser(page);
// "sls" is neither a substring nor a prefix of "sales" — it only matches as
// an in-order subsequence (s·a·l·e·s). Proves fuzzy matching end-to-end.
await page.getByTestId("channel-browser-search").fill("sls");
await expect(page.getByTestId("browse-channel-sales")).toBeVisible();
await expect(page.getByTestId("browse-channel-general")).toHaveCount(0);
});
test("channel browser ranks the best match first", async ({ page }) => {
await page.goto("/");
await openChannelBrowser(page);
// "gen" is a prefix of "general" (strong match) but only a substring of
// "agents" and a subsequence of "engineering" (weaker). The prefix match
// should float to the top regardless of the alphabetical default sort.
await page.getByTestId("channel-browser-search").fill("gen");
const firstRow = page.getByTestId(/^browse-channel-/).first();
await expect(firstRow).toHaveAttribute(
"data-testid",
"browse-channel-general",
);
});
test("sidebar add-channel button creates without treating the click as a callback", async ({
page,
}) => {
await page.goto("/");
await expect(page.getByTestId("app-sidebar")).toBeVisible();
await page.getByTestId("section-actions-channels-quick-create").click();
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
const channelName = `sidebar-created-${Date.now()}`;
await page.getByTestId("channel-browser-search").fill(channelName);
await page.getByTestId("channel-browser-create-row").click();
await page.getByTestId("create-channel-submit").click();
await expect(page.getByTestId("channel-browser-dialog")).not.toBeVisible();
await expect(page.getByTestId("stream-list")).toContainText(channelName);
});
test("custom section add button creates directly into that section", async ({
page,
}) => {
await seedCustomSection(page);
await page.goto("/");
const addButton = page.getByTestId(
`section-actions-${CUSTOM_SECTION.id}-quick-create`,
);
await expect(addButton).toHaveAccessibleName("Add channel to Projects");
await addButton.click();
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
const channelName = `section-created-${Date.now()}`;
await page.getByTestId("channel-browser-search").fill(channelName);
await page.getByTestId("channel-browser-create-row").click();
await page.getByTestId("create-channel-submit").click();
await expect(page.getByTestId("channel-browser-dialog")).not.toBeVisible();
await expect(
page.getByTestId(`section-title-${CUSTOM_SECTION.id}`),
).toBeVisible();
await expect(page.getByTestId(`channel-${channelName}`)).toBeVisible();
await expect(page.getByTestId("stream-list")).not.toContainText(channelName);
});
test("canceling section create does not affect the next global create", async ({
page,
}) => {
await seedCustomSection(page);
await page.goto("/");
await page
.getByTestId(`section-actions-${CUSTOM_SECTION.id}-quick-create`)
.click();
// Gate on the dialog mounting before dismissing it. Escape sent before mount
// is dropped (no handler yet), and not.toBeVisible() then passes vacuously
// against a dialog that hasn't rendered — so the dialog opens *after* the
// assertion and its overlay swallows every later click.
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByTestId("channel-browser-dialog")).not.toBeVisible();
// The overlay outlives the content by one exit animation; wait for it to
// detach so it can't intercept the next click.
await expect(page.getByTestId("dialog-overlay")).toHaveCount(0);
await page.getByTestId("section-actions-channels-quick-create").click();
const channelName = `global-after-cancel-${Date.now()}`;
await page.getByTestId("channel-browser-search").fill(channelName);
await page.getByTestId("channel-browser-create-row").click();
await page.getByTestId("create-channel-submit").click();
await expect(page.getByTestId("stream-list")).toContainText(channelName);
});
test("failed section create retry still assigns to the section", async ({
page,
}) => {
await seedCustomSection(page);
await page.goto("/");
await page
.getByTestId(`section-actions-${CUSTOM_SECTION.id}-quick-create`)
.click();
const channelName = `section-retry-${Date.now()}`;
await page.getByTestId("channel-browser-search").fill(channelName);
await page.getByTestId("channel-browser-create-row").click();
await page.getByTestId("create-channel-submit").click();
await expect(page.getByText("Create failed")).toBeVisible();
await page.getByTestId("create-channel-submit").click();
await expect(page.getByTestId("channel-browser-dialog")).not.toBeVisible();
await expect(page.getByTestId(`channel-${channelName}`)).toBeVisible();
await expect(page.getByTestId("stream-list")).not.toContainText(channelName);
});
test("create affordance is visible on open before typing", async ({ page }) => {
await page.goto("/");
await openChannelBrowser(page);
// The create row is present from the get-go so it's clear you can browse OR
// create — not just after you start typing.
const createRow = page.getByTestId("channel-browser-create-row");
await expect(createRow).toBeVisible();
await expect(createRow).toContainText("Create a new channel");
});
test("typing a partial match surfaces a persistent create row", async ({
page,
}) => {
await page.goto("/");
await openChannelBrowser(page);
// "desig" matches "design" by substring but is not an exact channel name,
// so both the matching channel AND the create row are shown.
await page.getByTestId("channel-browser-search").fill("desig");
const createRow = page.getByTestId("channel-browser-create-row");
await expect(createRow).toBeVisible();
await expect(createRow).toContainText("desig");
await expect(page.getByTestId("browse-channel-design")).toBeVisible();
});
test("exact name match hides the create row", async ({ page }) => {
await page.goto("/");
await openChannelBrowser(page);
await page.getByTestId("channel-browser-search").fill("general");
await expect(page.getByTestId("browse-channel-general")).toBeVisible();
await expect(page.getByTestId("channel-browser-create-row")).toHaveCount(0);
});
test("no-match search pins a create row above the empty state", async ({
page,
}) => {
await page.goto("/");
await openChannelBrowser(page);
await page.getByTestId("channel-browser-search").fill("zzz-nonexistent");
await expect(page.getByText("No channels match your search")).toBeVisible();
const createRow = page.getByTestId("channel-browser-create-row");
await expect(createRow).toBeVisible();
await expect(createRow).toContainText("zzz-nonexistent");
});
test("create row leads to the prefilled create form", async ({ page }) => {
await page.goto("/");
await openChannelBrowser(page);
await page.getByTestId("channel-browser-search").fill("desig");
await page.getByTestId("channel-browser-create-row").click();
// Create mode reuses the shared form; the name is prefilled from the query.
await expect(page.getByTestId("create-channel-name")).toHaveValue("desig");
// Back returns to the search list without closing the dialog.
await page.getByTestId("channel-browser-create-back").click();
await expect(page.getByTestId("channel-browser-search")).toBeVisible();
});
test("creating from the browser adds the channel to the sidebar", async ({
page,
}) => {
const channelName = `browse-created-${Date.now()}`;
await page.goto("/");
await openChannelBrowser(page);
await page.getByTestId("channel-browser-search").fill(channelName);
await page.getByTestId("channel-browser-create-row").click();
await expect(page.getByTestId("create-channel-name")).toHaveValue(
channelName,
);
await page.getByTestId("create-channel-submit").click();
await expect(page.getByTestId("channel-browser-dialog")).not.toBeVisible();
await expect(page.getByTestId("stream-list")).toContainText(channelName);
await expect(page.getByTestId("chat-title")).toContainText(channelName);
});
test("Enter with no matches jumps to create", async ({ page }) => {
const channelName = `enter-created-${Date.now()}`;
await page.goto("/");
await openChannelBrowser(page);
await page.getByTestId("channel-browser-search").fill(channelName);
await page.keyboard.press("Enter");
await expect(page.getByTestId("create-channel-name")).toHaveValue(
channelName,
);
});
test("arrow keys reach the pinned create row and Enter activates it", async ({
page,
}) => {
await page.goto("/");
await openChannelBrowser(page);
// "desig" keeps a channel match (#design) AND the create row visible, so the
// create row is not the only actionable item — it must be reachable by
// keyboard, not just Tab.
await page.getByTestId("channel-browser-search").fill("desig");
const createRow = page.getByTestId("channel-browser-create-row");
await expect(createRow).toBeVisible();
// The create row is pinned at the top → first ArrowDown highlights it.
await page.keyboard.press("ArrowDown");
await expect(createRow).toHaveAttribute("data-selected", "true");
// Enter on the highlighted create row enters the prefilled create form.
await page.keyboard.press("Enter");
await expect(page.getByTestId("create-channel-name")).toHaveValue("desig");
});
test("Enter selects a channel when create row is not highlighted", async ({
page,
}) => {
await page.goto("/");
await openChannelBrowser(page);
// With the create row present but NOT highlighted, Enter should still select
// the first channel match rather than jumping to create.
await page.getByTestId("channel-browser-search").fill("desig");
await expect(page.getByTestId("browse-channel-design")).toBeVisible();
await page.keyboard.press("Enter");
await expect(page.getByTestId("channel-browser-dialog")).not.toBeVisible();
await expect(page.getByTestId("chat-title")).toHaveText("design");
});
test("joining a channel from browser adds it to the sidebar", async ({
page,
}) => {
await page.goto("/");
// Verify "design" is not in the sidebar
const streamList = page.getByTestId("stream-list");
await expect(streamList).not.toContainText("design");
// Open browser and join
await openChannelBrowser(page);
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
await page
.getByTestId("browse-channel-design")
.getByRole("button", { name: "Join" })
.click();
// Dialog should close and navigate to the joined channel
await expect(page.getByTestId("channel-browser-dialog")).not.toBeVisible();
await expect(page).toHaveURL(/#\/channels\//);
await expect(page.getByTestId("chat-title")).toHaveText("design");
// Channel should now appear in the sidebar
await expect(streamList).toContainText("design");
});
test("clicking a joined channel in browser navigates to it", async ({
page,
}) => {
await page.goto("/");
await openChannelBrowser(page);
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
// "general" is already joined — clicking should navigate without join
await page.getByTestId("browse-channel-general").click();
await expect(page.getByTestId("channel-browser-dialog")).not.toBeVisible();
await expect(page).toHaveURL(/#\/channels\//);
await expect(page.getByTestId("chat-title")).toHaveText("general");
});
test("channel browser does not show DM or private channels", async ({
page,
}) => {
await page.goto("/");
await openChannelBrowser(page);
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
// DM channels should not appear
await expect(page.getByTestId("browse-channel-alice-tyler")).toHaveCount(0);
await expect(page.getByTestId("browse-channel-bob-tyler")).toHaveCount(0);
// Private forum should not appear
await expect(page.getByTestId("browse-channel-announcements")).toHaveCount(0);
});
test("channel browser closes on escape", async ({ page }) => {
await page.goto("/");
await openChannelBrowser(page);
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByTestId("channel-browser-dialog")).not.toBeVisible();
});
test("keyboard navigation works in channel browser", async ({ page }) => {
await page.goto("/");
await openChannelBrowser(page);
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
// Filter to unjoined channels only to get a predictable list
await page.getByTestId("channel-browser-search").fill("design");
await expect(page.getByTestId("browse-channel-design")).toBeVisible();
// Press Enter to join the selected (first) channel
await page.keyboard.press("Enter");
await expect(page.getByTestId("channel-browser-dialog")).not.toBeVisible();
await expect(page.getByTestId("chat-title")).toHaveText("design");
});
test("sidebar only shows channels the user has joined", async ({ page }) => {
await page.goto("/");
const streamList = page.getByTestId("stream-list");
// Channels the mock user IS a member of
await expect(streamList).toContainText("general");
await expect(streamList).toContainText("random");
await expect(streamList).toContainText("engineering");
await expect(streamList).toContainText("agents");
// Channels the mock user is NOT a member of
await expect(streamList).not.toContainText("design");
await expect(streamList).not.toContainText("sales");
});