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 client: Awaited<ReturnType<typeof connectArchiveTabDaemonClient>>;
|
||||||
let tempRepo: { path: string; cleanup: () => Promise<void> };
|
let tempRepo: { path: string; cleanup: () => Promise<void> };
|
||||||
|
|
||||||
test.describe.configure({ timeout: 120_000 });
|
test.describe.configure({ timeout: 300_000 });
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
tempRepo = await createTempGitRepo("archive-tab-");
|
tempRepo = await createTempGitRepo("archive-tab-");
|
||||||
|
|||||||
@@ -188,11 +188,16 @@ export async function openWorkspaceWithAgents(
|
|||||||
const serverId = getServerId();
|
const serverId = getServerId();
|
||||||
for (const agent of agents) {
|
for (const agent of agents) {
|
||||||
await page.goto(buildHostAgentDetailRoute(serverId, agent.id, agent.cwd));
|
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.
|
// The workspace layout consumes `?open=agent:xxx`, returns null during the effect,
|
||||||
await page.waitForFunction(() => !window.location.search.includes("open="), {
|
// then replaces the URL with the clean workspace route after preparing the tab.
|
||||||
timeout: 15_000,
|
// 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 waitForWorkspaceTabsVisible(page);
|
||||||
await expectWorkspaceTabVisible(page, agent.id);
|
await expectWorkspaceTabVisible(page, agent.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,9 +79,10 @@ export async function clickNewTerminalButton(page: Page): Promise<void> {
|
|||||||
await button.click();
|
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> {
|
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 ───────────────────────────────────────────────────
|
// ─── Tab bar assertions ───────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -6,6 +6,17 @@ export async function openNewAgentComposer(page: Page): Promise<void> {
|
|||||||
await gotoHome(page);
|
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 {
|
export function workspaceLabelFromPath(value: string): string {
|
||||||
const normalized = value.replace(/\\/g, "/").replace(/\/+$/, "");
|
const normalized = value.replace(/\\/g, "/").replace(/\/+$/, "");
|
||||||
const parts = normalized.split("/").filter(Boolean);
|
const parts = normalized.split("/").filter(Boolean);
|
||||||
@@ -62,7 +73,7 @@ export async function waitForWorkspaceInSidebar(
|
|||||||
const selector = candidates
|
const selector = candidates
|
||||||
.map((id) => `[data-testid="sidebar-workspace-row-${input.serverId}:${id}"]`)
|
.map((id) => `[data-testid="sidebar-workspace-row-${input.serverId}:${id}"]`)
|
||||||
.join(",");
|
.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(
|
export async function expectWorkspaceHeader(
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { createTempGitRepo } from "./helpers/workspace";
|
|||||||
import {
|
import {
|
||||||
expectWorkspaceHeader,
|
expectWorkspaceHeader,
|
||||||
switchWorkspaceViaSidebar,
|
switchWorkspaceViaSidebar,
|
||||||
|
waitForSidebarHydration,
|
||||||
waitForWorkspaceInSidebar,
|
waitForWorkspaceInSidebar,
|
||||||
workspaceLabelFromPath,
|
workspaceLabelFromPath,
|
||||||
} from "./helpers/workspace-ui";
|
} from "./helpers/workspace-ui";
|
||||||
@@ -22,7 +23,7 @@ test.describe("New workspace flow", () => {
|
|||||||
const localWorkspaceIds = new Set<string>();
|
const localWorkspaceIds = new Set<string>();
|
||||||
const createdWorktreeIds = new Set<string>();
|
const createdWorktreeIds = new Set<string>();
|
||||||
|
|
||||||
test.describe.configure({ timeout: 120_000 });
|
test.describe.configure({ timeout: 240_000 });
|
||||||
|
|
||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
client = await connectNewWorkspaceDaemonClient();
|
client = await connectNewWorkspaceDaemonClient();
|
||||||
@@ -58,7 +59,16 @@ test.describe("New workspace flow", () => {
|
|||||||
localWorkspaceIds.add(secondWorkspace.workspaceId);
|
localWorkspaceIds.add(secondWorkspace.workspaceId);
|
||||||
|
|
||||||
await page.goto(buildHostWorkspaceRoute(serverId, firstWorkspace.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, {
|
await waitForWorkspaceInSidebar(page, {
|
||||||
serverId,
|
serverId,
|
||||||
workspaceId: firstWorkspace.workspaceId,
|
workspaceId: firstWorkspace.workspaceId,
|
||||||
@@ -112,7 +122,15 @@ test.describe("New workspace flow", () => {
|
|||||||
localWorkspaceIds.add(openedProject.workspaceId);
|
localWorkspaceIds.add(openedProject.workspaceId);
|
||||||
|
|
||||||
await page.goto(buildHostWorkspaceRoute(serverId, 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, {
|
await waitForWorkspaceInSidebar(page, {
|
||||||
serverId,
|
serverId,
|
||||||
workspaceId: openedProject.workspaceId,
|
workspaceId: openedProject.workspaceId,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 } from "./helpers/workspace-tabs";
|
||||||
|
import { waitForTerminalContent } from "./helpers/terminal-perf";
|
||||||
import {
|
import {
|
||||||
connectWorkspaceSetupClient,
|
connectWorkspaceSetupClient,
|
||||||
createWorkspaceThroughDaemon,
|
createWorkspaceThroughDaemon,
|
||||||
@@ -271,14 +272,22 @@ test.describe("Workspace setup streaming", () => {
|
|||||||
await terminalTab.click();
|
await terminalTab.click();
|
||||||
|
|
||||||
// Verify the terminal surface rendered
|
// 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).
|
// Wait for terminal to fully attach (loading overlay gone)
|
||||||
// The script is spawned after setup completes and needs time for the shell to start,
|
await page
|
||||||
// the command to be typed, and Node to boot the HTTP server. Give extra time on CI.
|
.locator('[data-testid="terminal-attach-loading"]')
|
||||||
await expect(page.locator(".xterm-rows").first()).toContainText("listening on", {
|
.waitFor({ state: "hidden", timeout: 10_000 })
|
||||||
timeout: 60_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 {
|
} finally {
|
||||||
await client.close();
|
await client.close();
|
||||||
await repo.cleanup();
|
await repo.cleanup();
|
||||||
|
|||||||
Reference in New Issue
Block a user