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:
Mohamed Boudra
2026-04-14 10:54:38 +07:00
parent e647c61fb5
commit 95ea02a87f
6 changed files with 63 additions and 19 deletions
+1 -1
View File
@@ -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-");
+10 -5
View File
@@ -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);
}
+3 -2
View File
@@ -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 ───────────────────────────────────────────────────
+12 -1
View File
@@ -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(
+21 -3
View File
@@ -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();