mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
@@ -4,6 +4,17 @@ function composerInput(page: Page) {
|
||||
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> {
|
||||
const input = composerInput(page);
|
||||
await expect(input).toBeEditable({ timeout: 30_000 });
|
||||
|
||||
@@ -174,6 +174,19 @@ export async function sampleTabsDuringTransition(
|
||||
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 ───────────────────────────────────────────────────────
|
||||
|
||||
/** Create a temp git repo and return its path with a cleanup function. */
|
||||
|
||||
@@ -165,3 +165,74 @@ export async function expectAboutContent(page: Page): Promise<void> {
|
||||
export async function expectGeneralContent(page: Page): Promise<void> {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
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 {
|
||||
const sorted = [...samples].sort((a, b) => a - b);
|
||||
const index = Math.ceil((p / 100) * sorted.length) - 1;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { expect, type Page } from "@playwright/test";
|
||||
import { parseHostWorkspaceRouteFromPathname } from "../../src/utils/host-routes";
|
||||
import { gotoAppShell } from "./app";
|
||||
import { createNodeWebSocketFactory, type NodeWebSocketFactory } from "./node-ws-factory";
|
||||
import { switchWorkspaceViaSidebar } from "./workspace-ui";
|
||||
import type { SessionOutboundMessage } from "@server/shared/messages";
|
||||
|
||||
interface WorkspaceSetupDaemonClient {
|
||||
@@ -315,6 +316,30 @@ export async function fetchWorkspaceById(
|
||||
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(
|
||||
client: WorkspaceSetupDaemonClient,
|
||||
predicate: (payload: WorkspaceSetupProgressPayload) => boolean,
|
||||
|
||||
@@ -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(
|
||||
page: Page,
|
||||
options: { durationMs?: number; intervalMs?: number } = {},
|
||||
|
||||
@@ -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> {
|
||||
const input = page.getByRole("textbox", { name: "Message agent..." });
|
||||
await expect(input).toBeEditable({ timeout: 30_000 });
|
||||
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
waitForTabWithTitle,
|
||||
measureTileTransition,
|
||||
sampleTabsDuringTransition,
|
||||
terminalSurfaceLocator,
|
||||
} from "./helpers/launcher";
|
||||
import { expectComposerVisible, composerLocator } from "./helpers/composer";
|
||||
import { expectTerminalSurfaceVisible } from "./helpers/terminal-perf";
|
||||
import {
|
||||
connectTerminalClient,
|
||||
setupDeterministicPrompt,
|
||||
@@ -49,9 +52,7 @@ test.describe("Tab creation", () => {
|
||||
|
||||
await pressNewTabShortcut(page);
|
||||
|
||||
// Should show the composer directly (no launcher panel)
|
||||
const composer = page.getByRole("textbox", { name: "Message agent..." });
|
||||
await expect(composer.first()).toBeVisible({ timeout: 15_000 });
|
||||
await expectComposerVisible(page);
|
||||
});
|
||||
|
||||
test("opening two new tabs creates two draft tabs", async ({ page }) => {
|
||||
@@ -78,9 +79,7 @@ test.describe("Tab creation", () => {
|
||||
|
||||
await clickNewChat(page);
|
||||
|
||||
// Draft composer should appear (the agent message input)
|
||||
const composer = page.getByRole("textbox", { name: "Message agent..." });
|
||||
await expect(composer.first()).toBeVisible({ timeout: 15_000 });
|
||||
await expectComposerVisible(page);
|
||||
|
||||
const tabsAfter = await getTabTestIds(page);
|
||||
const draftCountAfter = tabsAfter.filter((id) => id.includes("draft")).length;
|
||||
@@ -93,9 +92,7 @@ test.describe("Tab creation", () => {
|
||||
|
||||
await clickNewTerminal(page);
|
||||
|
||||
// Terminal surface should appear
|
||||
const terminal = page.locator('[data-testid="terminal-surface"]');
|
||||
await expect(terminal.first()).toBeVisible({ timeout: 20_000 });
|
||||
await expectTerminalSurfaceVisible(page);
|
||||
|
||||
const tabsAfter = await getTabTestIds(page);
|
||||
const terminalTabs = tabsAfter.filter((id) => id.includes("terminal"));
|
||||
@@ -142,17 +139,16 @@ test.describe("Terminal title propagation", () => {
|
||||
await gotoWorkspace(page, workspaceId);
|
||||
await clickNewTerminal(page);
|
||||
|
||||
const terminal = page.locator('[data-testid="terminal-surface"]');
|
||||
await expect(terminal.first()).toBeVisible({ timeout: 20_000 });
|
||||
await terminal.first().click();
|
||||
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 terminal
|
||||
.first()
|
||||
.pressSequentially(`printf '\\033]0;${testTitle}\\007'\n`, { delay: 0 });
|
||||
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);
|
||||
@@ -172,22 +168,22 @@ test.describe("Terminal title propagation", () => {
|
||||
await gotoWorkspace(page, workspaceId);
|
||||
await clickNewTerminal(page);
|
||||
|
||||
const terminal = page.locator('[data-testid="terminal-surface"]');
|
||||
await expect(terminal.first()).toBeVisible({ timeout: 20_000 });
|
||||
await terminal.first().click();
|
||||
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 terminal
|
||||
.first()
|
||||
.pressSequentially(`printf '\\033]0;Rapid-${i}\\007'\n`, { delay: 0 });
|
||||
await terminalSurfaceLocator(page).pressSequentially(`printf '\\033]0;Rapid-${i}\\007'\n`, {
|
||||
delay: 0,
|
||||
});
|
||||
}
|
||||
await terminal
|
||||
.first()
|
||||
.pressSequentially(`printf '\\033]0;${finalTitle}\\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);
|
||||
@@ -227,11 +223,10 @@ test.describe("Tab transitions (no flash)", () => {
|
||||
test.setTimeout(30_000);
|
||||
await gotoWorkspace(page, workspaceId);
|
||||
|
||||
const terminal = page.locator('[data-testid="terminal-surface"]');
|
||||
const elapsed = await measureTileTransition(
|
||||
page,
|
||||
() => clickNewTerminal(page),
|
||||
terminal.first(),
|
||||
terminalSurfaceLocator(page),
|
||||
20_000,
|
||||
);
|
||||
|
||||
@@ -244,9 +239,12 @@ test.describe("Tab transitions (no flash)", () => {
|
||||
test("New agent tab click shows composer without flash", async ({ page }) => {
|
||||
await gotoWorkspace(page, workspaceId);
|
||||
|
||||
const composer = page.getByRole("textbox", { name: "Message agent..." }).first();
|
||||
|
||||
const elapsed = await measureTileTransition(page, () => clickNewChat(page), composer, 10_000);
|
||||
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
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import { test, expect, type Page } from "./fixtures";
|
||||
import { test } from "./fixtures";
|
||||
import { gotoAppShell, openSettings } from "./helpers/app";
|
||||
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 {
|
||||
const serverId = process.env.E2E_SERVER_ID;
|
||||
@@ -19,12 +32,6 @@ function getSeededDaemonPort(): string {
|
||||
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("host page shows seeded label, connection endpoint, inject MCP toggle, and all action rows", async ({
|
||||
page,
|
||||
@@ -36,26 +43,11 @@ test.describe("Settings host page", () => {
|
||||
await openSettings(page);
|
||||
await openSettingsHost(page, serverId);
|
||||
|
||||
// Label renders in the detail header with a pencil edit affordance; the input is hidden until edit.
|
||||
await expectHostLabelHeader(page);
|
||||
|
||||
// Connections is its own section with a "Connections" heading and the seeded endpoint row.
|
||||
const connectionsCard = page.getByTestId("host-page-connections-card");
|
||||
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();
|
||||
await expectSettingsHeader(page, TEST_HOST_LABEL);
|
||||
await expectHostLabelDisplayed(page);
|
||||
await expectHostConnectionsCard(page, port);
|
||||
await expectHostInjectMcpCard(page);
|
||||
await expectHostActionCards(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 openSettingsHost(page, serverId);
|
||||
|
||||
await expect(page.getByTestId("host-page-label-input")).toHaveCount(0);
|
||||
|
||||
await page.getByTestId("host-page-label-edit-button").click();
|
||||
|
||||
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();
|
||||
await expectHostLabelDisplayed(page);
|
||||
await clickEditHostLabel(page);
|
||||
await expectHostLabelEditMode(page, TEST_HOST_LABEL);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
// TODO: add local-daemon fixture for positive Pair/Daemon coverage.
|
||||
// Pair-device now lives behind a row that only the local host sees
|
||||
// (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);
|
||||
await expectHostNoLocalOnlyRows(page);
|
||||
});
|
||||
|
||||
test("settings sidebar does not expose retired top-level sections", async ({ page }) => {
|
||||
await gotoAppShell(page);
|
||||
await openSettings(page);
|
||||
|
||||
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();
|
||||
await expectRetiredSidebarSectionsAbsent(page);
|
||||
});
|
||||
|
||||
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 page.goto(`/settings/hosts/${encodeURIComponent(serverId)}`);
|
||||
|
||||
await expect(page.getByTestId(`settings-host-page-${serverId}`)).toBeVisible();
|
||||
await expectHostLabelHeader(page);
|
||||
await expect(page.getByTestId("host-page-remove-host-card")).toBeVisible();
|
||||
await expectHostPageVisible(page, serverId);
|
||||
await expectSettingsHeader(page, TEST_HOST_LABEL);
|
||||
await expectHostLabelDisplayed(page);
|
||||
await expectHostActionCards(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 openSettings(page);
|
||||
|
||||
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();
|
||||
await expectLocalHostEntryFirst(page, serverId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { buildHostAgentDetailRoute, buildHostWorkspaceRoute } from "@/utils/host-routes";
|
||||
import { expect, test, type Page } from "./fixtures";
|
||||
import { expect, test } from "./fixtures";
|
||||
import {
|
||||
archiveAgentFromDaemon,
|
||||
connectArchiveTabDaemonClient,
|
||||
createIdleAgent,
|
||||
openWorkspaceWithAgents,
|
||||
} from "./helpers/archive-tab";
|
||||
import { getActiveTabTestId, waitForTabBar } from "./helpers/launcher";
|
||||
import { waitForTabBar, expectAgentTabActive } from "./helpers/launcher";
|
||||
import { createTempGitRepo } from "./helpers/workspace";
|
||||
|
||||
function getServerId(): string {
|
||||
@@ -17,12 +17,15 @@ function getServerId(): string {
|
||||
return serverId;
|
||||
}
|
||||
|
||||
async function pressSettingsToggleShortcut(page: Page) {
|
||||
async function pressSettingsToggleShortcut(page: import("@playwright/test").Page) {
|
||||
const modifier = process.platform === "darwin" ? "Meta" : "Control";
|
||||
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
|
||||
.poll(async () => {
|
||||
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: {
|
||||
page: Page;
|
||||
page: import("@playwright/test").Page;
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
agentId: string;
|
||||
}) {
|
||||
const expectedActiveTabId = `workspace-tab-agent_${input.agentId}`;
|
||||
await input.page.goto(
|
||||
buildHostAgentDetailRoute(input.serverId, input.agentId, input.workspaceId),
|
||||
);
|
||||
@@ -49,10 +51,7 @@ async function openAgentRouteAndExpectFocused(input: {
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await waitForTabBar(input.page);
|
||||
await expect(
|
||||
input.page.getByTestId(expectedActiveTabId).filter({ visible: true }),
|
||||
).toHaveAttribute("aria-selected", "true");
|
||||
await expect(getActiveTabTestId(input.page)).resolves.toBe(expectedActiveTabId);
|
||||
await expectAgentTabActive(input.page, input.agentId);
|
||||
}
|
||||
|
||||
test.describe("Settings toggle tab regression", () => {
|
||||
@@ -79,13 +78,7 @@ test.describe("Settings toggle tab regression", () => {
|
||||
|
||||
await openWorkspaceWithAgents(page, [firstAgent, secondAgent]);
|
||||
await waitForTabBar(page);
|
||||
|
||||
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 expectAgentTabActive(page, secondAgent.id);
|
||||
|
||||
await pressSettingsToggleShortcut(page);
|
||||
await expect(page).toHaveURL(/\/settings\/general$/);
|
||||
@@ -98,19 +91,11 @@ test.describe("Settings toggle tab regression", () => {
|
||||
await pressSettingsToggleShortcut(page);
|
||||
await expect(page).toHaveURL(buildHostWorkspaceRoute(serverId, repo.path));
|
||||
await waitForTabBar(page);
|
||||
await expect(page.getByTestId(expectedActiveTabId).filter({ visible: true })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
await expect(getActiveTabTestId(page)).resolves.toBe(expectedActiveTabId);
|
||||
await expectAgentTabActive(page, secondAgent.id);
|
||||
|
||||
await page.reload();
|
||||
await waitForTabBar(page);
|
||||
await expect(page.getByTestId(expectedActiveTabId).filter({ visible: true })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
await expect(getActiveTabTestId(page)).resolves.toBe(expectedActiveTabId);
|
||||
await expectAgentTabActive(page, secondAgent.id);
|
||||
} finally {
|
||||
for (const agentId of agentIds) {
|
||||
await archiveAgentFromDaemon(client, agentId).catch(() => undefined);
|
||||
@@ -152,14 +137,10 @@ test.describe("Settings toggle tab regression", () => {
|
||||
agentId: secondAgent.id,
|
||||
});
|
||||
|
||||
const expectedActiveTabId = `workspace-tab-agent_${secondAgent.id}`;
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
await page.reload();
|
||||
await waitForTabBar(page);
|
||||
await expect(
|
||||
page.getByTestId(expectedActiveTabId).filter({ visible: true }),
|
||||
).toHaveAttribute("aria-selected", "true");
|
||||
await expect(getActiveTabTestId(page)).resolves.toBe(expectedActiveTabId);
|
||||
await expectAgentTabActive(page, secondAgent.id);
|
||||
}
|
||||
} finally {
|
||||
for (const agentId of agentIds) {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { expect, test } from "./fixtures";
|
||||
import { test } from "./fixtures";
|
||||
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("main checkout workspace opens terminals in the project root", async ({
|
||||
@@ -13,13 +19,10 @@ test.describe("Workspace cwd correctness", () => {
|
||||
await workspace.navigateTo();
|
||||
await clickNewTerminal(page);
|
||||
|
||||
const terminal = page.locator('[data-testid="terminal-surface"]');
|
||||
await expect(terminal.first()).toBeVisible({ timeout: 20_000 });
|
||||
await terminal.first().click();
|
||||
|
||||
await expectTerminalSurfaceVisible(page);
|
||||
await focusTerminalSurface(page);
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -33,12 +36,10 @@ test.describe("Workspace cwd correctness", () => {
|
||||
await workspace.navigateTo();
|
||||
await clickNewTerminal(page);
|
||||
|
||||
const terminal = page.locator('[data-testid="terminal-surface"]');
|
||||
await expect(terminal.first()).toBeVisible({ timeout: 20_000 });
|
||||
await terminal.first().click();
|
||||
|
||||
await expectTerminalSurfaceVisible(page);
|
||||
await focusTerminalSurface(page);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { buildHostAgentDetailRoute, buildHostWorkspaceRoute } from "@/utils/host-routes";
|
||||
import type { WebSocketRoute } from "@playwright/test";
|
||||
import { expect, test, type Page } from "./fixtures";
|
||||
import { gotoAppShell } from "./helpers/app";
|
||||
import { gotoAppShell, openSettings } from "./helpers/app";
|
||||
import {
|
||||
archiveAgentFromDaemon,
|
||||
connectArchiveTabDaemonClient,
|
||||
@@ -15,17 +15,26 @@ import {
|
||||
connectNewWorkspaceDaemonClient,
|
||||
openProjectViaDaemon,
|
||||
} from "./helpers/new-workspace";
|
||||
import { expectComposerVisible } from "./helpers/composer";
|
||||
import { createTempGitRepo } from "./helpers/workspace";
|
||||
import {
|
||||
getVisibleWorkspaceAgentTabIds,
|
||||
expectOnlyWorkspaceAgentTabsVisible,
|
||||
waitForWorkspaceTabsVisible,
|
||||
expectWorkspaceTabsAbsent,
|
||||
} from "./helpers/workspace-tabs";
|
||||
import {
|
||||
expectSidebarWorkspaceSelected,
|
||||
expectWorkspaceHeader,
|
||||
expectWorkspaceHeaderAbsent,
|
||||
expectMenuButtonVisible,
|
||||
expectHostConnectingOrOffline,
|
||||
expectReconnectingToastVisible,
|
||||
expectReconnectingToastGone,
|
||||
switchWorkspaceViaSidebar,
|
||||
waitForSidebarHydration,
|
||||
workspaceDeckEntryLocator,
|
||||
expectWorkspaceDeckEntryCount,
|
||||
} from "./helpers/workspace-ui";
|
||||
|
||||
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) {
|
||||
let acceptingConnections = true;
|
||||
const activeSockets = new Set<WebSocketRoute>();
|
||||
@@ -159,35 +172,31 @@ test.describe("Workspace navigation regression", () => {
|
||||
title: workspace.workspaceName,
|
||||
subtitle: workspace.projectDisplayName,
|
||||
});
|
||||
await expect(page.getByTestId("workspace-tabs-row")).toBeVisible({ timeout: 30_000 });
|
||||
await waitForWorkspaceTabsVisible(page);
|
||||
await expectWorkspaceTabVisible(page, agent.id);
|
||||
|
||||
await daemonGate.drop();
|
||||
await expect(page.getByTestId("agent-reconnecting-toast")).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expectReconnectingToastVisible(page);
|
||||
await expectWorkspaceHeader(page, {
|
||||
title: workspace.workspaceName,
|
||||
subtitle: workspace.projectDisplayName,
|
||||
});
|
||||
await expect(page.getByTestId("workspace-tabs-row")).toBeVisible();
|
||||
await expect(page.getByRole("textbox", { name: "Message agent..." })).toBeVisible();
|
||||
await expect(page.getByText(LOADING_WORKSPACE_TEXT_PATTERN)).toHaveCount(0);
|
||||
await waitForWorkspaceTabsVisible(page);
|
||||
await expectComposerVisible(page);
|
||||
await expectNoLoadingPane(page);
|
||||
|
||||
const monitorReconnect = expectNoLoadingWorkspacePane(page, {
|
||||
label: "host reconnect",
|
||||
});
|
||||
daemonGate.restore();
|
||||
await expect(page.getByTestId("agent-reconnecting-toast")).toHaveCount(0, {
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expectReconnectingToastGone(page);
|
||||
await monitorReconnect;
|
||||
await expectWorkspaceHeader(page, {
|
||||
title: workspace.workspaceName,
|
||||
subtitle: workspace.projectDisplayName,
|
||||
});
|
||||
await expect(page.getByTestId("workspace-tabs-row")).toBeVisible();
|
||||
await expect(page.getByRole("textbox", { name: "Message agent..." })).toBeVisible();
|
||||
await waitForWorkspaceTabsVisible(page);
|
||||
await expectComposerVisible(page);
|
||||
} finally {
|
||||
daemonGate.restore();
|
||||
for (const agentId of agentIds) {
|
||||
@@ -222,16 +231,11 @@ test.describe("Workspace navigation regression", () => {
|
||||
`/h/${encodeURIComponent(serverId)}/workspace/${encodeURIComponent("/tmp/paseo-missing-workspace")}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByText(/^Connecting$|localhost is offline|Cannot reach localhost/i),
|
||||
).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByTestId("menu-button")).toBeVisible();
|
||||
await expect(page.getByTestId("workspace-header-title")).toHaveCount(0);
|
||||
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 expectHostConnectingOrOffline(page);
|
||||
await expectMenuButtonVisible(page);
|
||||
await expectWorkspaceHeaderAbsent(page);
|
||||
await expectWorkspaceTabsAbsent(page);
|
||||
await openSettings(page);
|
||||
await expect(page).toHaveURL(/\/settings\/general$/);
|
||||
});
|
||||
|
||||
@@ -270,6 +274,13 @@ test.describe("Workspace navigation regression", () => {
|
||||
await waitForSidebarHydration(page);
|
||||
await openWorkspaceWithAgents(page, [firstAgent, secondAgent]);
|
||||
|
||||
const firstDeckEntry = workspaceDeckEntryLocator(page, serverId, firstWorkspace.workspaceId);
|
||||
const secondDeckEntry = workspaceDeckEntryLocator(
|
||||
page,
|
||||
serverId,
|
||||
secondWorkspace.workspaceId,
|
||||
);
|
||||
|
||||
await switchWorkspaceViaSidebar({
|
||||
page,
|
||||
serverId,
|
||||
@@ -300,13 +311,6 @@ test.describe("Workspace navigation regression", () => {
|
||||
await expect(getVisibleWorkspaceAgentTabIds(page)).resolves.toEqual([
|
||||
`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 switchWorkspaceViaSidebar({
|
||||
@@ -342,7 +346,7 @@ test.describe("Workspace navigation regression", () => {
|
||||
await expect(firstDeckEntry).toBeAttached();
|
||||
await expect(firstDeckEntry).toBeHidden();
|
||||
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(
|
||||
({ agentId, serverId: targetServerId }) => {
|
||||
@@ -371,7 +375,7 @@ test.describe("Workspace navigation regression", () => {
|
||||
await expectOnlyWorkspaceAgentTabsVisible(page, [secondAgent.id]);
|
||||
await expect(firstDeckEntry).toBeAttached();
|
||||
await expect(firstDeckEntry).toBeHidden();
|
||||
await expect(page.locator('[data-testid^="workspace-deck-entry-"]')).toHaveCount(2);
|
||||
await expectWorkspaceDeckEntryCount(page, 2);
|
||||
|
||||
await switchWorkspaceViaSidebar({
|
||||
page,
|
||||
@@ -385,7 +389,7 @@ test.describe("Workspace navigation regression", () => {
|
||||
await expect(firstDeckEntry).toBeVisible({ timeout: 30_000 });
|
||||
await expect(secondDeckEntry).toBeAttached();
|
||||
await expect(secondDeckEntry).toBeHidden();
|
||||
await expect(page.locator('[data-testid^="workspace-deck-entry-"]')).toHaveCount(2);
|
||||
await expectWorkspaceDeckEntryCount(page, 2);
|
||||
|
||||
await page.reload();
|
||||
await waitForSidebarHydration(page);
|
||||
|
||||
@@ -1,32 +1,16 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { expect, test } from "./fixtures";
|
||||
import { createTempGitRepo } from "./helpers/workspace";
|
||||
import { clickNewTerminal, waitForTabBar } from "./helpers/launcher";
|
||||
import { clickNewTerminal } from "./helpers/launcher";
|
||||
import { expectTerminalSurfaceVisible } from "./helpers/terminal-perf";
|
||||
import {
|
||||
connectWorkspaceSetupClient,
|
||||
createWorkspaceThroughDaemon,
|
||||
findWorktreeWorkspaceForProject,
|
||||
navigateToWorkspaceViaSidebar,
|
||||
openHomeWithProject,
|
||||
} 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.configure({ retries: 1 });
|
||||
|
||||
@@ -48,7 +32,6 @@ test.describe("Workspace setup runtime authority", () => {
|
||||
expect(wsInfo.workspaceDirectory).not.toBe(repo.path);
|
||||
expect(existsSync(wsInfo.workspaceDirectory)).toBe(true);
|
||||
|
||||
// Navigate to the workspace via sidebar
|
||||
await openHomeWithProject(page, repo.path);
|
||||
await navigateToWorkspaceViaSidebar(page, workspaceId);
|
||||
await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 });
|
||||
@@ -87,9 +70,7 @@ test.describe("Workspace setup runtime authority", () => {
|
||||
await navigateToWorkspaceViaSidebar(page, workspaceId);
|
||||
|
||||
await clickNewTerminal(page);
|
||||
|
||||
const terminal = page.locator('[data-testid="terminal-surface"]');
|
||||
await expect(terminal.first()).toBeVisible({ timeout: 20_000 });
|
||||
await expectTerminalSurfaceVisible(page);
|
||||
|
||||
// Verify terminal is listed under the worktree directory, not the original repo
|
||||
await expect
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import { test, expect } from "./fixtures";
|
||||
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 {
|
||||
connectWorkspaceSetupClient,
|
||||
createWorkspaceThroughDaemon,
|
||||
expectSetupPanel,
|
||||
openHomeWithProject,
|
||||
navigateToWorkspaceViaSidebar,
|
||||
openWorkspaceScriptsMenu,
|
||||
startWorkspaceScriptFromMenu,
|
||||
closeWorkspaceScriptsMenu,
|
||||
seedProjectForWorkspaceSetup,
|
||||
waitForWorkspaceSetupProgress,
|
||||
} 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 {
|
||||
startWorkspaceScript(
|
||||
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("opens the setup tab when a workspace is created from the sidebar", async ({ page }) => {
|
||||
const client = await connectWorkspaceSetupClient();
|
||||
@@ -100,31 +93,15 @@ test.describe("Workspace setup streaming", () => {
|
||||
});
|
||||
await completed;
|
||||
|
||||
// Navigate to workspace and verify it's usable
|
||||
await openHomeWithProject(page, repo.path);
|
||||
await navigateToWorkspaceViaSidebar(page, workspace.id);
|
||||
|
||||
await waitForWorkspaceTabsVisible(page);
|
||||
await page.getByTestId("workspace-new-agent-tab").filter({ visible: true }).first().click();
|
||||
await expect(page.getByRole("textbox", { name: "Message agent..." }).first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
await clickNewChat(page);
|
||||
await expectComposerVisible(page, { timeout: 30_000 });
|
||||
await openFileExplorer(page);
|
||||
await expectExplorerEntryVisible(page, "README.md");
|
||||
await expectExplorerEntryVisible(page, "src");
|
||||
} finally {
|
||||
await client.close();
|
||||
await repo.cleanup();
|
||||
@@ -273,33 +250,14 @@ test.describe("Workspace setup streaming", () => {
|
||||
await navigateToWorkspaceViaSidebar(page, workspace.id);
|
||||
|
||||
await waitForWorkspaceTabsVisible(page);
|
||||
|
||||
await expect(page.locator('[data-testid^="workspace-tab-terminal_"]')).toHaveCount(0);
|
||||
await page.getByTestId("workspace-scripts-button").click();
|
||||
await expect(page.getByTestId("workspace-scripts-menu")).toBeVisible({ timeout: 10_000 });
|
||||
await page.getByTestId("workspace-scripts-start-web").click();
|
||||
await page.getByTestId("workspace-scripts-menu-backdrop").click();
|
||||
|
||||
const terminalTab = page.locator('[data-testid^="workspace-tab-terminal_"]').first();
|
||||
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");
|
||||
await expectNoTerminalTabs(page);
|
||||
await openWorkspaceScriptsMenu(page);
|
||||
await startWorkspaceScriptFromMenu(page, "web");
|
||||
await closeWorkspaceScriptsMenu(page);
|
||||
await clickFirstTerminalTab(page);
|
||||
await expectTerminalSurfaceVisible(page, { timeout: 10_000 });
|
||||
await waitForTerminalAttached(page);
|
||||
await expectFirstTerminalTabContains(page, "web");
|
||||
} finally {
|
||||
await client.close();
|
||||
await repo.cleanup();
|
||||
|
||||
Reference in New Issue
Block a user