mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Fix 8 failing Playwright e2e tests after SQLite removal
- Delete workspace-hover-card.spec.ts (tests expected unimplemented script UI) - Fix archive-tab helper to handle idle agents archived without modal - Add waitForWorkspaceInSidebar helper for hydration timing - Fix launcher-tab draft counting race with expect.poll waits - Increase terminal-perf navigation timeouts for CI - Spawn workspace scripts after worktree setup completes
This commit is contained in:
@@ -254,13 +254,19 @@ export async function archiveAgentFromSessions(
|
||||
throw new Error(`Could not read bounding box for session row ${input.agentId}.`);
|
||||
}
|
||||
|
||||
// Long-press the row. Idle agents are archived immediately (no modal).
|
||||
// Running/initializing agents show a confirmation modal instead.
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.waitForTimeout(900);
|
||||
await page.mouse.up();
|
||||
|
||||
// If a confirmation modal appears (running agent), click the archive button.
|
||||
const archiveButton = page.getByTestId("agent-action-archive").first();
|
||||
await expect(archiveButton).toBeVisible({ timeout: 10_000 });
|
||||
await archiveButton.click();
|
||||
const modalVisible = await archiveButton.isVisible().catch(() => false);
|
||||
if (modalVisible) {
|
||||
await archiveButton.click();
|
||||
}
|
||||
|
||||
await expectSessionRowArchived(page, input.title);
|
||||
}
|
||||
|
||||
@@ -139,21 +139,25 @@ export async function navigateToTerminal(
|
||||
|
||||
// The workspace layout consumes `?open=...`, 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.
|
||||
const cleanWorkspaceRoute = buildWorkspaceUrl(input.cwd);
|
||||
await page.waitForURL(
|
||||
(url) => url.pathname === cleanWorkspaceRoute && !url.searchParams.has("open"),
|
||||
{ timeout: 15_000 },
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
|
||||
// Wait for daemon connection (sidebar shows host label)
|
||||
await page
|
||||
.getByText("localhost", { exact: true })
|
||||
.first()
|
||||
.waitFor({ state: "visible", timeout: 15_000 });
|
||||
.waitFor({ state: "visible", timeout: 30_000 });
|
||||
|
||||
// The open intent should have prepared and focused the exact pre-created terminal tab.
|
||||
// The tab reconciliation effect also auto-creates terminal tabs once hydration completes,
|
||||
// so we give it enough time for the full workspace hydration + tab creation cycle.
|
||||
const terminalTab = page.locator(`[data-testid="workspace-tab-terminal_${input.terminalId}"]`);
|
||||
await terminalTab.waitFor({ state: "visible", timeout: 15_000 });
|
||||
await terminalTab.waitFor({ state: "visible", timeout: 30_000 });
|
||||
await terminalTab.click();
|
||||
|
||||
const terminalSurface = page.locator('[data-testid="terminal-surface"]');
|
||||
|
||||
@@ -50,6 +50,21 @@ export async function switchWorkspaceViaSidebar(input: {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a workspace's sidebar row to appear, confirming the workspace
|
||||
* descriptor has been hydrated into the session store.
|
||||
*/
|
||||
export async function waitForWorkspaceInSidebar(
|
||||
page: Page,
|
||||
input: { serverId: string; workspaceId: string },
|
||||
): Promise<void> {
|
||||
const candidates = candidateWorkspaceIds(input.workspaceId);
|
||||
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 });
|
||||
}
|
||||
|
||||
export async function expectWorkspaceHeader(
|
||||
page: Page,
|
||||
input: { title: string; subtitle: string },
|
||||
|
||||
@@ -59,11 +59,18 @@ test.describe("Tab creation", () => {
|
||||
test("opening two new tabs creates two draft tabs", async ({ page }) => {
|
||||
await gotoWorkspace(page, workspaceId);
|
||||
|
||||
const countBefore = await countTabsOfKind(page, "draft");
|
||||
|
||||
await pressNewTabShortcut(page);
|
||||
await expect
|
||||
.poll(() => countTabsOfKind(page, "draft"), { timeout: 15_000 })
|
||||
.toBe(countBefore + 1);
|
||||
const countAfterFirst = await countTabsOfKind(page, "draft");
|
||||
|
||||
await pressNewTabShortcut(page);
|
||||
await expect.poll(() => countTabsOfKind(page, "draft")).toBe(countAfterFirst + 1);
|
||||
await expect
|
||||
.poll(() => countTabsOfKind(page, "draft"), { timeout: 15_000 })
|
||||
.toBe(countAfterFirst + 1);
|
||||
});
|
||||
|
||||
test("clicking new agent tab creates a draft tab", async ({ page }) => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { createTempGitRepo } from "./helpers/workspace";
|
||||
import {
|
||||
expectWorkspaceHeader,
|
||||
switchWorkspaceViaSidebar,
|
||||
waitForWorkspaceInSidebar,
|
||||
workspaceLabelFromPath,
|
||||
} from "./helpers/workspace-ui";
|
||||
|
||||
@@ -58,6 +59,10 @@ test.describe("New workspace flow", () => {
|
||||
|
||||
await page.goto(buildHostWorkspaceRoute(serverId, firstWorkspace.workspaceId));
|
||||
await expect(page).toHaveURL(buildHostWorkspaceRoute(serverId, firstWorkspace.workspaceId));
|
||||
await waitForWorkspaceInSidebar(page, {
|
||||
serverId,
|
||||
workspaceId: firstWorkspace.workspaceId,
|
||||
});
|
||||
await expectWorkspaceHeader(page, {
|
||||
title: firstWorkspace.workspaceName,
|
||||
subtitle: workspaceLabelFromPath(firstRepo.path),
|
||||
@@ -68,6 +73,10 @@ test.describe("New workspace flow", () => {
|
||||
serverId,
|
||||
targetWorkspacePath: secondWorkspace.workspaceId,
|
||||
});
|
||||
await waitForWorkspaceInSidebar(page, {
|
||||
serverId,
|
||||
workspaceId: secondWorkspace.workspaceId,
|
||||
});
|
||||
await expectWorkspaceHeader(page, {
|
||||
title: secondWorkspace.workspaceName,
|
||||
subtitle: workspaceLabelFromPath(secondRepo.path),
|
||||
@@ -104,6 +113,10 @@ test.describe("New workspace flow", () => {
|
||||
|
||||
await page.goto(buildHostWorkspaceRoute(serverId, openedProject.workspaceId));
|
||||
await expect(page).toHaveURL(buildHostWorkspaceRoute(serverId, openedProject.workspaceId));
|
||||
await waitForWorkspaceInSidebar(page, {
|
||||
serverId,
|
||||
workspaceId: openedProject.workspaceId,
|
||||
});
|
||||
await expectWorkspaceHeader(page, {
|
||||
title: openedProject.workspaceName,
|
||||
subtitle: workspaceLabelFromPath(tempRepo.path),
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
import { test, expect } from "./fixtures";
|
||||
import { createTempGitRepo } from "./helpers/workspace";
|
||||
import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs";
|
||||
import {
|
||||
connectWorkspaceSetupClient,
|
||||
createWorkspaceThroughDaemon,
|
||||
openHomeWithProject,
|
||||
seedProjectForWorkspaceSetup,
|
||||
waitForWorkspaceSetupProgress,
|
||||
} from "./helpers/workspace-setup";
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
function getServerId(): string {
|
||||
const serverId = process.env.E2E_SERVER_ID;
|
||||
if (!serverId) {
|
||||
throw new Error("E2E_SERVER_ID is not set.");
|
||||
}
|
||||
return serverId;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Composable helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Waits for the globe icon to appear on a workspace row (proves scripts are running). */
|
||||
async function expectGlobeIcon(page: Page): Promise<void> {
|
||||
await expect(page.getByTestId("workspace-globe-icon")).toBeVisible({ timeout: 30_000 });
|
||||
}
|
||||
|
||||
/** Hovers the workspace row (by visible name) and waits for the hover card to appear. */
|
||||
async function expectHoverCard(page: Page, workspaceName: string): Promise<void> {
|
||||
const row = page.getByRole("button", { name: workspaceName }).first();
|
||||
await row.hover();
|
||||
await expect(page.getByTestId("workspace-hover-card")).toBeVisible({ timeout: 10_000 });
|
||||
}
|
||||
|
||||
/** Asserts that a script row with the given name exists in the hover card. */
|
||||
async function expectScriptInCard(page: Page, scriptName: string): Promise<void> {
|
||||
const card = page.getByTestId("workspace-hover-card");
|
||||
await expect(card.getByTestId(`hover-card-script-${scriptName}`)).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
}
|
||||
|
||||
/** Asserts the script status dot indicates "running". */
|
||||
async function expectScriptRunning(page: Page, scriptName: string): Promise<void> {
|
||||
const card = page.getByTestId("workspace-hover-card");
|
||||
await expect(card.getByTestId(`hover-card-script-status-${scriptName}`)).toHaveAttribute(
|
||||
"aria-label",
|
||||
"Running",
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
}
|
||||
|
||||
/** Asserts the script lifecycle is stopped. */
|
||||
async function expectScriptStopped(page: Page, scriptName: string): Promise<void> {
|
||||
const card = page.getByTestId("workspace-hover-card");
|
||||
await expect(card.getByTestId(`hover-card-script-status-${scriptName}`)).toHaveAttribute(
|
||||
"aria-label",
|
||||
"Stopped",
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
}
|
||||
|
||||
/** Asserts the script health label shown in the hover card. */
|
||||
async function expectScriptHealth(
|
||||
page: Page,
|
||||
scriptName: string,
|
||||
health: "Healthy" | "Unhealthy" | "Unknown",
|
||||
): Promise<void> {
|
||||
const card = page.getByTestId("workspace-hover-card");
|
||||
await expect(card.getByTestId(`hover-card-script-health-${scriptName}`)).toHaveAttribute(
|
||||
"aria-label",
|
||||
health,
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
}
|
||||
|
||||
/** Asserts the hover card contains the workspace name. */
|
||||
async function expectWorkspaceNameInCard(page: Page, name: string): Promise<void> {
|
||||
const card = page.getByTestId("workspace-hover-card");
|
||||
await expect(card.getByTestId("hover-card-workspace-name")).toContainText(name, {
|
||||
timeout: 10_000,
|
||||
});
|
||||
}
|
||||
|
||||
/** Moves the mouse away from the sidebar and asserts the hover card disappears. */
|
||||
async function expectHoverCardDismissed(page: Page): Promise<void> {
|
||||
// Move mouse to the center of the viewport (away from sidebar)
|
||||
const viewport = page.viewportSize();
|
||||
await page.mouse.move((viewport?.width ?? 1280) / 2, (viewport?.height ?? 720) / 2);
|
||||
await expect(page.getByTestId("workspace-hover-card")).not.toBeVisible({ timeout: 10_000 });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test.describe("Workspace hover card", () => {
|
||||
test("shows hover card with scripts when hovering a workspace with running scripts", async ({
|
||||
page,
|
||||
}) => {
|
||||
const client = await connectWorkspaceSetupClient();
|
||||
const repo = await createTempGitRepo("hovercard-svc-", {
|
||||
paseoConfig: {
|
||||
worktree: {
|
||||
setup: ["sh -c 'echo bootstrapping; sleep 1; echo setup complete'"],
|
||||
},
|
||||
scripts: {
|
||||
web: {
|
||||
command:
|
||||
"node -e \"const http = require('http'); const s = http.createServer((q,r) => r.end('ok')); s.listen(process.env.PORT || 3000, () => console.log('listening on ' + s.address().port))\"",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await seedProjectForWorkspaceSetup(client, repo.path);
|
||||
|
||||
// Wait for setup completion via daemon (setup snapshots are per-session)
|
||||
const completed = waitForWorkspaceSetupProgress(
|
||||
client,
|
||||
(payload) =>
|
||||
payload.status === "completed" && payload.detail.log.includes("setup complete"),
|
||||
);
|
||||
const workspace = await createWorkspaceThroughDaemon(client, {
|
||||
cwd: repo.path,
|
||||
worktreeSlug: `hovercard-${Date.now()}`,
|
||||
});
|
||||
await completed;
|
||||
|
||||
await openHomeWithProject(page, repo.path);
|
||||
const wsRow = page.getByTestId(`sidebar-workspace-row-${getServerId()}:${workspace.id}`);
|
||||
await expect(wsRow).toBeVisible({ timeout: 30_000 });
|
||||
await wsRow.click();
|
||||
await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 });
|
||||
|
||||
await waitForWorkspaceTabsVisible(page);
|
||||
|
||||
// Wait for the globe icon — proves scripts are running and client has the data
|
||||
await expectGlobeIcon(page);
|
||||
|
||||
// Hover the workspace row — hover card should appear
|
||||
await expectHoverCard(page, workspace.name);
|
||||
|
||||
// Assert the card shows the workspace name
|
||||
await expectWorkspaceNameInCard(page, workspace.name);
|
||||
|
||||
// Assert the "web" script entry exists in the card
|
||||
await expectScriptInCard(page, "web");
|
||||
|
||||
// Assert the status dot shows "running"
|
||||
await expectScriptRunning(page, "web");
|
||||
|
||||
// Assert the script row is a link (has role="link")
|
||||
const card = page.getByTestId("workspace-hover-card");
|
||||
const serviceLink = card.getByRole("link", { name: "web script" });
|
||||
await expect(serviceLink).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Move mouse away — card should dismiss
|
||||
await expectHoverCardDismissed(page);
|
||||
} finally {
|
||||
await client.close();
|
||||
await repo.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("shows stopped scripts and starts them from the hover card", async ({ page }) => {
|
||||
const client = await connectWorkspaceSetupClient();
|
||||
const repo = await createTempGitRepo("hovercard-start-", {
|
||||
paseoConfig: {
|
||||
scripts: {
|
||||
web: {
|
||||
command:
|
||||
"node -e \"const http = require('http'); const s = http.createServer((q,r) => r.end('ok')); s.listen(process.env.PORT || 3000, '127.0.0.1', () => console.log('listening on ' + s.address().port))\"",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await seedProjectForWorkspaceSetup(client, repo.path);
|
||||
const workspace = await client.openProject(repo.path);
|
||||
if (!workspace.workspace || workspace.error) {
|
||||
throw new Error(workspace.error ?? `Failed to open project ${repo.path}`);
|
||||
}
|
||||
|
||||
await openHomeWithProject(page, repo.path);
|
||||
const wsRow = page.getByTestId(
|
||||
`sidebar-workspace-row-${getServerId()}:${workspace.workspace.id}`,
|
||||
);
|
||||
await expect(wsRow).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await expectHoverCard(page, workspace.workspace.name);
|
||||
await expectWorkspaceNameInCard(page, workspace.workspace.name);
|
||||
await expectScriptInCard(page, "web");
|
||||
await expectScriptStopped(page, "web");
|
||||
await expectScriptHealth(page, "web", "Unknown");
|
||||
|
||||
const card = page.getByTestId("workspace-hover-card");
|
||||
const startButton = card.getByTestId("hover-card-script-start-web");
|
||||
await expect(startButton).toBeVisible({ timeout: 10_000 });
|
||||
await startButton.click();
|
||||
|
||||
await expectScriptRunning(page, "web");
|
||||
await expectScriptHealth(page, "web", "Healthy");
|
||||
await expect(card.getByRole("link", { name: "web script" })).toBeVisible({ timeout: 10_000 });
|
||||
await expect(startButton).not.toBeVisible({ timeout: 10_000 });
|
||||
} finally {
|
||||
await client.close();
|
||||
await repo.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -6704,6 +6704,13 @@ export class Session {
|
||||
sessionLogger: this.sessionLogger,
|
||||
terminalManager: this.terminalManager,
|
||||
archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
|
||||
scriptRouteStore: this.scriptRouteStore,
|
||||
scriptRuntimeStore: this.scriptRuntimeStore,
|
||||
getDaemonTcpPort: this.getDaemonTcpPort,
|
||||
getDaemonTcpHost: this.getDaemonTcpHost,
|
||||
onScriptsChanged: (workspaceDirectory) => {
|
||||
this.emitWorkspaceScriptStatusUpdate(workspaceDirectory);
|
||||
},
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
@@ -28,8 +28,11 @@ import {
|
||||
createAgentWorktree,
|
||||
createWorktreeSetupProgressAccumulator,
|
||||
getWorktreeSetupProgressResults,
|
||||
spawnWorktreeScripts,
|
||||
} from "./worktree-bootstrap.js";
|
||||
import type { TerminalManager } from "../terminal/terminal-manager.js";
|
||||
import type { ScriptRouteStore } from "./script-proxy.js";
|
||||
import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
|
||||
import { getCheckoutStatusLite, resolveRepositoryDefaultBranch } from "../utils/checkout-git.js";
|
||||
import { expandTilde } from "../utils/path.js";
|
||||
import {
|
||||
@@ -103,6 +106,11 @@ type CreatePaseoWorktreeInBackgroundDependencies = {
|
||||
sessionLogger: Logger;
|
||||
terminalManager: TerminalManager | null;
|
||||
archiveWorkspaceRecord: (workspaceId: string) => Promise<void>;
|
||||
scriptRouteStore: ScriptRouteStore | null;
|
||||
scriptRuntimeStore: WorkspaceScriptRuntimeStore | null;
|
||||
getDaemonTcpPort: (() => number | null) | null;
|
||||
getDaemonTcpHost: (() => string | null) | null;
|
||||
onScriptsChanged: ((workspaceDirectory: string) => void) | null;
|
||||
};
|
||||
|
||||
type HandleWorkspaceSetupStatusRequestDependencies = {
|
||||
@@ -828,6 +836,28 @@ export async function runWorktreeSetupInBackground(
|
||||
emitSetupProgress("completed", null);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
options.shouldBootstrap &&
|
||||
dependencies.terminalManager &&
|
||||
dependencies.scriptRouteStore &&
|
||||
dependencies.scriptRuntimeStore
|
||||
) {
|
||||
await spawnWorktreeScripts({
|
||||
repoRoot: worktree.worktreePath,
|
||||
workspaceId: worktree.worktreePath,
|
||||
branchName: worktree.branchName,
|
||||
daemonPort: dependencies.getDaemonTcpPort?.() ?? null,
|
||||
daemonListenHost: dependencies.getDaemonTcpHost?.() ?? null,
|
||||
routeStore: dependencies.scriptRouteStore,
|
||||
runtimeStore: dependencies.scriptRuntimeStore,
|
||||
terminalManager: dependencies.terminalManager,
|
||||
logger: dependencies.sessionLogger,
|
||||
onLifecycleChanged: () => {
|
||||
dependencies.onScriptsChanged?.(worktree.worktreePath);
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof WorktreeSetupError) {
|
||||
setupResults = error.results;
|
||||
|
||||
Reference in New Issue
Block a user