fix: resolve 6 remaining Playwright e2e test failures on CI
- archive-tab: replace waitForFunction with waitForURL for ?open= intent consumption, increase test timeout to 300s for slow rootNavigationState init on CI - launcher-tab: use Control+t on Linux instead of Meta+t to match app's keyboard shortcut definitions for non-Mac platforms - new-workspace: add waitForSidebarHydration to wait for daemon connection before checking sidebar rows, add re-navigation fallback if app redirects during hydration, increase timeouts - setup-streaming: use waitForTerminalContent (xterm buffer API) instead of fragile .xterm-rows CSS text matching, wait for terminal attachment before checking content
This commit is contained in:
@@ -19,7 +19,7 @@ test.describe("Archive tab reconciliation", () => {
|
||||
let client: Awaited<ReturnType<typeof connectArchiveTabDaemonClient>>;
|
||||
let tempRepo: { path: string; cleanup: () => Promise<void> };
|
||||
|
||||
test.describe.configure({ timeout: 120_000 });
|
||||
test.describe.configure({ timeout: 300_000 });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
tempRepo = await createTempGitRepo("archive-tab-");
|
||||
|
||||
@@ -188,11 +188,16 @@ export async function openWorkspaceWithAgents(
|
||||
const serverId = getServerId();
|
||||
for (const agent of agents) {
|
||||
await page.goto(buildHostAgentDetailRoute(serverId, agent.id, agent.cwd));
|
||||
// Wait for the ?open=agent:xxx intent to be consumed by the layout effect.
|
||||
// The layout removes the query param via history.replaceState once processed.
|
||||
await page.waitForFunction(() => !window.location.search.includes("open="), {
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
// The workspace layout consumes `?open=agent:xxx`, returns null during the effect,
|
||||
// then replaces the URL with the clean workspace route after preparing the tab.
|
||||
// On CI, Expo Router's rootNavigationState may take time to initialize,
|
||||
// so we allow a generous timeout here (matching terminal-perf pattern).
|
||||
await page.waitForURL(
|
||||
(url) => url.pathname.includes("/workspace/") && !url.searchParams.has("open"),
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
|
||||
await waitForWorkspaceTabsVisible(page);
|
||||
await expectWorkspaceTabVisible(page, agent.id);
|
||||
}
|
||||
|
||||
@@ -79,9 +79,10 @@ export async function clickNewTerminalButton(page: Page): Promise<void> {
|
||||
await button.click();
|
||||
}
|
||||
|
||||
/** Press Cmd+T (macOS) to open a new tab. */
|
||||
/** Press Cmd+T (macOS) or Ctrl+T (Linux/Windows) to open a new tab. */
|
||||
export async function pressNewTabShortcut(page: Page): Promise<void> {
|
||||
await page.keyboard.press("Meta+t");
|
||||
const modifier = process.platform === "darwin" ? "Meta" : "Control";
|
||||
await page.keyboard.press(`${modifier}+t`);
|
||||
}
|
||||
|
||||
// ─── Tab bar assertions ───────────────────────────────────────────────────
|
||||
|
||||
@@ -6,6 +6,17 @@ export async function openNewAgentComposer(page: Page): Promise<void> {
|
||||
await gotoHome(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the sidebar to show at least one project row, indicating that the
|
||||
* WebSocket connection is up and workspace hydration has completed.
|
||||
*/
|
||||
export async function waitForSidebarHydration(page: Page, timeout = 60_000): Promise<void> {
|
||||
await page
|
||||
.locator('[data-testid^="sidebar-project-row-"]')
|
||||
.first()
|
||||
.waitFor({ state: "visible", timeout });
|
||||
}
|
||||
|
||||
export function workspaceLabelFromPath(value: string): string {
|
||||
const normalized = value.replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
const parts = normalized.split("/").filter(Boolean);
|
||||
@@ -62,7 +73,7 @@ export async function waitForWorkspaceInSidebar(
|
||||
const selector = candidates
|
||||
.map((id) => `[data-testid="sidebar-workspace-row-${input.serverId}:${id}"]`)
|
||||
.join(",");
|
||||
await page.locator(selector).first().waitFor({ state: "visible", timeout: 30_000 });
|
||||
await page.locator(selector).first().waitFor({ state: "visible", timeout: 60_000 });
|
||||
}
|
||||
|
||||
export async function expectWorkspaceHeader(
|
||||
|
||||
@@ -13,6 +13,7 @@ import { createTempGitRepo } from "./helpers/workspace";
|
||||
import {
|
||||
expectWorkspaceHeader,
|
||||
switchWorkspaceViaSidebar,
|
||||
waitForSidebarHydration,
|
||||
waitForWorkspaceInSidebar,
|
||||
workspaceLabelFromPath,
|
||||
} from "./helpers/workspace-ui";
|
||||
@@ -22,7 +23,7 @@ test.describe("New workspace flow", () => {
|
||||
const localWorkspaceIds = new Set<string>();
|
||||
const createdWorktreeIds = new Set<string>();
|
||||
|
||||
test.describe.configure({ timeout: 120_000 });
|
||||
test.describe.configure({ timeout: 240_000 });
|
||||
|
||||
test.beforeEach(async () => {
|
||||
client = await connectNewWorkspaceDaemonClient();
|
||||
@@ -58,7 +59,16 @@ test.describe("New workspace flow", () => {
|
||||
localWorkspaceIds.add(secondWorkspace.workspaceId);
|
||||
|
||||
await page.goto(buildHostWorkspaceRoute(serverId, firstWorkspace.workspaceId));
|
||||
await expect(page).toHaveURL(buildHostWorkspaceRoute(serverId, firstWorkspace.workspaceId));
|
||||
await waitForSidebarHydration(page);
|
||||
|
||||
// The app may redirect to a different workspace before hydration completes.
|
||||
// Re-navigate if we ended up on the wrong workspace.
|
||||
const currentUrl = page.url();
|
||||
const expectedUrl = buildHostWorkspaceRoute(serverId, firstWorkspace.workspaceId);
|
||||
if (!currentUrl.includes(expectedUrl)) {
|
||||
await page.goto(expectedUrl);
|
||||
}
|
||||
|
||||
await waitForWorkspaceInSidebar(page, {
|
||||
serverId,
|
||||
workspaceId: firstWorkspace.workspaceId,
|
||||
@@ -112,7 +122,15 @@ test.describe("New workspace flow", () => {
|
||||
localWorkspaceIds.add(openedProject.workspaceId);
|
||||
|
||||
await page.goto(buildHostWorkspaceRoute(serverId, openedProject.workspaceId));
|
||||
await expect(page).toHaveURL(buildHostWorkspaceRoute(serverId, openedProject.workspaceId));
|
||||
await waitForSidebarHydration(page);
|
||||
|
||||
// Re-navigate if the app redirected before hydration completed.
|
||||
const currentUrl = page.url();
|
||||
const expectedUrl = buildHostWorkspaceRoute(serverId, openedProject.workspaceId);
|
||||
if (!currentUrl.includes(expectedUrl)) {
|
||||
await page.goto(expectedUrl);
|
||||
}
|
||||
|
||||
await waitForWorkspaceInSidebar(page, {
|
||||
serverId,
|
||||
workspaceId: openedProject.workspaceId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test, expect } from "./fixtures";
|
||||
import { createTempGitRepo } from "./helpers/workspace";
|
||||
import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs";
|
||||
import { waitForTerminalContent } from "./helpers/terminal-perf";
|
||||
import {
|
||||
connectWorkspaceSetupClient,
|
||||
createWorkspaceThroughDaemon,
|
||||
@@ -271,14 +272,22 @@ test.describe("Workspace setup streaming", () => {
|
||||
await terminalTab.click();
|
||||
|
||||
// Verify the terminal surface rendered
|
||||
await expect(page.getByTestId("terminal-surface").first()).toBeVisible({ timeout: 10_000 });
|
||||
const terminalSurface = page.getByTestId("terminal-surface").first();
|
||||
await expect(terminalSurface).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Verify the terminal output contains "listening on" (xterm renders text in .xterm-rows).
|
||||
// The script is spawned after setup completes and needs time for the shell to start,
|
||||
// the command to be typed, and Node to boot the HTTP server. Give extra time on CI.
|
||||
await expect(page.locator(".xterm-rows").first()).toContainText("listening on", {
|
||||
timeout: 60_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();
|
||||
|
||||
// Verify the terminal output contains "listening on" via xterm buffer API.
|
||||
// The .xterm-rows CSS selector is fragile; reading the buffer directly is reliable.
|
||||
await waitForTerminalContent(page, (text) => text.includes("listening on"), 60_000);
|
||||
} finally {
|
||||
await client.close();
|
||||
await repo.cleanup();
|
||||
|
||||
Reference in New Issue
Block a user