diff --git a/site/e2e/tests/app.spec.ts b/site/e2e/tests/app.spec.ts index 0d933c833f..b2600f1b30 100644 --- a/site/e2e/tests/app.spec.ts +++ b/site/e2e/tests/app.spec.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import * as http from "node:http"; -import { test } from "@playwright/test"; +import { expect, test } from "@playwright/test"; import { createTemplate, createWorkspace, @@ -20,54 +20,76 @@ test.beforeEach(async ({ page }) => { test("app", async ({ context, page }) => { const appContent = "Hello World"; const token = randomUUID(); - const srv = http - .createServer((_req, res) => { - res.writeHead(200, { "Content-Type": "text/plain" }); - res.end(appContent); - }) - .listen(0); - const addr = srv.address(); - if (typeof addr !== "object" || !addr) { - throw new Error("Expected addr to be an object"); - } const appName = "test-app"; - const template = await createTemplate(page, { - graph: [ - { - graph: { - resources: [ - { - agents: [ - { - token, - apps: [ - { - id: randomUUID(), - url: `http://localhost:${addr.port}`, - displayName: appName, - order: 0, - openIn: AppOpenIn.SLIM_WINDOW, - }, - ], - order: 0, - }, - ], - }, - ], - }, - }, - ], + + // Start an HTTP server to act as the workspace app backend. + const server = http.createServer((_req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end(appContent); }); - const workspaceName = await createWorkspace(page, template); - const agent = await startAgent(page, token); - // Wait for the web terminal to open in a new tab - const pagePromise = context.waitForEvent("page", { timeout: 10_000 }); - await page.getByText(appName).click({ timeout: 10_000 }); - const app = await pagePromise; - await app.waitForLoadState("domcontentloaded"); - await app.getByText(appContent).isVisible(); + // Wait for the server to be fully listening before proceeding. + // Using a callback avoids the race where address() is called + // before the socket is bound. + const port = await new Promise((resolve, reject) => { + server.on("error", reject); + server.listen(0, () => { + const addr = server.address(); + if (typeof addr !== "object" || !addr) { + reject(new Error("Expected address to be an AddressInfo")); + return; + } + resolve(addr.port); + }); + }); - await stopWorkspace(page, workspaceName); - await stopAgent(agent); + try { + const template = await createTemplate(page, { + graph: [ + { + graph: { + resources: [ + { + agents: [ + { + token, + apps: [ + { + id: randomUUID(), + url: `http://localhost:${port}`, + displayName: appName, + order: 0, + openIn: AppOpenIn.SLIM_WINDOW, + }, + ], + order: 0, + }, + ], + }, + ], + }, + }, + ], + }); + const workspaceName = await createWorkspace(page, template); + const agent = await startAgent(page, token); + + // Register the popup listener before clicking so we never miss + // the event. + const appPagePromise = context.waitForEvent("page"); + await page.getByRole("link", { name: appName }).click(); + const appPage = await appPagePromise; + + // SLIM_WINDOW opens about:blank first, then sets location.href + // to the proxied app URL. A retrying assertion tolerates the + // intermediate blank page and any app-proxy startup delay. + await expect(appPage.getByText(appContent)).toBeVisible({ + timeout: 30_000, + }); + + await stopWorkspace(page, workspaceName); + await stopAgent(agent); + } finally { + server.close(); + } });