import { expect, test, type Page } from "@playwright/test"; import { installMockBridge } from "../helpers/bridge"; import { openSettings } from "../helpers/settings"; const SIDEBAR_WIDTH_STORAGE_KEY = "buzz-sidebar-width"; const COMMUNITY_ONBOARDING_STORAGE_KEY = "buzz-community-onboarding-transaction.v1"; const DEFAULT_SIDEBAR_WIDTH = 300; test.beforeEach(async ({ page }) => { await installMockBridge(page); }); async function sidebarWidth(page: Page) { return page.getByTestId("app-sidebar").evaluate((element) => { return Math.round(element.getBoundingClientRect().width); }); } async function storedSidebarWidth(page: Page) { return page.evaluate( (key) => localStorage.getItem(key), SIDEBAR_WIDTH_STORAGE_KEY, ); } async function loadTheme(page: Page, theme: string) { await page.addInitScript((selectedTheme) => { window.localStorage.setItem("buzz-theme", selectedTheme); }, theme); await installMockBridge(page); await page.goto("/"); } async function openAddCommunityDialog(page: Page) { await page.getByTestId("sidebar-profile-avatar-button").click(); await page.getByTestId("community-switcher").click(); await page.getByRole("menuitem", { name: "Add a community" }).click(); } // Regression guard for the "Leave channel" lockup: with two bundled copies of // @radix-ui/react-dismissable-layer, opening a modal AlertDialog from a modal // Radix ContextMenu left `pointer-events: none` stuck on
after the // dialog closed, freezing the whole app. Fixed by the pnpm override in // pnpm-workspace.yaml deduplicating the layer. This asserts the app is still // interactive. async function expectAppClickable(page: Page) { await expect .poll(() => page.evaluate(() => getComputedStyle(document.body).pointerEvents), ) .not.toBe("none"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); } async function dragSidebarRail(page: Page, deltaX: number) { const sidebarRail = page.locator('[data-sidebar="rail"]'); await expect(sidebarRail).toBeVisible(); await expect(sidebarRail).toBeEnabled(); const box = await sidebarRail.boundingBox(); expect(box).not.toBeNull(); if (!box) return; const startX = box.x + box.width / 2; const startY = box.y + box.height / 2; await page.mouse.move(startX, startY); await page.mouse.down(); await page.mouse.move(startX + deltaX, startY, { steps: 8 }); await page.mouse.up(); } test("add community starts with create and join choices", async ({ page }) => { await installMockBridge(page, {}); await page.goto("/"); await openAddCommunityDialog(page); await expect( page.getByRole("heading", { name: "Add community" }), ).toBeVisible(); await expect(page.getByTestId("add-community-create")).toContainText( "Create a new community", ); await expect(page.getByTestId("add-community-join")).toContainText( "Join an existing community", ); await expect(page.getByLabel("API Token")).toHaveCount(0); await expect(page.getByLabel("Repos Directory")).toHaveCount(0); await page.getByTestId("add-community-join").click(); await expect( page.getByRole("heading", { name: "Join an existing community" }), ).toBeVisible(); await expect(page.getByLabel("Community URL or invite link")).toBeVisible(); await page.getByTestId("add-community-back").click(); await page.getByTestId("add-community-create").click(); await expect( page.getByRole("heading", { name: "Create a new community" }), ).toBeVisible(); await page.getByRole("button", { name: "Continue to Builderlab" }).click(); await page.getByRole("button", { name: "Connect and continue" }).click(); await expect(page.getByLabel("Community address")).toBeVisible(); }); test("automatically shows community join requirements near the community URL", async ({ page, }) => { await installMockBridge(page, { applyCommunityDelayMs: 1_000, joinPolicy: { terms_markdown: "# Terms", privacy_markdown: "# Privacy", age_attestation_required: true, version: "policy-v1", }, }); await page.goto("/"); await openAddCommunityDialog(page); await page.getByTestId("add-community-join").click(); await page .getByLabel("Community URL or invite link") .fill("https://policy.example.com"); const ageConfirmation = page.getByLabel("I am 18 years of age or older."); const agreementConfirmation = page.getByLabel( "I agree to the Buzz Terms of Service and Privacy Policy.", ); await expect(ageConfirmation).toBeVisible(); await expect(agreementConfirmation).toBeVisible(); await expect( page.getByText("Review this relay's join policy below."), ).toHaveCount(0); await expect(page.getByText(/By continuing, you agree/)).toHaveCount(0); const joinCommunityButton = page.getByTestId("invite-redeem-submit"); await expect(joinCommunityButton).toBeDisabled(); await ageConfirmation.check(); await expect(ageConfirmation.locator("svg path")).toBeVisible(); await expect(joinCommunityButton).toBeDisabled(); await agreementConfirmation.check(); await expect(joinCommunityButton).toBeEnabled(); await expect(joinCommunityButton).toHaveText("Accept and join"); const consentBox = await agreementConfirmation.boundingBox(); const communityInput = await page .getByLabel("Community URL or invite link") .boundingBox(); const joinButtonBox = await joinCommunityButton.boundingBox(); expect(consentBox?.y).toBeGreaterThan(communityInput?.y ?? Number.MAX_VALUE); expect(consentBox?.y).toBeLessThan(joinButtonBox?.y ?? 0); await joinCommunityButton.click(); await expect .poll(() => page.evaluate( (key) => window.localStorage.getItem(key), COMMUNITY_ONBOARDING_STORAGE_KEY, ), ) .toContain('"relayUrl":"wss://policy.example.com"'); }); test("joins a community URL without an API token field", async ({ page }) => { await installMockBridge(page, { applyCommunityDelayMs: 1_000 }); await page.goto("/"); await openAddCommunityDialog(page); await page.getByTestId("add-community-join").click(); await page .getByLabel("Community URL or invite link") .fill("token.example.com"); await expect(page.getByLabel("API token")).toHaveCount(0); await expect(page.getByTestId("community-api-token-reveal")).toHaveCount(0); await page.getByTestId("invite-redeem-submit").click(); await expect .poll(() => page.evaluate((key) => { const raw = window.localStorage.getItem(key); if (!raw) return null; return JSON.parse(raw) as { relayUrl?: string }; }, COMMUNITY_ONBOARDING_STORAGE_KEY), ) .toMatchObject({ relayUrl: "wss://token.example.com" }); }); test("hides Invites settings on open relays", async ({ page }) => { await page.goto("/"); await openSettings(page); await expect(page.getByTestId("settings-nav-community-members")).toHaveCount( 0, ); }); test("leaving a channel from the context menu never freezes the app", async ({ page, }) => { await page.goto("/"); await expect(page.getByTestId("app-sidebar")).toBeVisible(); // Cancel path: dialog opens from the context menu, then is dismissed. await page.getByTestId("channel-random").click({ button: "right" }); await page.getByRole("menuitem", { name: "Leave channel" }).click(); await expect(page.getByRole("alertdialog")).toBeVisible(); await page.getByRole("button", { name: "Cancel" }).click(); await expect(page.getByRole("alertdialog")).toHaveCount(0); await expectAppClickable(page); // Confirm path: same overlay lifecycle, plus the leave mutation. await page.getByTestId("channel-random").click({ button: "right" }); await page.getByRole("menuitem", { name: "Leave channel" }).click(); await expect(page.getByRole("alertdialog")).toBeVisible(); await page.getByRole("button", { name: "Leave" }).click(); await expect(page.getByRole("alertdialog")).toHaveCount(0); await expectAppClickable(page); }); test("channel context menu only shows owner actions to the owner", async ({ page, }) => { await page.goto("/"); await page.getByTestId("channel-general").click({ button: "right" }); await expect( page.getByRole("menuitem", { name: "Archive channel" }), ).toBeVisible(); await expect( page.getByRole("menuitem", { name: "Delete channel" }), ).toBeVisible(); const lifecycleOrder = ( await page.getByRole("menuitem").allTextContents() ).filter((label) => ["Leave channel", "Archive channel", "Delete channel"].includes(label), ); expect(lifecycleOrder).toEqual([ "Leave channel", "Archive channel", "Delete channel", ]); await page.keyboard.press("Escape"); await page.getByTestId("channel-random").click({ button: "right" }); await expect( page.getByRole("menuitem", { name: "Leave channel" }), ).toBeVisible(); await expect( page.getByRole("menuitem", { name: "Loading channel actions..." }), ).toHaveCount(0); await expect( page.getByRole("menuitem", { name: "Archive channel" }), ).toHaveCount(0); await expect( page.getByRole("menuitem", { name: "Delete channel" }), ).toHaveCount(0); }); test("channel context menu explains when owner actions are loading", async ({ page, }) => { await installMockBridge(page, { channelMembersReadDelayMs: 500 }); await page.goto("/"); await page.getByTestId("channel-general").click({ button: "right" }); await expect( page.getByRole("menuitem", { name: "Loading channel actions..." }), ).toBeVisible(); await expect( page.getByRole("menuitem", { name: "Archive channel" }), ).toBeVisible(); await expect( page.getByRole("menuitem", { name: "Loading channel actions..." }), ).toHaveCount(0); }); test("channel owner can archive from the context menu", async ({ page }) => { await page.goto("/"); await page.getByTestId("channel-general").click({ button: "right" }); await page.getByRole("menuitem", { name: "Archive channel" }).click(); await expect(page.getByTestId("stream-list")).not.toContainText("general"); }); test("channel owner can delete from the context menu", async ({ page }) => { await page.goto("/"); await page.getByTestId("channel-general").click(); await page.getByTestId("channel-general").click({ button: "right" }); await page.getByRole("menuitem", { name: "Delete channel" }).click(); await expect( page.getByTestId("channel-delete-confirmation-dialog"), ).toBeVisible(); await page.getByTestId("channel-delete-confirm").click(); await expect(page.getByTestId("home-inbox-list")).toBeVisible(); await expect(page.getByTestId("stream-list")).not.toContainText("general"); }); for (const theme of ["buzz", "github-light", "catppuccin-mocha"]) { test(`uses the continuous sidebar surface in ${theme}`, async ({ page }) => { await loadTheme(page, theme); const pinnedHeader = page.getByTestId("sidebar-pinned-header"); const footer = page.locator( '[data-testid="app-sidebar"] [data-sidebar="footer"]', ); const channelContent = page.getByTestId("sidebar-channel-content"); await expect(pinnedHeader).toBeVisible(); await expect(footer).toBeVisible(); await expect(channelContent).toBeVisible(); const chromeStyles = await page.evaluate(() => { const header = document.querySelector