refactor(app/e2e): eliminate raw locators from all offending spec bodies (cluster #13) (#727)

Rewrites 7 spec files to use DSL helpers throughout — zero raw
page.locator/getByText/getByTestId in test() bodies. Adds 30+ new
helper primitives across 7 existing helper modules.
This commit is contained in:
Mohamed Boudra
2026-05-05 01:02:50 +08:00
parent 685e86cffc
commit 4d102df2cf
14 changed files with 367 additions and 267 deletions

View File

@@ -4,6 +4,17 @@ function composerInput(page: Page) {
return page.getByRole("textbox", { name: "Message agent..." }).first(); return page.getByRole("textbox", { name: "Message agent..." }).first();
} }
export function composerLocator(page: Page) {
return composerInput(page);
}
export async function expectComposerVisible(
page: Page,
options?: { timeout?: number },
): Promise<void> {
await expect(composerInput(page)).toBeVisible({ timeout: options?.timeout ?? 15_000 });
}
export async function submitMessage(page: Page, text: string): Promise<void> { export async function submitMessage(page: Page, text: string): Promise<void> {
const input = composerInput(page); const input = composerInput(page);
await expect(input).toBeEditable({ timeout: 30_000 }); await expect(input).toBeEditable({ timeout: 30_000 });

View File

@@ -174,6 +174,19 @@ export async function sampleTabsDuringTransition(
return snapshots; return snapshots;
} }
export function terminalSurfaceLocator(page: Page) {
return page.locator('[data-testid="terminal-surface"]').first();
}
export async function expectAgentTabActive(page: Page, agentId: string): Promise<void> {
const tabTestId = `workspace-tab-agent_${agentId}`;
await expect(page.getByTestId(tabTestId).filter({ visible: true })).toHaveAttribute(
"aria-selected",
"true",
);
await expect(getActiveTabTestId(page)).resolves.toBe(tabTestId);
}
// ─── Workspace setup ─────────────────────────────────────────────────────── // ─── Workspace setup ───────────────────────────────────────────────────────
/** Create a temp git repo and return its path with a cleanup function. */ /** Create a temp git repo and return its path with a cleanup function. */

View File

@@ -165,3 +165,74 @@ export async function expectAboutContent(page: Page): Promise<void> {
export async function expectGeneralContent(page: Page): Promise<void> { export async function expectGeneralContent(page: Page): Promise<void> {
await expect(page.getByText("Theme", { exact: true }).first()).toBeVisible(); await expect(page.getByText("Theme", { exact: true }).first()).toBeVisible();
} }
export async function expectHostLabelDisplayed(page: Page): Promise<void> {
await expect(page.getByTestId("host-page-label-edit-button")).toBeVisible();
await expect(page.getByTestId("host-page-label-input")).toHaveCount(0);
}
export async function clickEditHostLabel(page: Page): Promise<void> {
await page.getByTestId("host-page-label-edit-button").click();
}
export async function expectHostLabelEditMode(page: Page, expectedLabel: string): Promise<void> {
await expect(page.getByTestId("host-page-label-input")).toBeVisible();
await expect(page.getByTestId("host-page-label-input")).toHaveValue(expectedLabel);
await expect(page.getByTestId("host-page-label-save")).toBeVisible();
}
export async function expectHostConnectionsCard(page: Page, port: string): Promise<void> {
const card = page.getByTestId("host-page-connections-card");
await expect(card).toBeVisible();
await expect(page.getByText("Connections", { exact: true })).toBeVisible();
await expect(
card.getByText(new RegExp(`TCP \\((localhost|127\\.0\\.0\\.1):${port}\\)`)),
).toBeVisible();
}
export async function expectHostInjectMcpCard(page: Page): Promise<void> {
const card = page.getByTestId("host-page-inject-mcp-card");
await expect(card).toBeVisible();
await expect(card.getByRole("switch", { name: "Inject Paseo tools" })).toBeVisible();
}
export async function expectHostActionCards(page: Page): Promise<void> {
await expect(page.getByTestId("host-page-restart-card")).toBeVisible();
await expect(page.getByTestId("host-page-restart-button")).toBeVisible();
await expect(page.getByTestId("host-page-providers-card")).toBeVisible();
await expect(page.getByTestId("host-page-remove-host-card")).toBeVisible();
await expect(page.getByTestId("host-page-remove-host-button")).toBeVisible();
}
export async function expectHostNoLocalOnlyRows(page: Page): Promise<void> {
await expect(page.getByTestId("host-page-pair-device-row")).toHaveCount(0);
await expect(page.getByTestId("host-page-daemon-lifecycle-card")).toHaveCount(0);
}
export async function expectRetiredSidebarSectionsAbsent(page: Page): Promise<void> {
const sidebar = page.getByTestId("settings-sidebar");
await expect(sidebar).toBeVisible();
await expect(sidebar.getByRole("button", { name: "Hosts", exact: true })).toHaveCount(0);
await expect(sidebar.getByRole("button", { name: "Providers", exact: true })).toHaveCount(0);
await expect(sidebar.getByRole("button", { name: "Pair device", exact: true })).toHaveCount(0);
await expect(sidebar.getByRole("button", { name: "Daemon", exact: true })).toHaveCount(0);
await expect(sidebar.getByRole("button", { name: "General", exact: true })).toBeVisible();
await expect(sidebar.getByRole("button", { name: "Diagnostics", exact: true })).toBeVisible();
await expect(sidebar.getByRole("button", { name: "About", exact: true })).toBeVisible();
}
export async function expectHostPageVisible(page: Page, serverId: string): Promise<void> {
await expect(page.getByTestId(`settings-host-page-${serverId}`)).toBeVisible();
}
export async function expectLocalHostEntryFirst(page: Page, serverId: string): Promise<void> {
const sidebar = page.getByTestId("settings-sidebar");
await expect(sidebar).toBeVisible({ timeout: 15_000 });
await expect(sidebar.locator('[data-testid^="settings-host-entry-"]').first()).toHaveAttribute(
"data-testid",
`settings-host-entry-${serverId}`,
);
const localHostEntry = page.getByTestId(`settings-host-entry-${serverId}`);
await expect(localHostEntry.getByTestId("settings-host-local-marker")).toBeVisible();
await expect(localHostEntry.getByText("Local", { exact: true })).toBeVisible();
}

View File

@@ -1,4 +1,4 @@
import type { Page } from "@playwright/test"; import { expect, type Page } from "@playwright/test";
import path from "node:path"; import path from "node:path";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
@@ -263,6 +263,34 @@ export async function measureKeystrokeLatency(page: Page, char: string): Promise
); );
} }
export async function expectTerminalSurfaceVisible(
page: Page,
options?: { timeout?: number },
): Promise<void> {
await expect(page.locator('[data-testid="terminal-surface"]').first()).toBeVisible({
timeout: options?.timeout ?? 20_000,
});
}
export async function focusTerminalSurface(page: Page): Promise<void> {
await expectTerminalSurfaceVisible(page);
await page.locator('[data-testid="terminal-surface"]').first().click();
}
export async function typeInTerminal(page: Page, text: string): Promise<void> {
await page
.locator('[data-testid="terminal-surface"]')
.first()
.pressSequentially(text, { delay: 0 });
}
export async function waitForTerminalAttached(page: Page): Promise<void> {
await page
.locator('[data-testid="terminal-attach-loading"]')
.waitFor({ state: "hidden", timeout: 10_000 })
.catch(() => undefined);
}
export function computePercentile(samples: number[], p: number): number { export function computePercentile(samples: number[], p: number): number {
const sorted = [...samples].sort((a, b) => a - b); const sorted = [...samples].sort((a, b) => a - b);
const index = Math.ceil((p / 100) * sorted.length) - 1; const index = Math.ceil((p / 100) * sorted.length) - 1;

View File

@@ -6,6 +6,7 @@ import { expect, type Page } from "@playwright/test";
import { parseHostWorkspaceRouteFromPathname } from "../../src/utils/host-routes"; import { parseHostWorkspaceRouteFromPathname } from "../../src/utils/host-routes";
import { gotoAppShell } from "./app"; import { gotoAppShell } from "./app";
import { createNodeWebSocketFactory, type NodeWebSocketFactory } from "./node-ws-factory"; import { createNodeWebSocketFactory, type NodeWebSocketFactory } from "./node-ws-factory";
import { switchWorkspaceViaSidebar } from "./workspace-ui";
import type { SessionOutboundMessage } from "@server/shared/messages"; import type { SessionOutboundMessage } from "@server/shared/messages";
interface WorkspaceSetupDaemonClient { interface WorkspaceSetupDaemonClient {
@@ -315,6 +316,30 @@ export async function fetchWorkspaceById(
return workspace; return workspace;
} }
export async function navigateToWorkspaceViaSidebar(
page: Page,
workspaceId: string,
): Promise<void> {
const serverId = process.env.E2E_SERVER_ID;
if (!serverId) {
throw new Error("E2E_SERVER_ID is not set.");
}
await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: workspaceId });
}
export async function openWorkspaceScriptsMenu(page: Page): Promise<void> {
await page.getByTestId("workspace-scripts-button").click();
await expect(page.getByTestId("workspace-scripts-menu")).toBeVisible({ timeout: 10_000 });
}
export async function startWorkspaceScriptFromMenu(page: Page, scriptName: string): Promise<void> {
await page.getByTestId(`workspace-scripts-start-${scriptName}`).click();
}
export async function closeWorkspaceScriptsMenu(page: Page): Promise<void> {
await page.getByTestId("workspace-scripts-menu-backdrop").click();
}
export async function waitForWorkspaceSetupProgress( export async function waitForWorkspaceSetupProgress(
client: WorkspaceSetupDaemonClient, client: WorkspaceSetupDaemonClient,
predicate: (payload: WorkspaceSetupProgressPayload) => boolean, predicate: (payload: WorkspaceSetupProgressPayload) => boolean,

View File

@@ -70,6 +70,29 @@ export async function ensureWorkspaceAgentPaneVisible(page: Page): Promise<void>
} }
} }
export async function expectWorkspaceTabsAbsent(page: Page): Promise<void> {
await expect(page.getByTestId("workspace-tabs-row")).toHaveCount(0);
}
export async function expectNoTerminalTabs(page: Page): Promise<void> {
await expect(page.locator('[data-testid^="workspace-tab-terminal_"]')).toHaveCount(0);
}
export async function clickFirstTerminalTab(
page: Page,
options?: { timeout?: number },
): Promise<void> {
const tab = page.locator('[data-testid^="workspace-tab-terminal_"]').first();
await expect(tab).toBeVisible({ timeout: options?.timeout ?? 30_000 });
await tab.click();
}
export async function expectFirstTerminalTabContains(page: Page, text: string): Promise<void> {
await expect(page.locator('[data-testid^="workspace-tab-terminal_"]').first()).toContainText(
text,
);
}
export async function sampleWorkspaceTabIds( export async function sampleWorkspaceTabIds(
page: Page, page: Page,
options: { durationMs?: number; intervalMs?: number } = {}, options: { durationMs?: number; intervalMs?: number } = {},

View File

@@ -114,6 +114,49 @@ export async function expectWorkspaceHeader(
}); });
} }
export async function expectReconnectingToastVisible(
page: Page,
options?: { timeout?: number },
): Promise<void> {
await expect(page.getByTestId("agent-reconnecting-toast")).toBeVisible({
timeout: options?.timeout ?? 30_000,
});
}
export async function expectReconnectingToastGone(
page: Page,
options?: { timeout?: number },
): Promise<void> {
await expect(page.getByTestId("agent-reconnecting-toast")).toHaveCount(0, {
timeout: options?.timeout ?? 30_000,
});
}
export async function expectHostConnectingOrOffline(
page: Page,
options?: { timeout?: number },
): Promise<void> {
await expect(
page.getByText(/^Connecting$|localhost is offline|Cannot reach localhost/i),
).toBeVisible({ timeout: options?.timeout ?? 30_000 });
}
export async function expectMenuButtonVisible(page: Page): Promise<void> {
await expect(page.getByTestId("menu-button")).toBeVisible();
}
export async function expectWorkspaceHeaderAbsent(page: Page): Promise<void> {
await expect(page.getByTestId("workspace-header-title")).toHaveCount(0);
}
export function workspaceDeckEntryLocator(page: Page, serverId: string, workspaceId: string) {
return page.getByTestId(`workspace-deck-entry-${serverId}:${workspaceId}`);
}
export async function expectWorkspaceDeckEntryCount(page: Page, count: number): Promise<void> {
await expect(page.locator('[data-testid^="workspace-deck-entry-"]')).toHaveCount(count);
}
export async function seedWorkspaceActivity(page: Page, marker: string): Promise<void> { export async function seedWorkspaceActivity(page: Page, marker: string): Promise<void> {
const input = page.getByRole("textbox", { name: "Message agent..." }); const input = page.getByRole("textbox", { name: "Message agent..." });
await expect(input).toBeEditable({ timeout: 30_000 }); await expect(input).toBeEditable({ timeout: 30_000 });

View File

@@ -13,7 +13,10 @@ import {
waitForTabWithTitle, waitForTabWithTitle,
measureTileTransition, measureTileTransition,
sampleTabsDuringTransition, sampleTabsDuringTransition,
terminalSurfaceLocator,
} from "./helpers/launcher"; } from "./helpers/launcher";
import { expectComposerVisible, composerLocator } from "./helpers/composer";
import { expectTerminalSurfaceVisible } from "./helpers/terminal-perf";
import { import {
connectTerminalClient, connectTerminalClient,
setupDeterministicPrompt, setupDeterministicPrompt,
@@ -49,9 +52,7 @@ test.describe("Tab creation", () => {
await pressNewTabShortcut(page); await pressNewTabShortcut(page);
// Should show the composer directly (no launcher panel) await expectComposerVisible(page);
const composer = page.getByRole("textbox", { name: "Message agent..." });
await expect(composer.first()).toBeVisible({ timeout: 15_000 });
}); });
test("opening two new tabs creates two draft tabs", async ({ page }) => { test("opening two new tabs creates two draft tabs", async ({ page }) => {
@@ -78,9 +79,7 @@ test.describe("Tab creation", () => {
await clickNewChat(page); await clickNewChat(page);
// Draft composer should appear (the agent message input) await expectComposerVisible(page);
const composer = page.getByRole("textbox", { name: "Message agent..." });
await expect(composer.first()).toBeVisible({ timeout: 15_000 });
const tabsAfter = await getTabTestIds(page); const tabsAfter = await getTabTestIds(page);
const draftCountAfter = tabsAfter.filter((id) => id.includes("draft")).length; const draftCountAfter = tabsAfter.filter((id) => id.includes("draft")).length;
@@ -93,9 +92,7 @@ test.describe("Tab creation", () => {
await clickNewTerminal(page); await clickNewTerminal(page);
// Terminal surface should appear await expectTerminalSurfaceVisible(page);
const terminal = page.locator('[data-testid="terminal-surface"]');
await expect(terminal.first()).toBeVisible({ timeout: 20_000 });
const tabsAfter = await getTabTestIds(page); const tabsAfter = await getTabTestIds(page);
const terminalTabs = tabsAfter.filter((id) => id.includes("terminal")); const terminalTabs = tabsAfter.filter((id) => id.includes("terminal"));
@@ -142,17 +139,16 @@ test.describe("Terminal title propagation", () => {
await gotoWorkspace(page, workspaceId); await gotoWorkspace(page, workspaceId);
await clickNewTerminal(page); await clickNewTerminal(page);
const terminal = page.locator('[data-testid="terminal-surface"]'); await expectTerminalSurfaceVisible(page);
await expect(terminal.first()).toBeVisible({ timeout: 20_000 }); await terminalSurfaceLocator(page).click();
await terminal.first().click();
await setupDeterministicPrompt(page); await setupDeterministicPrompt(page);
// Send OSC 0 (set window title) escape sequence // Send OSC 0 (set window title) escape sequence
const testTitle = `E2E-Title-${Date.now()}`; const testTitle = `E2E-Title-${Date.now()}`;
await terminal await terminalSurfaceLocator(page).pressSequentially(`printf '\\033]0;${testTitle}\\007'\n`, {
.first() delay: 0,
.pressSequentially(`printf '\\033]0;${testTitle}\\007'\n`, { delay: 0 }); });
// Wait for the tab to reflect the new title // Wait for the tab to reflect the new title
await waitForTabWithTitle(page, testTitle, 15_000); await waitForTabWithTitle(page, testTitle, 15_000);
@@ -172,22 +168,22 @@ test.describe("Terminal title propagation", () => {
await gotoWorkspace(page, workspaceId); await gotoWorkspace(page, workspaceId);
await clickNewTerminal(page); await clickNewTerminal(page);
const terminal = page.locator('[data-testid="terminal-surface"]'); await expectTerminalSurfaceVisible(page);
await expect(terminal.first()).toBeVisible({ timeout: 20_000 }); await terminalSurfaceLocator(page).click();
await terminal.first().click();
await setupDeterministicPrompt(page); await setupDeterministicPrompt(page);
// Fire many rapid title changes — only the last should stick // Fire many rapid title changes — only the last should stick
const finalTitle = `Final-${Date.now()}`; const finalTitle = `Final-${Date.now()}`;
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
await terminal await terminalSurfaceLocator(page).pressSequentially(`printf '\\033]0;Rapid-${i}\\007'\n`, {
.first() delay: 0,
.pressSequentially(`printf '\\033]0;Rapid-${i}\\007'\n`, { delay: 0 }); });
} }
await terminal await terminalSurfaceLocator(page).pressSequentially(
.first() `printf '\\033]0;${finalTitle}\\007'\n`,
.pressSequentially(`printf '\\033]0;${finalTitle}\\007'\n`, { delay: 0 }); { delay: 0 },
);
// The tab should eventually settle on the final title // The tab should eventually settle on the final title
await waitForTabWithTitle(page, finalTitle, 15_000); await waitForTabWithTitle(page, finalTitle, 15_000);
@@ -227,11 +223,10 @@ test.describe("Tab transitions (no flash)", () => {
test.setTimeout(30_000); test.setTimeout(30_000);
await gotoWorkspace(page, workspaceId); await gotoWorkspace(page, workspaceId);
const terminal = page.locator('[data-testid="terminal-surface"]');
const elapsed = await measureTileTransition( const elapsed = await measureTileTransition(
page, page,
() => clickNewTerminal(page), () => clickNewTerminal(page),
terminal.first(), terminalSurfaceLocator(page),
20_000, 20_000,
); );
@@ -244,9 +239,12 @@ test.describe("Tab transitions (no flash)", () => {
test("New agent tab click shows composer without flash", async ({ page }) => { test("New agent tab click shows composer without flash", async ({ page }) => {
await gotoWorkspace(page, workspaceId); await gotoWorkspace(page, workspaceId);
const composer = page.getByRole("textbox", { name: "Message agent..." }).first(); const elapsed = await measureTileTransition(
page,
const elapsed = await measureTileTransition(page, () => clickNewChat(page), composer, 10_000); () => clickNewChat(page),
composerLocator(page),
10_000,
);
// Draft creation is fully in-memory — should be fast // Draft creation is fully in-memory — should be fast
// We use a generous budget here because CI can be slow, but the key assertion // We use a generous budget here because CI can be slow, but the key assertion

View File

@@ -1,7 +1,20 @@
import { test, expect, type Page } from "./fixtures"; import { test } from "./fixtures";
import { gotoAppShell, openSettings } from "./helpers/app"; import { gotoAppShell, openSettings } from "./helpers/app";
import { TEST_HOST_LABEL } from "./helpers/daemon-registry"; import { TEST_HOST_LABEL } from "./helpers/daemon-registry";
import { expectSettingsHeader, openSettingsHost } from "./helpers/settings"; import {
expectSettingsHeader,
openSettingsHost,
expectHostLabelDisplayed,
clickEditHostLabel,
expectHostLabelEditMode,
expectHostConnectionsCard,
expectHostInjectMcpCard,
expectHostActionCards,
expectHostNoLocalOnlyRows,
expectRetiredSidebarSectionsAbsent,
expectHostPageVisible,
expectLocalHostEntryFirst,
} from "./helpers/settings";
function getSeededServerId(): string { function getSeededServerId(): string {
const serverId = process.env.E2E_SERVER_ID; const serverId = process.env.E2E_SERVER_ID;
@@ -19,12 +32,6 @@ function getSeededDaemonPort(): string {
return port; return port;
} }
async function expectHostLabelHeader(page: Page) {
await expectSettingsHeader(page, TEST_HOST_LABEL);
await expect(page.getByTestId("host-page-label-edit-button")).toBeVisible();
await expect(page.getByTestId("host-page-label-input")).toHaveCount(0);
}
test.describe("Settings host page", () => { test.describe("Settings host page", () => {
test("host page shows seeded label, connection endpoint, inject MCP toggle, and all action rows", async ({ test("host page shows seeded label, connection endpoint, inject MCP toggle, and all action rows", async ({
page, page,
@@ -36,26 +43,11 @@ test.describe("Settings host page", () => {
await openSettings(page); await openSettings(page);
await openSettingsHost(page, serverId); await openSettingsHost(page, serverId);
// Label renders in the detail header with a pencil edit affordance; the input is hidden until edit. await expectSettingsHeader(page, TEST_HOST_LABEL);
await expectHostLabelHeader(page); await expectHostLabelDisplayed(page);
await expectHostConnectionsCard(page, port);
// Connections is its own section with a "Connections" heading and the seeded endpoint row. await expectHostInjectMcpCard(page);
const connectionsCard = page.getByTestId("host-page-connections-card"); await expectHostActionCards(page);
await expect(connectionsCard).toBeVisible();
await expect(page.getByText("Connections", { exact: true })).toBeVisible();
await expect(
connectionsCard.getByText(new RegExp(`TCP \\((localhost|127\\.0\\.0\\.1):${port}\\)`)),
).toBeVisible();
const injectMcpCard = page.getByTestId("host-page-inject-mcp-card");
await expect(injectMcpCard).toBeVisible();
await expect(injectMcpCard.getByRole("switch", { name: "Inject Paseo tools" })).toBeVisible();
await expect(page.getByTestId("host-page-restart-card")).toBeVisible();
await expect(page.getByTestId("host-page-restart-button")).toBeVisible();
await expect(page.getByTestId("host-page-providers-card")).toBeVisible();
await expect(page.getByTestId("host-page-remove-host-card")).toBeVisible();
await expect(page.getByTestId("host-page-remove-host-button")).toBeVisible();
}); });
test("clicking the label pencil reveals the inline editor", async ({ page }) => { test("clicking the label pencil reveals the inline editor", async ({ page }) => {
@@ -65,13 +57,9 @@ test.describe("Settings host page", () => {
await openSettings(page); await openSettings(page);
await openSettingsHost(page, serverId); await openSettingsHost(page, serverId);
await expect(page.getByTestId("host-page-label-input")).toHaveCount(0); await expectHostLabelDisplayed(page);
await clickEditHostLabel(page);
await page.getByTestId("host-page-label-edit-button").click(); await expectHostLabelEditMode(page, TEST_HOST_LABEL);
await expect(page.getByTestId("host-page-label-input")).toBeVisible();
await expect(page.getByTestId("host-page-label-input")).toHaveValue(TEST_HOST_LABEL);
await expect(page.getByTestId("host-page-label-save")).toBeVisible();
}); });
test("host page does not render pair-device or daemon-lifecycle rows for a remote daemon", async ({ test("host page does not render pair-device or daemon-lifecycle rows for a remote daemon", async ({
@@ -84,28 +72,14 @@ test.describe("Settings host page", () => {
await openSettingsHost(page, serverId); await openSettingsHost(page, serverId);
// TODO: add local-daemon fixture for positive Pair/Daemon coverage. // TODO: add local-daemon fixture for positive Pair/Daemon coverage.
// Pair-device now lives behind a row that only the local host sees await expectHostNoLocalOnlyRows(page);
// (gated by useIsLocalDaemon); the seeded host is remote, so it must
// not appear. The daemon-lifecycle card is still local-host only.
await expect(page.getByTestId("host-page-pair-device-row")).toHaveCount(0);
await expect(page.getByTestId("host-page-daemon-lifecycle-card")).toHaveCount(0);
}); });
test("settings sidebar does not expose retired top-level sections", async ({ page }) => { test("settings sidebar does not expose retired top-level sections", async ({ page }) => {
await gotoAppShell(page); await gotoAppShell(page);
await openSettings(page); await openSettings(page);
const sidebar = page.getByTestId("settings-sidebar"); await expectRetiredSidebarSectionsAbsent(page);
await expect(sidebar).toBeVisible();
await expect(sidebar.getByRole("button", { name: "Hosts", exact: true })).toHaveCount(0);
await expect(sidebar.getByRole("button", { name: "Providers", exact: true })).toHaveCount(0);
await expect(sidebar.getByRole("button", { name: "Pair device", exact: true })).toHaveCount(0);
await expect(sidebar.getByRole("button", { name: "Daemon", exact: true })).toHaveCount(0);
await expect(sidebar.getByRole("button", { name: "General", exact: true })).toBeVisible();
await expect(sidebar.getByRole("button", { name: "Diagnostics", exact: true })).toBeVisible();
await expect(sidebar.getByRole("button", { name: "About", exact: true })).toBeVisible();
}); });
test("navigating to /settings/hosts/[serverId] directly renders the host page", async ({ test("navigating to /settings/hosts/[serverId] directly renders the host page", async ({
@@ -116,9 +90,10 @@ test.describe("Settings host page", () => {
await gotoAppShell(page); await gotoAppShell(page);
await page.goto(`/settings/hosts/${encodeURIComponent(serverId)}`); await page.goto(`/settings/hosts/${encodeURIComponent(serverId)}`);
await expect(page.getByTestId(`settings-host-page-${serverId}`)).toBeVisible(); await expectHostPageVisible(page, serverId);
await expectHostLabelHeader(page); await expectSettingsHeader(page, TEST_HOST_LABEL);
await expect(page.getByTestId("host-page-remove-host-card")).toBeVisible(); await expectHostLabelDisplayed(page);
await expectHostActionCards(page);
}); });
test("sidebar pins the local daemon host first with a Local marker", async ({ page }) => { test("sidebar pins the local daemon host first with a Local marker", async ({ page }) => {
@@ -160,18 +135,6 @@ test.describe("Settings host page", () => {
await gotoAppShell(page); await gotoAppShell(page);
await openSettings(page); await openSettings(page);
await expectLocalHostEntryFirst(page, serverId);
const sidebar = page.getByTestId("settings-sidebar");
await expect(sidebar).toBeVisible({ timeout: 15000 });
const hostEntries = sidebar.locator('[data-testid^="settings-host-entry-"]');
await expect(hostEntries.first()).toHaveAttribute(
"data-testid",
`settings-host-entry-${serverId}`,
);
const localHostEntry = page.getByTestId(`settings-host-entry-${serverId}`);
await expect(localHostEntry.getByTestId("settings-host-local-marker")).toBeVisible();
await expect(localHostEntry.getByText("Local", { exact: true })).toBeVisible();
}); });
}); });

View File

@@ -1,12 +1,12 @@
import { buildHostAgentDetailRoute, buildHostWorkspaceRoute } from "@/utils/host-routes"; import { buildHostAgentDetailRoute, buildHostWorkspaceRoute } from "@/utils/host-routes";
import { expect, test, type Page } from "./fixtures"; import { expect, test } from "./fixtures";
import { import {
archiveAgentFromDaemon, archiveAgentFromDaemon,
connectArchiveTabDaemonClient, connectArchiveTabDaemonClient,
createIdleAgent, createIdleAgent,
openWorkspaceWithAgents, openWorkspaceWithAgents,
} from "./helpers/archive-tab"; } from "./helpers/archive-tab";
import { getActiveTabTestId, waitForTabBar } from "./helpers/launcher"; import { waitForTabBar, expectAgentTabActive } from "./helpers/launcher";
import { createTempGitRepo } from "./helpers/workspace"; import { createTempGitRepo } from "./helpers/workspace";
function getServerId(): string { function getServerId(): string {
@@ -17,12 +17,15 @@ function getServerId(): string {
return serverId; return serverId;
} }
async function pressSettingsToggleShortcut(page: Page) { async function pressSettingsToggleShortcut(page: import("@playwright/test").Page) {
const modifier = process.platform === "darwin" ? "Meta" : "Control"; const modifier = process.platform === "darwin" ? "Meta" : "Control";
await page.keyboard.press(`${modifier}+Comma`); await page.keyboard.press(`${modifier}+Comma`);
} }
async function expectSendBehavior(page: Page, expected: "interrupt" | "queue") { async function expectSendBehavior(
page: import("@playwright/test").Page,
expected: "interrupt" | "queue",
) {
await expect await expect
.poll(async () => { .poll(async () => {
const raw = await page.evaluate(() => localStorage.getItem("@paseo:app-settings")); const raw = await page.evaluate(() => localStorage.getItem("@paseo:app-settings"));
@@ -35,12 +38,11 @@ async function expectSendBehavior(page: Page, expected: "interrupt" | "queue") {
} }
async function openAgentRouteAndExpectFocused(input: { async function openAgentRouteAndExpectFocused(input: {
page: Page; page: import("@playwright/test").Page;
serverId: string; serverId: string;
workspaceId: string; workspaceId: string;
agentId: string; agentId: string;
}) { }) {
const expectedActiveTabId = `workspace-tab-agent_${input.agentId}`;
await input.page.goto( await input.page.goto(
buildHostAgentDetailRoute(input.serverId, input.agentId, input.workspaceId), buildHostAgentDetailRoute(input.serverId, input.agentId, input.workspaceId),
); );
@@ -49,10 +51,7 @@ async function openAgentRouteAndExpectFocused(input: {
{ timeout: 60_000 }, { timeout: 60_000 },
); );
await waitForTabBar(input.page); await waitForTabBar(input.page);
await expect( await expectAgentTabActive(input.page, input.agentId);
input.page.getByTestId(expectedActiveTabId).filter({ visible: true }),
).toHaveAttribute("aria-selected", "true");
await expect(getActiveTabTestId(input.page)).resolves.toBe(expectedActiveTabId);
} }
test.describe("Settings toggle tab regression", () => { test.describe("Settings toggle tab regression", () => {
@@ -79,13 +78,7 @@ test.describe("Settings toggle tab regression", () => {
await openWorkspaceWithAgents(page, [firstAgent, secondAgent]); await openWorkspaceWithAgents(page, [firstAgent, secondAgent]);
await waitForTabBar(page); await waitForTabBar(page);
await expectAgentTabActive(page, secondAgent.id);
const expectedActiveTabId = `workspace-tab-agent_${secondAgent.id}`;
await expect(page.getByTestId(expectedActiveTabId).filter({ visible: true })).toHaveAttribute(
"aria-selected",
"true",
);
await expect(getActiveTabTestId(page)).resolves.toBe(expectedActiveTabId);
await pressSettingsToggleShortcut(page); await pressSettingsToggleShortcut(page);
await expect(page).toHaveURL(/\/settings\/general$/); await expect(page).toHaveURL(/\/settings\/general$/);
@@ -98,19 +91,11 @@ test.describe("Settings toggle tab regression", () => {
await pressSettingsToggleShortcut(page); await pressSettingsToggleShortcut(page);
await expect(page).toHaveURL(buildHostWorkspaceRoute(serverId, repo.path)); await expect(page).toHaveURL(buildHostWorkspaceRoute(serverId, repo.path));
await waitForTabBar(page); await waitForTabBar(page);
await expect(page.getByTestId(expectedActiveTabId).filter({ visible: true })).toHaveAttribute( await expectAgentTabActive(page, secondAgent.id);
"aria-selected",
"true",
);
await expect(getActiveTabTestId(page)).resolves.toBe(expectedActiveTabId);
await page.reload(); await page.reload();
await waitForTabBar(page); await waitForTabBar(page);
await expect(page.getByTestId(expectedActiveTabId).filter({ visible: true })).toHaveAttribute( await expectAgentTabActive(page, secondAgent.id);
"aria-selected",
"true",
);
await expect(getActiveTabTestId(page)).resolves.toBe(expectedActiveTabId);
} finally { } finally {
for (const agentId of agentIds) { for (const agentId of agentIds) {
await archiveAgentFromDaemon(client, agentId).catch(() => undefined); await archiveAgentFromDaemon(client, agentId).catch(() => undefined);
@@ -152,14 +137,10 @@ test.describe("Settings toggle tab regression", () => {
agentId: secondAgent.id, agentId: secondAgent.id,
}); });
const expectedActiveTabId = `workspace-tab-agent_${secondAgent.id}`;
for (let attempt = 0; attempt < 5; attempt += 1) { for (let attempt = 0; attempt < 5; attempt += 1) {
await page.reload(); await page.reload();
await waitForTabBar(page); await waitForTabBar(page);
await expect( await expectAgentTabActive(page, secondAgent.id);
page.getByTestId(expectedActiveTabId).filter({ visible: true }),
).toHaveAttribute("aria-selected", "true");
await expect(getActiveTabTestId(page)).resolves.toBe(expectedActiveTabId);
} }
} finally { } finally {
for (const agentId of agentIds) { for (const agentId of agentIds) {

View File

@@ -1,6 +1,12 @@
import { expect, test } from "./fixtures"; import { test } from "./fixtures";
import { clickNewTerminal } from "./helpers/launcher"; import { clickNewTerminal } from "./helpers/launcher";
import { setupDeterministicPrompt, waitForTerminalContent } from "./helpers/terminal-perf"; import {
expectTerminalSurfaceVisible,
focusTerminalSurface,
typeInTerminal,
setupDeterministicPrompt,
waitForTerminalContent,
} from "./helpers/terminal-perf";
test.describe("Workspace cwd correctness", () => { test.describe("Workspace cwd correctness", () => {
test("main checkout workspace opens terminals in the project root", async ({ test("main checkout workspace opens terminals in the project root", async ({
@@ -13,13 +19,10 @@ test.describe("Workspace cwd correctness", () => {
await workspace.navigateTo(); await workspace.navigateTo();
await clickNewTerminal(page); await clickNewTerminal(page);
const terminal = page.locator('[data-testid="terminal-surface"]'); await expectTerminalSurfaceVisible(page);
await expect(terminal.first()).toBeVisible({ timeout: 20_000 }); await focusTerminalSurface(page);
await terminal.first().click();
await setupDeterministicPrompt(page, `PWD_READY_${Date.now()}`); await setupDeterministicPrompt(page, `PWD_READY_${Date.now()}`);
await terminal.first().pressSequentially("pwd\n", { delay: 0 }); await typeInTerminal(page, "pwd\n");
await waitForTerminalContent(page, (text) => text.includes(workspace.repoPath), 10_000); await waitForTerminalContent(page, (text) => text.includes(workspace.repoPath), 10_000);
}); });
@@ -33,12 +36,10 @@ test.describe("Workspace cwd correctness", () => {
await workspace.navigateTo(); await workspace.navigateTo();
await clickNewTerminal(page); await clickNewTerminal(page);
const terminal = page.locator('[data-testid="terminal-surface"]'); await expectTerminalSurfaceVisible(page);
await expect(terminal.first()).toBeVisible({ timeout: 20_000 }); await focusTerminalSurface(page);
await terminal.first().click();
await setupDeterministicPrompt(page, `PWD_READY_${Date.now()}`); await setupDeterministicPrompt(page, `PWD_READY_${Date.now()}`);
await terminal.first().pressSequentially("pwd\n", { delay: 0 }); await typeInTerminal(page, "pwd\n");
await waitForTerminalContent(page, (text) => text.includes(workspace.repoPath), 10_000); await waitForTerminalContent(page, (text) => text.includes(workspace.repoPath), 10_000);
}); });
}); });

View File

@@ -1,7 +1,7 @@
import { buildHostAgentDetailRoute, buildHostWorkspaceRoute } from "@/utils/host-routes"; import { buildHostAgentDetailRoute, buildHostWorkspaceRoute } from "@/utils/host-routes";
import type { WebSocketRoute } from "@playwright/test"; import type { WebSocketRoute } from "@playwright/test";
import { expect, test, type Page } from "./fixtures"; import { expect, test, type Page } from "./fixtures";
import { gotoAppShell } from "./helpers/app"; import { gotoAppShell, openSettings } from "./helpers/app";
import { import {
archiveAgentFromDaemon, archiveAgentFromDaemon,
connectArchiveTabDaemonClient, connectArchiveTabDaemonClient,
@@ -15,17 +15,26 @@ import {
connectNewWorkspaceDaemonClient, connectNewWorkspaceDaemonClient,
openProjectViaDaemon, openProjectViaDaemon,
} from "./helpers/new-workspace"; } from "./helpers/new-workspace";
import { expectComposerVisible } from "./helpers/composer";
import { createTempGitRepo } from "./helpers/workspace"; import { createTempGitRepo } from "./helpers/workspace";
import { import {
getVisibleWorkspaceAgentTabIds, getVisibleWorkspaceAgentTabIds,
expectOnlyWorkspaceAgentTabsVisible, expectOnlyWorkspaceAgentTabsVisible,
waitForWorkspaceTabsVisible, waitForWorkspaceTabsVisible,
expectWorkspaceTabsAbsent,
} from "./helpers/workspace-tabs"; } from "./helpers/workspace-tabs";
import { import {
expectSidebarWorkspaceSelected, expectSidebarWorkspaceSelected,
expectWorkspaceHeader, expectWorkspaceHeader,
expectWorkspaceHeaderAbsent,
expectMenuButtonVisible,
expectHostConnectingOrOffline,
expectReconnectingToastVisible,
expectReconnectingToastGone,
switchWorkspaceViaSidebar, switchWorkspaceViaSidebar,
waitForSidebarHydration, waitForSidebarHydration,
workspaceDeckEntryLocator,
expectWorkspaceDeckEntryCount,
} from "./helpers/workspace-ui"; } from "./helpers/workspace-ui";
const LOADING_WORKSPACE_TEXT_PATTERN = /Loading workspace/i; const LOADING_WORKSPACE_TEXT_PATTERN = /Loading workspace/i;
@@ -62,6 +71,10 @@ async function expectNoLoadingWorkspacePane(
} }
} }
async function expectNoLoadingPane(page: Page): Promise<void> {
await expect(page.getByText(LOADING_WORKSPACE_TEXT_PATTERN)).toHaveCount(0);
}
async function installDaemonWebSocketGate(page: Page, daemonPort: string) { async function installDaemonWebSocketGate(page: Page, daemonPort: string) {
let acceptingConnections = true; let acceptingConnections = true;
const activeSockets = new Set<WebSocketRoute>(); const activeSockets = new Set<WebSocketRoute>();
@@ -159,35 +172,31 @@ test.describe("Workspace navigation regression", () => {
title: workspace.workspaceName, title: workspace.workspaceName,
subtitle: workspace.projectDisplayName, subtitle: workspace.projectDisplayName,
}); });
await expect(page.getByTestId("workspace-tabs-row")).toBeVisible({ timeout: 30_000 }); await waitForWorkspaceTabsVisible(page);
await expectWorkspaceTabVisible(page, agent.id); await expectWorkspaceTabVisible(page, agent.id);
await daemonGate.drop(); await daemonGate.drop();
await expect(page.getByTestId("agent-reconnecting-toast")).toBeVisible({ await expectReconnectingToastVisible(page);
timeout: 30_000,
});
await expectWorkspaceHeader(page, { await expectWorkspaceHeader(page, {
title: workspace.workspaceName, title: workspace.workspaceName,
subtitle: workspace.projectDisplayName, subtitle: workspace.projectDisplayName,
}); });
await expect(page.getByTestId("workspace-tabs-row")).toBeVisible(); await waitForWorkspaceTabsVisible(page);
await expect(page.getByRole("textbox", { name: "Message agent..." })).toBeVisible(); await expectComposerVisible(page);
await expect(page.getByText(LOADING_WORKSPACE_TEXT_PATTERN)).toHaveCount(0); await expectNoLoadingPane(page);
const monitorReconnect = expectNoLoadingWorkspacePane(page, { const monitorReconnect = expectNoLoadingWorkspacePane(page, {
label: "host reconnect", label: "host reconnect",
}); });
daemonGate.restore(); daemonGate.restore();
await expect(page.getByTestId("agent-reconnecting-toast")).toHaveCount(0, { await expectReconnectingToastGone(page);
timeout: 30_000,
});
await monitorReconnect; await monitorReconnect;
await expectWorkspaceHeader(page, { await expectWorkspaceHeader(page, {
title: workspace.workspaceName, title: workspace.workspaceName,
subtitle: workspace.projectDisplayName, subtitle: workspace.projectDisplayName,
}); });
await expect(page.getByTestId("workspace-tabs-row")).toBeVisible(); await waitForWorkspaceTabsVisible(page);
await expect(page.getByRole("textbox", { name: "Message agent..." })).toBeVisible(); await expectComposerVisible(page);
} finally { } finally {
daemonGate.restore(); daemonGate.restore();
for (const agentId of agentIds) { for (const agentId of agentIds) {
@@ -222,16 +231,11 @@ test.describe("Workspace navigation regression", () => {
`/h/${encodeURIComponent(serverId)}/workspace/${encodeURIComponent("/tmp/paseo-missing-workspace")}`, `/h/${encodeURIComponent(serverId)}/workspace/${encodeURIComponent("/tmp/paseo-missing-workspace")}`,
); );
await expect( await expectHostConnectingOrOffline(page);
page.getByText(/^Connecting$|localhost is offline|Cannot reach localhost/i), await expectMenuButtonVisible(page);
).toBeVisible({ timeout: 30_000 }); await expectWorkspaceHeaderAbsent(page);
await expect(page.getByTestId("menu-button")).toBeVisible(); await expectWorkspaceTabsAbsent(page);
await expect(page.getByTestId("workspace-header-title")).toHaveCount(0); await openSettings(page);
await expect(page.getByTestId("workspace-tabs-row")).toHaveCount(0);
const settingsButton = page.locator('[data-testid="sidebar-settings"]:visible').first();
await expect(settingsButton).toBeVisible({ timeout: 30_000 });
await settingsButton.click();
await expect(page).toHaveURL(/\/settings\/general$/); await expect(page).toHaveURL(/\/settings\/general$/);
}); });
@@ -270,6 +274,13 @@ test.describe("Workspace navigation regression", () => {
await waitForSidebarHydration(page); await waitForSidebarHydration(page);
await openWorkspaceWithAgents(page, [firstAgent, secondAgent]); await openWorkspaceWithAgents(page, [firstAgent, secondAgent]);
const firstDeckEntry = workspaceDeckEntryLocator(page, serverId, firstWorkspace.workspaceId);
const secondDeckEntry = workspaceDeckEntryLocator(
page,
serverId,
secondWorkspace.workspaceId,
);
await switchWorkspaceViaSidebar({ await switchWorkspaceViaSidebar({
page, page,
serverId, serverId,
@@ -300,13 +311,6 @@ test.describe("Workspace navigation regression", () => {
await expect(getVisibleWorkspaceAgentTabIds(page)).resolves.toEqual([ await expect(getVisibleWorkspaceAgentTabIds(page)).resolves.toEqual([
`workspace-tab-agent_${firstAgent.id}`, `workspace-tab-agent_${firstAgent.id}`,
]); ]);
const firstDeckEntry = page.getByTestId(
`workspace-deck-entry-${serverId}:${firstWorkspace.workspaceId}`,
);
const secondDeckEntry = page.getByTestId(
`workspace-deck-entry-${serverId}:${secondWorkspace.workspaceId}`,
);
await expect(firstDeckEntry).toBeVisible({ timeout: 30_000 }); await expect(firstDeckEntry).toBeVisible({ timeout: 30_000 });
await switchWorkspaceViaSidebar({ await switchWorkspaceViaSidebar({
@@ -342,7 +346,7 @@ test.describe("Workspace navigation regression", () => {
await expect(firstDeckEntry).toBeAttached(); await expect(firstDeckEntry).toBeAttached();
await expect(firstDeckEntry).toBeHidden(); await expect(firstDeckEntry).toBeHidden();
await expect(secondDeckEntry).toBeVisible({ timeout: 30_000 }); await expect(secondDeckEntry).toBeVisible({ timeout: 30_000 });
await expect(page.locator('[data-testid^="workspace-deck-entry-"]')).toHaveCount(2); await expectWorkspaceDeckEntryCount(page, 2);
await page.evaluate( await page.evaluate(
({ agentId, serverId: targetServerId }) => { ({ agentId, serverId: targetServerId }) => {
@@ -371,7 +375,7 @@ test.describe("Workspace navigation regression", () => {
await expectOnlyWorkspaceAgentTabsVisible(page, [secondAgent.id]); await expectOnlyWorkspaceAgentTabsVisible(page, [secondAgent.id]);
await expect(firstDeckEntry).toBeAttached(); await expect(firstDeckEntry).toBeAttached();
await expect(firstDeckEntry).toBeHidden(); await expect(firstDeckEntry).toBeHidden();
await expect(page.locator('[data-testid^="workspace-deck-entry-"]')).toHaveCount(2); await expectWorkspaceDeckEntryCount(page, 2);
await switchWorkspaceViaSidebar({ await switchWorkspaceViaSidebar({
page, page,
@@ -385,7 +389,7 @@ test.describe("Workspace navigation regression", () => {
await expect(firstDeckEntry).toBeVisible({ timeout: 30_000 }); await expect(firstDeckEntry).toBeVisible({ timeout: 30_000 });
await expect(secondDeckEntry).toBeAttached(); await expect(secondDeckEntry).toBeAttached();
await expect(secondDeckEntry).toBeHidden(); await expect(secondDeckEntry).toBeHidden();
await expect(page.locator('[data-testid^="workspace-deck-entry-"]')).toHaveCount(2); await expectWorkspaceDeckEntryCount(page, 2);
await page.reload(); await page.reload();
await waitForSidebarHydration(page); await waitForSidebarHydration(page);

View File

@@ -1,32 +1,16 @@
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import { expect, test } from "./fixtures"; import { expect, test } from "./fixtures";
import { createTempGitRepo } from "./helpers/workspace"; import { createTempGitRepo } from "./helpers/workspace";
import { clickNewTerminal, waitForTabBar } from "./helpers/launcher"; import { clickNewTerminal } from "./helpers/launcher";
import { expectTerminalSurfaceVisible } from "./helpers/terminal-perf";
import { import {
connectWorkspaceSetupClient, connectWorkspaceSetupClient,
createWorkspaceThroughDaemon, createWorkspaceThroughDaemon,
findWorktreeWorkspaceForProject, findWorktreeWorkspaceForProject,
navigateToWorkspaceViaSidebar,
openHomeWithProject, openHomeWithProject,
} from "./helpers/workspace-setup"; } from "./helpers/workspace-setup";
function getServerId(): string {
const serverId = process.env.E2E_SERVER_ID;
if (!serverId) {
throw new Error("E2E_SERVER_ID is not set.");
}
return serverId;
}
async function navigateToWorkspaceViaSidebar(
page: import("@playwright/test").Page,
workspaceId: string,
): Promise<void> {
const row = page.getByTestId(`sidebar-workspace-row-${getServerId()}:${workspaceId}`);
await expect(row).toBeVisible({ timeout: 30_000 });
await row.click();
await waitForTabBar(page);
}
test.describe("Workspace setup runtime authority", () => { test.describe("Workspace setup runtime authority", () => {
test.describe.configure({ retries: 1 }); test.describe.configure({ retries: 1 });
@@ -48,7 +32,6 @@ test.describe("Workspace setup runtime authority", () => {
expect(wsInfo.workspaceDirectory).not.toBe(repo.path); expect(wsInfo.workspaceDirectory).not.toBe(repo.path);
expect(existsSync(wsInfo.workspaceDirectory)).toBe(true); expect(existsSync(wsInfo.workspaceDirectory)).toBe(true);
// Navigate to the workspace via sidebar
await openHomeWithProject(page, repo.path); await openHomeWithProject(page, repo.path);
await navigateToWorkspaceViaSidebar(page, workspaceId); await navigateToWorkspaceViaSidebar(page, workspaceId);
await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 }); await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 });
@@ -87,9 +70,7 @@ test.describe("Workspace setup runtime authority", () => {
await navigateToWorkspaceViaSidebar(page, workspaceId); await navigateToWorkspaceViaSidebar(page, workspaceId);
await clickNewTerminal(page); await clickNewTerminal(page);
await expectTerminalSurfaceVisible(page);
const terminal = page.locator('[data-testid="terminal-surface"]');
await expect(terminal.first()).toBeVisible({ timeout: 20_000 });
// Verify terminal is listed under the worktree directory, not the original repo // Verify terminal is listed under the worktree directory, not the original repo
await expect await expect

View File

@@ -1,23 +1,28 @@
import { test, expect } from "./fixtures"; import { test, expect } from "./fixtures";
import { createTempGitRepo } from "./helpers/workspace"; import { createTempGitRepo } from "./helpers/workspace";
import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs"; import {
waitForWorkspaceTabsVisible,
expectNoTerminalTabs,
clickFirstTerminalTab,
expectFirstTerminalTabContains,
} from "./helpers/workspace-tabs";
import { clickNewChat } from "./helpers/launcher";
import { expectComposerVisible } from "./helpers/composer";
import { openFileExplorer, expectExplorerEntryVisible } from "./helpers/file-explorer";
import { expectTerminalSurfaceVisible, waitForTerminalAttached } from "./helpers/terminal-perf";
import { import {
connectWorkspaceSetupClient, connectWorkspaceSetupClient,
createWorkspaceThroughDaemon, createWorkspaceThroughDaemon,
expectSetupPanel, expectSetupPanel,
openHomeWithProject, openHomeWithProject,
navigateToWorkspaceViaSidebar,
openWorkspaceScriptsMenu,
startWorkspaceScriptFromMenu,
closeWorkspaceScriptsMenu,
seedProjectForWorkspaceSetup, seedProjectForWorkspaceSetup,
waitForWorkspaceSetupProgress, waitForWorkspaceSetupProgress,
} from "./helpers/workspace-setup"; } from "./helpers/workspace-setup";
function getServerId(): string {
const serverId = process.env.E2E_SERVER_ID;
if (!serverId) {
throw new Error("E2E_SERVER_ID is not set.");
}
return serverId;
}
interface WorkspaceScriptStarter { interface WorkspaceScriptStarter {
startWorkspaceScript( startWorkspaceScript(
workspaceId: string, workspaceId: string,
@@ -30,18 +35,6 @@ interface WorkspaceScriptStarter {
}>; }>;
} }
/** Click the sidebar row for a workspace (by ID) and wait for navigation. */
async function navigateToWorkspaceViaSidebar(
page: import("@playwright/test").Page,
workspaceId: string,
): Promise<void> {
const testId = `sidebar-workspace-row-${getServerId()}:${workspaceId}`;
const row = page.getByTestId(testId);
await expect(row).toBeVisible({ timeout: 30_000 });
await row.click();
await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 });
}
test.describe("Workspace setup streaming", () => { test.describe("Workspace setup streaming", () => {
test("opens the setup tab when a workspace is created from the sidebar", async ({ page }) => { test("opens the setup tab when a workspace is created from the sidebar", async ({ page }) => {
const client = await connectWorkspaceSetupClient(); const client = await connectWorkspaceSetupClient();
@@ -100,31 +93,15 @@ test.describe("Workspace setup streaming", () => {
}); });
await completed; await completed;
// Navigate to workspace and verify it's usable
await openHomeWithProject(page, repo.path); await openHomeWithProject(page, repo.path);
await navigateToWorkspaceViaSidebar(page, workspace.id); await navigateToWorkspaceViaSidebar(page, workspace.id);
await waitForWorkspaceTabsVisible(page); await waitForWorkspaceTabsVisible(page);
await page.getByTestId("workspace-new-agent-tab").filter({ visible: true }).first().click(); await clickNewChat(page);
await expect(page.getByRole("textbox", { name: "Message agent..." }).first()).toBeVisible({ await expectComposerVisible(page, { timeout: 30_000 });
timeout: 30_000, await openFileExplorer(page);
}); await expectExplorerEntryVisible(page, "README.md");
await expectExplorerEntryVisible(page, "src");
const explorerToggle = page.getByTestId("workspace-explorer-toggle").first();
if ((await explorerToggle.getAttribute("aria-label")) === "Open explorer") {
await explorerToggle.click();
}
await expect(explorerToggle).toHaveAttribute("aria-label", "Close explorer", {
timeout: 30_000,
});
await page.getByTestId("explorer-tab-files").click();
await expect(page.getByTestId("file-explorer-tree-scroll")).toBeVisible({ timeout: 30_000 });
await expect(page.getByText("README.md", { exact: true }).first()).toBeVisible({
timeout: 30_000,
});
await expect(page.getByText("src", { exact: true }).first()).toBeVisible({
timeout: 30_000,
});
} finally { } finally {
await client.close(); await client.close();
await repo.cleanup(); await repo.cleanup();
@@ -273,33 +250,14 @@ test.describe("Workspace setup streaming", () => {
await navigateToWorkspaceViaSidebar(page, workspace.id); await navigateToWorkspaceViaSidebar(page, workspace.id);
await waitForWorkspaceTabsVisible(page); await waitForWorkspaceTabsVisible(page);
await expectNoTerminalTabs(page);
await expect(page.locator('[data-testid^="workspace-tab-terminal_"]')).toHaveCount(0); await openWorkspaceScriptsMenu(page);
await page.getByTestId("workspace-scripts-button").click(); await startWorkspaceScriptFromMenu(page, "web");
await expect(page.getByTestId("workspace-scripts-menu")).toBeVisible({ timeout: 10_000 }); await closeWorkspaceScriptsMenu(page);
await page.getByTestId("workspace-scripts-start-web").click(); await clickFirstTerminalTab(page);
await page.getByTestId("workspace-scripts-menu-backdrop").click(); await expectTerminalSurfaceVisible(page, { timeout: 10_000 });
await waitForTerminalAttached(page);
const terminalTab = page.locator('[data-testid^="workspace-tab-terminal_"]').first(); await expectFirstTerminalTabContains(page, "web");
await expect(terminalTab).toBeVisible({ timeout: 30_000 });
await terminalTab.click();
// Verify the terminal surface rendered
const terminalSurface = page.getByTestId("terminal-surface").first();
await expect(terminalSurface).toBeVisible({ timeout: 10_000 });
// Wait for terminal to fully attach (loading overlay gone)
await page
.locator('[data-testid="terminal-attach-loading"]')
.waitFor({ state: "hidden", timeout: 10_000 })
.catch(() => {
// overlay may never appear if attachment is instant
});
await terminalSurface.click();
await expect(terminalTab).toContainText("web");
} finally { } finally {
await client.close(); await client.close();
await repo.cleanup(); await repo.cleanup();