import { test, expect } from "../support/fixtures"; import { gotoWorkspace, assertNewChatTileVisible, assertNewTabMenuTriggerVisible, assertSingleNewTabButton, openNewTabMenuWithShortcut, pressDirectNewTabShortcut, clickNewChat, clickNewTerminal, countTabsOfKind, getTabTestIds, waitForTabWithTitle, measureTileTransition, sampleTabsDuringTransition, expectTabTitleFits, terminalSurfaceLocator, } from "../support/helpers/launcher"; import { expectComposerVisible, composerLocator } from "../support/helpers/composer"; import { expectTerminalSurfaceVisible, setupDeterministicPrompt, } from "../support/helpers/terminal-perf"; import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; import { expectTerminalOutputContains, seedTerminalProfiles, type TerminalProfile, } from "../support/helpers/new-workspace-launch"; // ─── Shared state ────────────────────────────────────────────────────────── let workspace: SeededWorkspace; const EMPTY_PROMPT_PROFILE: TerminalProfile = { id: "e2e-empty-prompt", name: "Empty Prompt", command: "/bin/sh", args: ["-c", 'echo prompt-args: "$#"; sleep 10', "profile-name", "{{{prompt}}}"], }; test.beforeAll(async () => { workspace = await seedWorkspace({ repoPrefix: "launcher-e2e-" }); }); test.afterAll(async () => { await workspace?.cleanup(); }); // ═══════════════════════════════════════════════════════════════════════════ // Tab Creation Tests // ═══════════════════════════════════════════════════════════════════════════ test.describe("Tab creation", () => { test("retained inactive workspaces cannot own New tab shortcuts", async ({ page }) => { await gotoWorkspace(page, workspace.workspaceId); const workspaceUrl = page.url(); const modifier = process.platform === "darwin" ? "Meta" : "Control"; await page.keyboard.press(`${modifier}+Comma`); await expect(page.getByRole("navigation", { name: "Settings" })).toBeVisible(); await page.keyboard.press(`${modifier}+t`); await expect(page.getByRole("menuitem", { name: /New agent/ })).toHaveCount(0); await page.goto(workspaceUrl); await assertNewTabMenuTriggerVisible(page); const countBefore = await countTabsOfKind(page, "draft"); await pressDirectNewTabShortcut(page, "a"); await expect.poll(() => countTabsOfKind(page, "draft")).toBe(countBefore + 1); }); test("Cmd+T opens the New tab menu without creating an agent", async ({ page }) => { await gotoWorkspace(page, workspace.workspaceId); const countBefore = await countTabsOfKind(page, "draft"); await openNewTabMenuWithShortcut(page); await expect.poll(() => countTabsOfKind(page, "draft")).toBe(countBefore); await expect(page.getByRole("button", { name: "New tab", expanded: true })).toBeVisible(); }); test("opening two new tabs creates two draft tabs", async ({ page }) => { await gotoWorkspace(page, workspace.workspaceId); const countBefore = await countTabsOfKind(page, "draft"); await pressDirectNewTabShortcut(page, "a"); await expect .poll(() => countTabsOfKind(page, "draft"), { timeout: 15_000 }) .toBe(countBefore + 1); const countAfterFirst = await countTabsOfKind(page, "draft"); await pressDirectNewTabShortcut(page, "a"); await expect .poll(() => countTabsOfKind(page, "draft"), { timeout: 15_000 }) .toBe(countAfterFirst + 1); }); test("New tab menu exposes shortcuts and supports arrow navigation", async ({ page }) => { await gotoWorkspace(page, workspace.workspaceId); await openNewTabMenuWithShortcut(page); const agent = page.getByRole("menuitem", { name: /New agent/ }); const terminal = page.getByRole("menuitem", { name: /New terminal/ }); const changes = page.getByRole("menuitem", { name: /Changes/ }); const files = page.getByRole("menuitem", { name: /Files/ }); const shortcutPrefix = process.platform === "darwin" ? /⇧⌘/ : /Ctrl.*Shift/; await expect(agent).toContainText(new RegExp(`${shortcutPrefix.source}.*A`)); await expect(terminal).toContainText(new RegExp(`${shortcutPrefix.source}.*T`)); await expect(changes).toContainText(new RegExp(`${shortcutPrefix.source}.*C`)); await expect(files).toContainText(new RegExp(`${shortcutPrefix.source}.*E`)); await expect(agent).toBeFocused(); await page.keyboard.press("ArrowDown"); await expect(terminal).toBeFocused(); await page.keyboard.press("ArrowUp"); await expect(agent).toBeFocused(); await page.keyboard.press("Escape"); await expect(page.getByRole("button", { name: "New tab" })).toBeFocused(); await expect(page.getByRole("button", { name: "New tab" })).toHaveAttribute( "aria-expanded", "false", ); }); test("clicking new agent tab creates a draft tab", async ({ page }) => { await gotoWorkspace(page, workspace.workspaceId); await clickNewChat(page); await expectComposerVisible(page); const tabsAfter = await getTabTestIds(page); const draftCountAfter = tabsAfter.filter((id) => id.includes("draft")).length; expect(draftCountAfter).toBeGreaterThanOrEqual(1); }); test("clicking terminal button creates a standalone terminal", async ({ page }) => { test.setTimeout(45_000); await gotoWorkspace(page, workspace.workspaceId); await clickNewTerminal(page); await expectTerminalSurfaceVisible(page); const tabsAfter = await getTabTestIds(page); const terminalTabs = tabsAfter.filter((id) => id.includes("terminal")); expect(terminalTabs.length).toBeGreaterThanOrEqual(1); }); test("launching a profile from the New tab menu drops its empty prompt argument", async ({ page, }) => { test.setTimeout(45_000); const profileSeed = await seedTerminalProfiles([EMPTY_PROMPT_PROFILE]); try { await gotoWorkspace(page, workspace.workspaceId); await page.getByTestId("workspace-new-tab-menu-trigger").filter({ visible: true }).click(); await page.getByRole("menuitem", { name: EMPTY_PROMPT_PROFILE.name }).click(); await expectTerminalOutputContains(page, "prompt-args: 0"); } finally { await profileSeed.restore(); } }); test("tab bar shows action buttons per pane", async ({ page }) => { await gotoWorkspace(page, workspace.workspaceId); await assertSingleNewTabButton(page); await assertNewChatTileVisible(page); await assertNewTabMenuTriggerVisible(page); }); test("never shades the New tab button", async ({ page }) => { await gotoWorkspace(page, workspace.workspaceId); const tabRow = page.getByTestId("workspace-tabs-row").filter({ visible: true }).first(); const scrollArea = tabRow.getByTestId("workspace-tabs-scroll"); const newTabButtonInScroll = await scrollArea .getByTestId("workspace-new-tab-menu-trigger") .count(); const scrollShades = await tabRow .locator('[data-testid^="workspace-tabs-scroll-shade-"]') .count(); expect(newTabButtonInScroll === 0 || scrollShades === 0).toBe(true); await expect(tabRow.getByTestId("workspace-new-tab-menu-trigger")).toBeVisible(); }); }); // ═══════════════════════════════════════════════════════════════════════════ // Terminal Title Tests // ═══════════════════════════════════════════════════════════════════════════ test.describe("Terminal title propagation", () => { // OSC title escape sequence propagation is inherently flaky — the terminal // must process the sequence, emit a title change event, and the tab bar // must re-render before the assertion deadline. Allow retries. test.describe.configure({ retries: 2 }); test.skip("terminal tab title updates from OSC title escape sequence", async ({ page }) => { test.setTimeout(60_000); const result = await workspace.client.createTerminal( workspace.repoPath, "title-test", undefined, { workspaceId: workspace.workspaceId, }, ); if (!result.terminal) throw new Error(`Failed to create terminal: ${result.error}`); const terminalId = result.terminal.id; try { // Navigate to workspace and open a terminal await gotoWorkspace(page, workspace.workspaceId); await clickNewTerminal(page); await expectTerminalSurfaceVisible(page); await terminalSurfaceLocator(page).click(); await setupDeterministicPrompt(page); // Send OSC 0 (set window title) escape sequence const testTitle = `E2E-Title-${Date.now()}`; await terminalSurfaceLocator(page).pressSequentially(`printf '\\033]0;${testTitle}\\007'\n`, { delay: 0, }); // Wait for the tab to reflect the new title await waitForTabWithTitle(page, testTitle, 15_000); } finally { await workspace.client.killTerminal(terminalId).catch(() => {}); } }); test.skip("title debouncing coalesces rapid changes", async ({ page }) => { test.setTimeout(60_000); const result = await workspace.client.createTerminal( workspace.repoPath, "debounce-test", undefined, { workspaceId: workspace.workspaceId, }, ); if (!result.terminal) throw new Error(`Failed to create terminal: ${result.error}`); const terminalId = result.terminal.id; try { await gotoWorkspace(page, workspace.workspaceId); await clickNewTerminal(page); await expectTerminalSurfaceVisible(page); await terminalSurfaceLocator(page).click(); await setupDeterministicPrompt(page); // Fire many rapid title changes — only the last should stick const finalTitle = `Final-${Date.now()}`; for (let i = 0; i < 5; i++) { await terminalSurfaceLocator(page).pressSequentially(`printf '\\033]0;Rapid-${i}\\007'\n`, { delay: 0, }); } await terminalSurfaceLocator(page).pressSequentially( `printf '\\033]0;${finalTitle}\\007'\n`, { delay: 0 }, ); // The tab should eventually settle on the final title await waitForTabWithTitle(page, finalTitle, 15_000); } finally { await workspace.client.killTerminal(terminalId).catch(() => {}); } }); }); // ═══════════════════════════════════════════════════════════════════════════ // No-Flash Transition Tests // ═══════════════════════════════════════════════════════════════════════════ test.describe("Tab transitions (no flash)", () => { test("New agent tab transition has no blank intermediate tab state", async ({ page }) => { await gotoWorkspace(page, workspace.workspaceId); // Sample tabs at high frequency across the transition const snapshots = await sampleTabsDuringTransition(page, () => clickNewChat(page)); // Every snapshot should have at least one tab — no blank/zero-tab frames for (const snapshot of snapshots) { expect(snapshot.length).toBeGreaterThanOrEqual(1); } // Tab count should never spike excessively (no duplicate flash from add-then-remove). // When running in-suite, previous tests may have created tabs on the shared workspace, // so we allow +2 tolerance for accumulated state and React render batching. const counts = snapshots.map((snapshot) => snapshot.length); const maxCount = Math.max(...counts); const initialCount = counts[0] ?? 0; expect(maxCount).toBeLessThanOrEqual(initialCount + 2); expect(new Set(snapshots.map((snapshot) => JSON.stringify(snapshot))).size).toBeLessThanOrEqual( 2, ); await expectTabTitleFits(page, "New Agent", { min: 96, max: 160 }); }); test("Terminal transition completes within visual budget", async ({ page }) => { test.setTimeout(30_000); await gotoWorkspace(page, workspace.workspaceId); const elapsed = await measureTileTransition( page, () => clickNewTerminal(page), terminalSurfaceLocator(page), 20_000, ); // Terminal surface should appear within a reasonable budget. // Note: terminal creation involves a server round-trip, so we allow more time // than a pure in-memory transition, but it should still be well under 5 seconds. expect(elapsed).toBeLessThan(5_000); }); test("New agent tab click shows composer without flash", async ({ page }) => { await gotoWorkspace(page, workspace.workspaceId); const elapsed = await measureTileTransition( page, () => clickNewChat(page), composerLocator(page), 10_000, ); // Draft creation is fully in-memory — should be fast // We use a generous budget here because CI can be slow, but the key assertion // is that no blank/flash frame appears (tested above). expect(elapsed).toBeLessThan(3_000); }); });