import { createServer, type Server } from "node:http";
import { expect, test, type Page, type TestInfo } from "@playwright/test";
let server: Server;
let appUrl: string;
test.beforeAll(async () => {
// Compile this separate fixture entrypoint in setup, like the main app warmup.
// Cold Metro compilation must not consume the recovery interaction's timeout.
test.setTimeout(120_000);
const metroPort = process.env.E2E_METRO_PORT;
if (!metroPort) throw new Error("E2E_METRO_PORT is required");
const bundle = `http://localhost:${metroPort}/packages/app/e2e/support/recovery-app.bundle?platform=web&dev=true&minify=false&hot=false&lazy=true&transform.engine=hermes&transform.routerRoot=src%2Fapp`;
const compiled = await fetch(bundle, { signal: AbortSignal.timeout(120_000) });
if (!compiled.ok) throw new Error(`Recovery fixture compilation failed: HTTP ${compiled.status}`);
await compiled.arrayBuffer();
server = createServer((_request, response) => {
response.setHeader("Content-Type", "text/html");
response.end(
`
`,
);
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
if (!address || typeof address === "string") throw new Error("Fixture server did not start");
appUrl = `http://127.0.0.1:${address.port}`;
});
test.afterAll(async () => {
await new Promise((resolve, reject) =>
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
}),
);
});
async function openBrokenStartup(page: Page) {
await page.addInitScript(() => {
localStorage.setItem(
"workspace-layout-state",
JSON.stringify({ state: { layoutByWorkspace: {} }, version: 1 }),
);
localStorage.setItem(
"paseo:last-workspace-route-selection",
JSON.stringify({ serverId: "fixture-host", workspaceId: "broken" }),
);
});
await page.goto(appUrl);
await expect(page.getByText("Paseo ran into a problem.", { exact: true })).toBeVisible();
}
async function readSavedWorkspaceState(page: Page) {
return page.evaluate(() => ({
layout: localStorage.getItem("workspace-layout-state"),
selection: localStorage.getItem("paseo:last-workspace-route-selection"),
}));
}
async function reloadToPicker(page: Page) {
await page.getByRole("button", { name: "Reload", exact: true }).click();
await expect(page).toHaveURL(`${appUrl}/open-project`);
await expect(page.getByText("Project picker", { exact: true })).toBeVisible();
await expect(page.getByText("Paseo ran into a problem.", { exact: true })).toHaveCount(0);
}
async function openHealthyWorkspace(page: Page) {
await page.getByRole("link", { name: "Open healthy workspace" }).click();
await expect(page.getByText("Healthy workspace", { exact: true })).toBeVisible();
}
async function breakCurrentWorkspace(page: Page) {
await page.getByRole("button", { name: "Break this workspace" }).click();
await expect(page.getByText("Paseo ran into a problem.", { exact: true })).toBeVisible();
}
async function captureRecoveryScreen(page: Page, testInfo: TestInfo, name: string) {
const path = testInfo.outputPath(`${name}.png`);
await page.screenshot({ path });
await testInfo.attach(name, { path, contentType: "image/png" });
}
test("Reload escapes a startup redirect and supports another recovery", async ({
page,
}, testInfo) => {
await openBrokenStartup(page);
const saved = await readSavedWorkspaceState(page);
await captureRecoveryScreen(page, testInfo, "error-screen");
await reloadToPicker(page);
expect(await readSavedWorkspaceState(page)).toEqual(saved);
await captureRecoveryScreen(page, testInfo, "recovered-picker");
await openHealthyWorkspace(page);
await breakCurrentWorkspace(page);
await reloadToPicker(page);
});
test.describe("compact error recovery", () => {
test.use({ viewport: { width: 390, height: 844 } });
test("Reload remains reachable and returns to the picker", async ({ page }, testInfo) => {
await openBrokenStartup(page);
await captureRecoveryScreen(page, testInfo, "compact-error-screen");
await reloadToPicker(page);
await captureRecoveryScreen(page, testInfo, "compact-recovered-picker");
});
});