import { expect, test } from "@playwright/test"; import { installMockBridge } from "../helpers/bridge"; import { openSettings } from "../helpers/settings"; const ENGINEERING_CHANNEL_ID = "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9"; const WATERCOLOR_CHANNEL_ID = "a27e1ee9-76a6-5bdf-a5d5-1d85610dad11"; const FORUM_POST_ID = "mock-forum-release-thread"; const FORUM_REPLY_ID = "mock-forum-release-reply"; test.beforeEach(async ({ page }) => { await installMockBridge(page); }); async function navigateToWorkflows(page: import("@playwright/test").Page) { await page.goto("/"); await page.getByTestId("open-workflows-view").click(); await expect(page).toHaveURL(/#\/workflows$/); await expect(page.getByTestId("workflows-view")).toBeVisible(); } async function createWorkflow( page: import("@playwright/test").Page, name: string, ) { await page.getByRole("button", { name: "Create Workflow" }).click(); const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible(); await dialog.getByLabel("Workflow name").fill(name); await dialog.getByRole("button", { name: "Add step" }).click(); await dialog.getByRole("button", { name: "Create" }).click(); await expect(dialog).not.toBeVisible(); } test("global back and forward move across channel routes", async ({ page }) => { await page.goto("/"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); await page.getByTestId("channel-random").click(); await expect(page.getByTestId("chat-title")).toHaveText("random"); await page.getByTestId("global-back").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); await page.getByTestId("global-forward").click(); await expect(page.getByTestId("chat-title")).toHaveText("random"); }); test("back/forward keyboard chords work while the composer has focus", async ({ page, }) => { const backChord = process.platform === "darwin" ? "Meta+[" : "Alt+ArrowLeft"; const forwardChord = process.platform === "darwin" ? "Meta+]" : "Alt+ArrowRight"; await page.goto("/"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); await page.getByTestId("channel-random").click(); await expect(page.getByTestId("chat-title")).toHaveText("random"); // The composer autofocuses on channel switch; make the regression // condition explicit by clicking into it. The chords must still fire // from inside the contenteditable (#3775). await page.getByTestId("message-input").click(); await expect(page.getByTestId("message-input")).toBeFocused(); await page.keyboard.press(backChord); await expect(page.getByTestId("chat-title")).toHaveText("general"); await page.keyboard.press(forwardChord); await expect(page.getByTestId("chat-title")).toHaveText("random"); // preventDefault kept the chord out of the editor — no stray characters. await expect(page.getByTestId("message-input")).toHaveText(""); }); // FIXME: the forum post "Back to posts" header renders under the fixed top // chrome drag region, which intercepts the click. Pre-existing breakage — // this spec file was never registered in playwright.config.ts until now. // The header-chrome rework (PR #941) covers this overlap class. test.fixme("direct forum thread links close back to the forum route", async ({ page, }) => { await page.goto( `/#/channels/${WATERCOLOR_CHANNEL_ID}/posts/${FORUM_POST_ID}`, ); await expect(page.getByTestId("chat-title")).toHaveText("watercooler"); await expect( page.getByRole("button", { name: "Back to posts" }), ).toBeVisible(); await page.getByRole("button", { name: "Back to posts" }).click(); await expect(page).toHaveURL( /#\/channels\/a27e1ee9-76a6-5bdf-a5d5-1d85610dad11$/, ); await expect( page.getByText("Release checklist: async feedback thread."), ).toBeVisible(); }); test("direct workflow detail links close back to workflows", async ({ page, }) => { const workflowName = `workflow_nav_${Date.now()}`; await navigateToWorkflows(page); await createWorkflow(page, workflowName); const workflowCard = page .locator('[data-testid^="workflow-card-"]') .filter({ hasText: workflowName }) .first(); const workflowTestId = await workflowCard.getAttribute("data-testid"); const workflowId = workflowTestId?.replace("workflow-card-", ""); expect(workflowId).toBeTruthy(); await page.goto(`/#/workflows/${workflowId}`); await expect(page.getByTestId("workflow-detail-panel")).toBeVisible(); await page.getByRole("button", { name: "Close detail panel" }).click(); await expect(page).toHaveURL(/#\/workflows$/); await expect(page.getByTestId("workflows-view")).toBeVisible(); }); test("forum reply deep links survive reload", async ({ page }) => { await page.goto( `/#/channels/${WATERCOLOR_CHANNEL_ID}/posts/${FORUM_POST_ID}?replyId=${FORUM_REPLY_ID}`, ); await expect(page.getByTestId("chat-title")).toHaveText("watercooler"); await expect( page.getByText("Looks good to me. We should ship it."), ).toBeVisible(); await page.reload(); await expect(page.getByTestId("chat-title")).toHaveText("watercooler"); await expect( page.getByText("Looks good to me. We should ship it."), ).toBeVisible(); }); test("back and forward restore open thread panels", async ({ page }) => { await page.goto("/"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); const rootMessage = page .getByTestId("message-timeline") .getByTestId("message-row") .first(); await rootMessage.hover(); await rootMessage.getByRole("button", { name: "Reply" }).click(); const threadPanel = page.getByTestId("message-thread-panel"); await expect(threadPanel).toBeVisible(); await expect(page).toHaveURL(/thread=/); await page.getByTestId("channel-random").click(); await expect(page.getByTestId("chat-title")).toHaveText("random"); await expect(threadPanel).not.toBeVisible(); await page.getByTestId("global-back").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); await expect(threadPanel).toBeVisible(); await page.getByTestId("global-forward").click(); await expect(page.getByTestId("chat-title")).toHaveText("random"); await expect(threadPanel).not.toBeVisible(); }); test("back undoes closing a thread panel", async ({ page }) => { await page.goto("/"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); const rootMessage = page .getByTestId("message-timeline") .getByTestId("message-row") .first(); await rootMessage.hover(); await rootMessage.getByRole("button", { name: "Reply" }).click(); const threadPanel = page.getByTestId("message-thread-panel"); await expect(threadPanel).toBeVisible(); await threadPanel.getByRole("button", { name: "Close panel" }).click(); await expect(threadPanel).not.toBeVisible(); await page.getByTestId("global-back").click(); await expect(threadPanel).toBeVisible(); }); test("open thread panels survive reload", async ({ page }) => { await page.goto("/"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); const rootMessage = page .getByTestId("message-timeline") .getByTestId("message-row") .first(); await rootMessage.hover(); await rootMessage.getByRole("button", { name: "Reply" }).click(); const threadPanel = page.getByTestId("message-thread-panel"); await expect(threadPanel).toBeVisible(); await expect(page).toHaveURL(/thread=/); await page.reload(); await expect(page.getByTestId("chat-title")).toHaveText("general"); await expect(threadPanel).toBeVisible(); }); test("home inbox selection survives reload and back restores it", async ({ page, }) => { await page.goto("/"); const inboxList = page.getByTestId("home-inbox-list"); await expect(inboxList).toBeVisible(); const items = inboxList.locator('[data-testid^="home-inbox-item-"]'); await expect(items.first()).toBeVisible(); // The wide-viewport default selection stays local-only — the URL records // explicit selections, so background loads never touch the history stack. await expect(page.getByTestId("home-inbox-detail")).toBeVisible(); expect(page.url()).not.toContain("item="); const defaultUrl = page.url(); const selectedItem = items.first(); const selectedTestId = await selectedItem.getAttribute("data-testid"); const selectedItemId = selectedTestId?.replace("home-inbox-item-", ""); expect(selectedItemId).toBeTruthy(); await selectedItem.click(); await expect .poll(() => page.url()) .toContain(`item=${encodeURIComponent(selectedItemId ?? "")}`); await page.reload(); await expect(inboxList).toBeVisible(); await expect(page.getByTestId("home-inbox-detail")).toBeVisible(); expect(page.url()).toContain( `item=${encodeURIComponent(selectedItemId ?? "")}`, ); await page.getByTestId("global-back").click(); await expect.poll(() => page.url()).toBe(defaultUrl); }); test("settings is a route: section survives reload, closing returns to the previous panel state", async ({ page, }) => { await page.goto("/"); // Open a channel with a thread panel so there's panel state to come back to. await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); const rootMessage = page .getByTestId("message-timeline") .getByTestId("message-row") .first(); await rootMessage.hover(); await rootMessage.getByRole("button", { name: "Reply" }).click(); const threadPanel = page.getByTestId("message-thread-panel"); await expect(threadPanel).toBeVisible(); const channelUrl = page.url(); await openSettings(page); await expect(page).toHaveURL(/#\/settings/); // Section switches rewrite the settings entry (replace, not push). await page.getByTestId("settings-nav-notifications").click(); await expect(page).toHaveURL(/section=notifications/); await page.reload(); await expect(page.getByTestId("settings-view")).toBeVisible(); await expect(page).toHaveURL(/section=notifications/); await page.getByTestId("settings-back-to-app").click(); await expect.poll(() => page.url()).toBe(channelUrl); await expect(page.getByTestId("chat-title")).toHaveText("general"); await expect(threadPanel).toBeVisible(); }); test("settings shortcut returns without opening search dialog", async ({ page, }) => { await page.goto("/"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); const channelUrl = page.url(); // Open search via the ⌘K shortcut so the focus-request counter is non-zero, // then close it. The Settings subtree remounts the sidebar + search on close, // which must not replay the stale counter and resurrect search. await page.evaluate(() => { const isMac = /mac|iphone|ipad|ipod/i.test(navigator.platform); window.dispatchEvent( new KeyboardEvent("keydown", { bubbles: true, cancelable: true, code: "KeyK", ctrlKey: !isMac, key: "k", metaKey: isMac, }), ); }); await expect(page.getByTestId("search-dialog-input")).toBeFocused(); await page.keyboard.press("Escape"); await expect(page.getByTestId("search-results")).not.toBeVisible(); await page.keyboard.press( process.platform === "darwin" ? "Meta+Comma" : "Control+Comma", ); await expect(page).toHaveURL(/#\/settings/); await page.getByTestId("settings-back-to-app").click(); await expect.poll(() => page.url()).toBe(channelUrl); await expect(page.getByTestId("chat-title")).toHaveText("general"); await expect(page.getByTestId("search-results")).not.toBeVisible(); }); test("message links to visible root messages open the thread panel", async ({ page, }) => { await page.goto("/"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); await expect(page.getByTestId("message-timeline")).toContainText( "Welcome to #general", ); const link = "buzz://message?channel=9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50&id=mock-general-welcome"; await page.getByTestId("message-input").fill(`Root link repro ${link}`); await page.getByTestId("send-message").click(); const linkMessage = page .getByTestId("message-row") .filter({ hasText: "Root link repro" }) .last(); await expect(linkMessage).toBeVisible(); await linkMessage .getByRole("button", { name: "Open message in general" }) .click(); const threadPanel = page.getByTestId("message-thread-panel"); await expect(threadPanel).toBeVisible(); await expect(page).toHaveURL(/thread=mock-general-welcome/); await expect(threadPanel.getByTestId("message-thread-head")).toContainText( "Welcome to #general", ); }); test("message links reopen a closed thread when the same messageId is already in the URL", async ({ page, }) => { await page.goto( "/#/channels/9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50?messageId=mock-general-welcome", ); await expect(page.getByTestId("chat-title")).toHaveText("general"); const threadPanel = page.getByTestId("message-thread-panel"); await expect(threadPanel).toBeVisible(); await expect(threadPanel.getByTestId("message-thread-head")).toContainText( "Welcome to #general", ); await threadPanel.getByRole("button", { name: "Close panel" }).click(); await expect(threadPanel).not.toBeVisible(); const link = "buzz://message?channel=9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50&id=mock-general-welcome"; await page .getByTestId("message-input") .fill(`Reopen same root link repro ${link}`); await page.getByTestId("send-message").click(); const linkMessage = page .getByTestId("message-row") .filter({ hasText: "Reopen same root link repro" }) .last(); await expect(linkMessage).toBeVisible(); await linkMessage .getByRole("button", { name: "Open message in general" }) .click(); await expect(threadPanel).toBeVisible(); await expect(threadPanel.getByTestId("message-thread-head")).toContainText( "Welcome to #general", ); }); test("message deep links survive reload", async ({ page }) => { await page.goto( `/#/channels/${ENGINEERING_CHANNEL_ID}?messageId=mock-engineering-shipped`, ); await expect(page.getByTestId("chat-title")).toHaveText("engineering"); await expect(page.getByTestId("message-timeline")).toContainText( "Engineering shipped the desktop build.", ); await page.reload(); await expect(page.getByTestId("chat-title")).toHaveText("engineering"); await expect(page.getByTestId("message-timeline")).toContainText( "Engineering shipped the desktop build.", ); });