From a4eb0adcf62b643a7b817743f7a6fa752f4068af Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 17 Oct 2026 23:01:36 +0700 Subject: [PATCH] test(app): consolidate overlapping Playwright journeys (#7245) --- packages/app/e2e/browser/launcher-tab.spec.ts | 67 ++++-------- .../app/e2e/browser/projects-settings.spec.ts | 100 +++++++----------- .../browser/provider-settings-refresh.spec.ts | 22 +--- .../app/e2e/browser/workspace-cwd.spec.ts | 99 ----------------- .../e2e/browser/workspace-lifecycle.spec.ts | 4 +- packages/app/e2e/support/helpers/launcher.ts | 14 --- .../support/helpers/workspace-lifecycle.ts | 33 ++++++ 7 files changed, 94 insertions(+), 245 deletions(-) delete mode 100644 packages/app/e2e/browser/workspace-cwd.spec.ts diff --git a/packages/app/e2e/browser/launcher-tab.spec.ts b/packages/app/e2e/browser/launcher-tab.spec.ts index e9d43ced27..a34aa259d2 100644 --- a/packages/app/e2e/browser/launcher-tab.spec.ts +++ b/packages/app/e2e/browser/launcher-tab.spec.ts @@ -2,9 +2,7 @@ import { test, expect } from "../support/fixtures"; import type { Locator } from "@playwright/test"; import { gotoWorkspace, - assertNewChatTileVisible, assertNewTabMenuTriggerVisible, - assertSingleNewTabButton, openNewTabMenuWithShortcut, clickNewChat, clickNewTerminal, @@ -126,6 +124,7 @@ test.describe("Tab creation", () => { page, }) => { await gotoWorkspace(page, workspace.workspaceId); + await assertNewTabMenuTriggerVisible(page); await pressNewTabShortcut(page); const newTabs = page .locator('[data-testid^="workspace-tab-tab_"]') @@ -190,11 +189,18 @@ test.describe("Tab creation", () => { await expect(agent).toBeFocused(); }); - test("clicking new agent tab creates a draft tab", async ({ page }) => { + test("the Agent menu creates a draft and shows its composer within the visual budget", async ({ + page, + }) => { await gotoWorkspace(page, workspace.workspaceId); - await clickNewChat(page); - + const elapsed = await measureTileTransition( + page, + () => clickNewChat(page), + composerLocator(page), + 10_000, + ); + expect(elapsed).toBeLessThan(3_000); await expectComposerVisible(page); const tabsAfter = await getTabTestIds(page); @@ -202,12 +208,17 @@ test.describe("Tab creation", () => { expect(draftCountAfter).toBeGreaterThanOrEqual(1); }); - test("clicking terminal button creates a standalone terminal", async ({ page }) => { + test("the Terminal menu creates a terminal within the visual budget", async ({ page }) => { test.setTimeout(45_000); await gotoWorkspace(page, workspace.workspaceId); - await clickNewTerminal(page); - + const elapsed = await measureTileTransition( + page, + () => clickNewTerminal(page), + terminalSurfaceLocator(page), + 20_000, + ); + expect(elapsed).toBeLessThan(5_000); await expectTerminalSurfaceVisible(page); const tabsAfter = await getTabTestIds(page); @@ -295,13 +306,6 @@ test.describe("Tab creation", () => { 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); - }); }); // ═══════════════════════════════════════════════════════════════════════════ @@ -339,37 +343,4 @@ test.describe("Tab transitions (no flash)", () => { expect(new Set(snapshots.map(tabIdentityKey)).size).toBeLessThanOrEqual(2); await expectTabTitleFits(page, "New Agent", { min: 64, 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); - }); }); diff --git a/packages/app/e2e/browser/projects-settings.spec.ts b/packages/app/e2e/browser/projects-settings.spec.ts index 3845e017a6..0612d5ce07 100644 --- a/packages/app/e2e/browser/projects-settings.spec.ts +++ b/packages/app/e2e/browser/projects-settings.spec.ts @@ -223,7 +223,10 @@ test.describe("Projects settings", () => { } }); - test("user edits worktree setup from the projects page", async ({ page, editableProject }) => { + test("edits worktree setup, navigates within the host, and removes a script", async ({ + page, + editableProject, + }) => { await openProjects(page); await openProjectSettings(page, editableProject.name); await expectNoUncommittedSetupWarning(page); @@ -236,15 +239,22 @@ test.describe("Projects settings", () => { await returnToProjectsList(page); await openProjectSettings(page, editableProject.name); await expectNoUncommittedSetupWarning(page); - }); - test("project navigation stays inside the selected host", async ({ page, editableProject }) => { - await openProjects(page); - await openProjectSettings(page, editableProject.name); - await expectProjectHostContextHidden(page); - await returnToProjectsList(page); - await openProjectSettings(page, editableProject.name); - await expectProjectSettingsHistoryRoundTrip(page, editableProject.name); + await test.step("navigation stays inside the selected host", async () => { + await expectProjectHostContextHidden(page); + await returnToProjectsList(page); + await openProjectSettings(page, editableProject.name); + await expectProjectSettingsHistoryRoundTrip(page, editableProject.name); + }); + + await test.step("removing a script leaves the committed setup unchanged", async () => { + await expectScriptRowCount(page, 1); + await removeProjectScript(page, "dev"); + await expectScriptRowCount(page, 0); + await expectEmptyScriptList(page); + await clickSaveProjectSettings(page); + await expectNoUncommittedSetupWarning(page); + }); }); test("user edits worktree setup on a non-GitHub remote project", async ({ @@ -259,18 +269,7 @@ test.describe("Projects settings", () => { await expectProjectConfigSaved(gitlabRemoteProject); }); - test("user renames a project from the edit sheet", async ({ page, editableProject }) => { - await openProjects(page); - await openProjectSettings(page, editableProject.name); - await openProjectEditSheet(page); - await fillProjectName(page, "Renamed project"); - await saveProjectEdits(page); - - await expectProjectEditSaved(page); - await expectProjectTitle(page, "Renamed project"); - }); - - test("reopening the edit sheet seeds from the saved project", async ({ + test("renames a project, reopens the saved edit sheet, and adds an icon", async ({ page, editableProject, }) => { @@ -279,26 +278,26 @@ test.describe("Projects settings", () => { await openProjectEditSheet(page); await fillProjectName(page, "Renamed project"); await saveProjectEdits(page); + await expectProjectEditSaved(page); + await expectProjectTitle(page, "Renamed project"); - await openProjectEditSheet(page); - - await expectProjectEditName(page, "Renamed project"); - await expectProjectEditsSaveDisabled(page); - }); - - test("user picks a custom project icon from a file", async ({ page, editableProject }) => { - await openProjects(page); - await openProjectSettings(page, editableProject.name); - await openProjectEditSheet(page); - await chooseProjectIconImage(page, { - name: "logo.png", - mimeType: "image/png", - buffer: PNG_1X1, + await test.step("reopening seeds the saved name with no unsaved changes", async () => { + await openProjectEditSheet(page); + await expectProjectEditName(page, "Renamed project"); + await expectProjectEditsSaveDisabled(page); }); - await saveProjectEdits(page); - await expectProjectEditSaved(page); + await test.step("an icon can be saved without another name change", async () => { + await chooseProjectIconImage(page, { + name: "logo.png", + mimeType: "image/png", + buffer: PNG_1X1, + }); + await saveProjectEdits(page); + await expectProjectEditSaved(page); + await expectProjectTitle(page, "Renamed project"); + }); }); test("user sets a project name and icon in one save", async ({ page, editableProject }) => { @@ -459,31 +458,4 @@ test.describe("Projects settings — error UX", () => { await expectNoEditableTarget(page); }); - - test("project detail does not render a second host selector", async ({ - page, - editableProject, - }) => { - await openProjects(page); - await openProjectSettings(page, editableProject.name); - - await expectProjectHostContextHidden(page); - }); - - test("script removal via kebab menu removes the row from the form", async ({ - page, - editableProject, - }) => { - await openProjects(page); - await openProjectSettings(page, editableProject.name); - - await expectScriptRowCount(page, 1); - - await removeProjectScript(page, "dev"); - - await expectScriptRowCount(page, 0); - await expectEmptyScriptList(page); - await clickSaveProjectSettings(page); - await expectNoUncommittedSetupWarning(page); - }); }); diff --git a/packages/app/e2e/browser/provider-settings-refresh.spec.ts b/packages/app/e2e/browser/provider-settings-refresh.spec.ts index efc4fc7f88..961b518435 100644 --- a/packages/app/e2e/browser/provider-settings-refresh.spec.ts +++ b/packages/app/e2e/browser/provider-settings-refresh.spec.ts @@ -116,28 +116,12 @@ async function exerciseProviderSettingsStack(page: Page) { await page.getByRole("button", { name: "Refresh", exact: true }).click(); await expectProviderSettingsVisible(page); + await expect(page.getByText("Updated just now", { exact: true })).toBeVisible({ + timeout: 30_000, + }); } test.describe("provider settings overlay stack", () => { - test("provider model update age says just now", async ({ page }) => { - const session = await seedMockAgentWorkspace({ - repoPrefix: "provider-relative-time-", - title: "Provider relative time", - }); - try { - await page.clock.install({ time: Date.now() }); - await page.setViewportSize(MOBILE_VIEWPORT); - await openAgentRoute(page, session); - await expectComposerVisible(page); - await openProviderSettingsFromModelSelector(page); - await expect(page.getByText("Updated just now", { exact: true })).toBeVisible({ - timeout: 30_000, - }); - } finally { - await session.cleanup(); - } - }); - test("provider settings covers the desktop model selector without closing it", async ({ page, }) => { diff --git a/packages/app/e2e/browser/workspace-cwd.spec.ts b/packages/app/e2e/browser/workspace-cwd.spec.ts deleted file mode 100644 index e702cc0f7d..0000000000 --- a/packages/app/e2e/browser/workspace-cwd.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { expect, test } from "../support/fixtures"; -import { clickNewChat, clickNewTerminal, gotoWorkspace } from "../support/helpers/launcher"; -import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; -import { - expectTerminalSurfaceVisible, - focusTerminalSurface, - typeInTerminal, - setupDeterministicPrompt, - waitForTerminalContent, -} from "../support/helpers/terminal-perf"; - -interface CreatedAgentCwdAssertion { - workspaceId: string; - cwd: string | null; -} - -async function fetchSingleAgentForWorkspace( - workspace: SeededWorkspace, -): Promise { - const agents = (await workspace.client.fetchAgents({ scope: "active" })).entries - .map((entry) => entry.agent) - .filter((agent) => agent.workspaceId === workspace.workspaceId); - if (agents.length !== 1) { - return null; - } - const [agent] = agents; - return { - workspaceId: workspace.workspaceId, - cwd: agent.cwd, - }; -} - -test.describe("Workspace cwd correctness", () => { - test("main checkout workspace opens terminals in the project root", async ({ - page, - withWorkspace, - }) => { - test.setTimeout(60_000); - - const workspace = await withWorkspace({ prefix: "workspace-cwd-main-" }); - await workspace.navigateTo(); - await clickNewTerminal(page); - - await expectTerminalSurfaceVisible(page); - await focusTerminalSurface(page); - await setupDeterministicPrompt(page, `PWD_READY_${Date.now()}`); - await typeInTerminal(page, "pwd\n"); - await waitForTerminalContent(page, (text) => text.includes(workspace.repoPath), 10_000); - }); - - test("draft tab creates an agent in the workspace cwd", async ({ page }) => { - test.setTimeout(60_000); - - const workspace = await seedWorkspace({ repoPrefix: "workspace-cwd-draft-agent-" }); - try { - await gotoWorkspace(page, workspace.workspaceId); - - await clickNewChat(page); - const composer = page.getByRole("textbox", { name: "Message agent..." }).first(); - const message = `cwd draft create ${Date.now()}`; - await expect(composer).toBeEditable({ timeout: 15_000 }); - await composer.fill(message); - await composer.press("Enter"); - await expect(page.getByText(message, { exact: true }).first()).toBeVisible({ - timeout: 30_000, - }); - - await expect(page.locator('[data-testid^="workspace-tab-agent_"]').first()).toBeVisible({ - timeout: 30_000, - }); - - await expect - .poll(() => fetchSingleAgentForWorkspace(workspace), { timeout: 30_000 }) - .toEqual({ - workspaceId: workspace.workspaceId, - cwd: workspace.repoPath, - }); - } finally { - await workspace.cleanup(); - } - }); - - test("worktree workspace opens terminals in the worktree directory", async ({ - page, - withWorkspace, - }) => { - test.setTimeout(90_000); - - const workspace = await withWorkspace({ worktree: true, prefix: "workspace-cwd-worktree-" }); - await workspace.navigateTo(); - await clickNewTerminal(page); - - await expectTerminalSurfaceVisible(page); - await focusTerminalSurface(page); - await setupDeterministicPrompt(page, `PWD_READY_${Date.now()}`); - await typeInTerminal(page, "pwd\n"); - await waitForTerminalContent(page, (text) => text.includes(workspace.repoPath), 10_000); - }); -}); diff --git a/packages/app/e2e/browser/workspace-lifecycle.spec.ts b/packages/app/e2e/browser/workspace-lifecycle.spec.ts index 7c7a20658f..83659cd001 100644 --- a/packages/app/e2e/browser/workspace-lifecycle.spec.ts +++ b/packages/app/e2e/browser/workspace-lifecycle.spec.ts @@ -3,6 +3,7 @@ import { createAgentChatFromLauncher, createStandaloneTerminalFromLauncher, expectTerminalCwd, + submitAgentChatAndExpectCwd, } from "../support/helpers/workspace-lifecycle"; test.describe("Workspace lifecycle", () => { @@ -19,8 +20,9 @@ test.describe("Workspace lifecycle", () => { const workspace = await withWorkspace({ prefix: "lifecycle-main-" }); await workspace.navigateTo(); - await test.step("creates an agent chat", async () => { + await test.step("submitting a draft creates an agent in the project root", async () => { await createAgentChatFromLauncher(page); + await submitAgentChatAndExpectCwd(page, workspace); }); await test.step("creates a terminal in the project root", async () => { diff --git a/packages/app/e2e/support/helpers/launcher.ts b/packages/app/e2e/support/helpers/launcher.ts index cf4a6b14a0..e967dc4d6d 100644 --- a/packages/app/e2e/support/helpers/launcher.ts +++ b/packages/app/e2e/support/helpers/launcher.ts @@ -80,13 +80,6 @@ export async function pressDirectNewTabShortcut(page: Page, key: string): Promis // ─── Tab bar assertions ─────────────────────────────────────────────────── -/** Assert the inline plus button is visible in the tab bar. */ -export async function assertNewChatTileVisible(page: Page): Promise { - await expect( - page.getByTestId("workspace-new-tab-button").filter({ visible: true }).first(), - ).toBeVisible(); -} - /** Assert the New tab button is visible in the tab bar. */ export async function assertNewTabMenuTriggerVisible(page: Page): Promise { await expect( @@ -132,13 +125,6 @@ export async function waitForTabWithTitle( ).toBeVisible({ timeout }); } -/** Assert the inline plus button is visible in the tab bar. */ -export async function assertSingleNewTabButton(page: Page): Promise { - const buttons = page.getByTestId("workspace-new-tab-button").filter({ visible: true }); - const count = await buttons.count(); - expect(count).toBeGreaterThanOrEqual(1); -} - // ─── No-flash measurement ────────────────────────────────────────────────── /** diff --git a/packages/app/e2e/support/helpers/workspace-lifecycle.ts b/packages/app/e2e/support/helpers/workspace-lifecycle.ts index 383872850a..dde9490dea 100644 --- a/packages/app/e2e/support/helpers/workspace-lifecycle.ts +++ b/packages/app/e2e/support/helpers/workspace-lifecycle.ts @@ -1,5 +1,6 @@ import { expect, type Page } from "@playwright/test"; import { clickNewChat, clickNewTerminal } from "./launcher"; +import { connectSeedClient } from "./seed-client"; import { setupDeterministicPrompt, waitForTerminalContent } from "./terminal-perf"; function terminalSurface(page: Page) { @@ -30,3 +31,35 @@ export async function createAgentChatFromLauncher(page: Page): Promise { await expect(composerInput(page)).toBeEditable({ timeout: 15_000 }); await expect(page.getByTestId("agent-loading")).toHaveCount(0); } + +export async function submitAgentChatAndExpectCwd( + page: Page, + workspace: { workspaceId: string; repoPath: string }, +): Promise { + const client = await connectSeedClient(); + try { + const message = "cwd draft create"; + await composerInput(page).fill(message); + await composerInput(page).press("Enter"); + await expect(page.getByText(message, { exact: true }).first()).toBeVisible({ + timeout: 30_000, + }); + await expect(page.locator('[data-testid^="workspace-tab-agent_"]').first()).toBeVisible({ + timeout: 30_000, + }); + await expect + .poll( + async () => { + const agents = await client.fetchAgents({ scope: "active" }); + return agents.entries + .map((entry) => entry.agent) + .filter((agent) => agent.workspaceId === workspace.workspaceId) + .map((agent) => ({ workspaceId: agent.workspaceId, cwd: agent.cwd })); + }, + { timeout: 30_000 }, + ) + .toEqual([{ workspaceId: workspace.workspaceId, cwd: workspace.repoPath }]); + } finally { + await client.close(); + } +}