fix(site): stabilize app.spec.ts e2e test (#24400)

The `app` e2e test flakes in CI with `waitForEvent("page")` or
`isVisible()` timeouts when opening a workspace app in a new tab.

The test had several timing hazards: the HTTP server address was read
before the socket finished binding, the popup event listener was
registered after the click, and content visibility used a point-in-time
check with the default 5s timeout.

Rewritten to wait for the server to listen, register the popup promise
before clicking, use `getByRole("link")` for a precise locator, and
replace `isVisible()` with a retrying `toBeVisible({ timeout: 30_000 })`
assertion that tolerates the intermediate `about:blank` page and
app-proxy startup delay. The test body is wrapped in `try/finally` to
ensure the HTTP server is always closed.

Fixes coder/internal#577

> Generated by Coder Agents

---

someone wise once said

> and I really don't know what we could do to make it more reliable
other than maybe trying to just rewrite it from scratch.
This commit is contained in:
Jeremy Ruppel
2026-04-20 10:21:36 -04:00
committed by GitHub
parent d99949df43
commit 4b755e514a
+69 -47
View File
@@ -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<number>((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();
}
});