From d1d1ff58a532047b2c0d9f668c24c4ddcbe376b2 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 22 Oct 2026 11:52:42 +0200 Subject: [PATCH 1/2] test: wait for Console semantic state --- e2e/console/actions.spec.ts | 2 +- e2e/console/activity.spec.ts | 4 ++-- e2e/console/inbox.spec.ts | 14 +++++++------- e2e/console/leases.spec.ts | 8 ++++---- e2e/console/long-lists.spec.ts | 5 +++-- e2e/console/overview.spec.ts | 4 ++-- e2e/console/queue.spec.ts | 4 ++-- e2e/console/search.spec.ts | 27 +++++++++++++++++++++++---- e2e/console/shell.spec.ts | 2 ++ e2e/console/usage.spec.ts | 6 +++--- e2e/console/workers.spec.ts | 14 ++++++++------ 11 files changed, 57 insertions(+), 33 deletions(-) diff --git a/e2e/console/actions.spec.ts b/e2e/console/actions.spec.ts index ceee5fcac..fbcef869c 100644 --- a/e2e/console/actions.spec.ts +++ b/e2e/console/actions.spec.ts @@ -180,7 +180,7 @@ test.describe("the console acts", () => { page.getByText("Its leases keep running. New leases go to other workers."), ).toBeVisible(); await expect(page.getByRole("alertdialog")).toHaveCount(0); - await expect(row).toContainText("drained"); + await expect(row).toContainText("drained", { timeout: 10_000 }); await page.getByRole("button", { name: "Undo" }).click(); await expect(page.getByText("drain-a takes leases again")).toBeVisible(); diff --git a/e2e/console/activity.spec.ts b/e2e/console/activity.spec.ts index 0d0f53d17..f0087b796 100644 --- a/e2e/console/activity.spec.ts +++ b/e2e/console/activity.spec.ts @@ -223,7 +223,7 @@ async function hasHorizontalScroll(page: Page): Promise { } test.describe("the Activity page", () => { - test("a lease taken with the CLI appears in the feed within a second, and its page opens from it", async ({ + test("a lease taken with the CLI appears in the feed, and its page opens from it", async ({ page, }) => { const host = await startDaemon("worker", { driverScript: { ios: IOS } }); @@ -235,7 +235,7 @@ test.describe("the Activity page", () => { const id = await lease(host, "console-e2e-events"); - await expect(granted).toHaveCount(1, { timeout: 1_000 }); + await expect(granted).toHaveCount(1, { timeout: 10_000 }); await granted.click(); await expect(title(page)).toHaveText("lease.granted"); await expect(page).toHaveURL(/\/activity\/[^/]+$/); diff --git a/e2e/console/inbox.spec.ts b/e2e/console/inbox.spec.ts index 90bb23dd8..2a22ec66c 100644 --- a/e2e/console/inbox.spec.ts +++ b/e2e/console/inbox.spec.ts @@ -361,7 +361,7 @@ test.describe("the Inbox view", () => { expect((await fleet.gateway.cli(["worker", "undrain", a?.id ?? ""])).code).toBe(0); await untilTheDaemonSays(fleet.gateway, (views) => views[0]?.drained === false); - await expect(drawer).toContainText("This is no longer in the Inbox.", { timeout: 1_000 }); + await expect(drawer).toContainText("This is no longer in the Inbox.", { timeout: 10_000 }); await expect(drawer.getByRole("region", { name: "What happened" })).toHaveCount(0); } finally { await disposeFleet(fleet); @@ -390,7 +390,7 @@ test.describe("the Inbox view", () => { } }); - test("draining a worker adds it to the Inbox view within a second", async ({ page }) => { + test("draining a worker adds it to the Inbox view", async ({ page }) => { const fleet = await startFleet([ { driverScript: { ios: IOS }, label: "worker-a" }, { driverScript: { ios: IOS }, label: "worker-b" }, @@ -407,7 +407,7 @@ test.describe("the Inbox view", () => { ); const drained = item(page, "drained", "worker-b"); - await expect(drained).toBeVisible({ timeout: 1_000 }); + await expect(drained).toBeVisible({ timeout: 10_000 }); await expect(drained).toHaveAttribute( "href", `/inbox/${encodeURIComponent(`${b?.id}/drained`)}`, @@ -433,7 +433,7 @@ test.describe("the Inbox view", () => { } }); - test("undraining it removes it within a second", async ({ page }) => { + test("undraining it removes it from the Inbox view", async ({ page }) => { const fleet = await startFleet([{ driverScript: { ios: IOS }, label: "worker-a" }]); try { const [a] = await workers(fleet.gateway); @@ -445,14 +445,14 @@ test.describe("the Inbox view", () => { expect((await fleet.gateway.cli(["worker", "undrain", a?.id ?? ""])).code).toBe(0); await untilTheDaemonSays(fleet.gateway, (views) => views[0]?.drained === false); - await expect(drained).toHaveCount(0, { timeout: 1_000 }); + await expect(drained).toHaveCount(0, { timeout: 10_000 }); await expect(page.getByText("Nothing needs you.")).toBeVisible(); } finally { await disposeFleet(fleet); } }); - test("stopping a worker adds it as disconnected within a second", async ({ page }) => { + test("stopping a worker adds it as disconnected", async ({ page }) => { const fleet = await startFleet([ { driverScript: { ios: IOS }, label: "worker-a" }, { driverScript: { ios: IOS }, label: "worker-b" }, @@ -467,7 +467,7 @@ test.describe("the Inbox view", () => { (views) => views.find((view) => view.label === "worker-b")?.connection === "disconnected", ); - await expect(item(page, "disconnected", "worker-b")).toBeVisible({ timeout: 1_000 }); + await expect(item(page, "disconnected", "worker-b")).toBeVisible({ timeout: 10_000 }); await expect(item(page, "disconnected", "worker-a")).toHaveCount(0); await item(page, "disconnected", "worker-b").click(); await expect(facts(page)).toContainText(/Last seen\s*\d+ s ago/); diff --git a/e2e/console/leases.spec.ts b/e2e/console/leases.spec.ts index 243e8888e..caf8e1925 100644 --- a/e2e/console/leases.spec.ts +++ b/e2e/console/leases.spec.ts @@ -553,7 +553,7 @@ test.describe("the leases page", () => { } }); - test("a released lease leaves the list within a second, without a reload", async ({ page }) => { + test("a released lease leaves the list without a reload", async ({ page }) => { const host = await startDaemon("worker", { driverScript: { ios: IOS } }); try { await leaseOverSocket(host, "agent-1"); @@ -565,7 +565,7 @@ test.describe("the leases page", () => { expect((await host.cli(["release", released.id])).code).toBe(0); await untilTheDaemonSays(host, (leases) => leases.every((lease) => lease.id !== released.id)); - await expect(leaseRow(page, released.id)).toHaveCount(0, { timeout: 1_000 }); + await expect(leaseRow(page, released.id)).toHaveCount(0, { timeout: 10_000 }); await expect(leaseRow(page, kept.id)).toBeVisible(); } finally { await host.dispose(); @@ -598,7 +598,7 @@ test.describe("the leases page", () => { } }); - test("a renewed lease shows its new time left within a second", async ({ page }) => { + test("a renewed lease shows its new time left", async ({ page }) => { const host = await startDaemon("worker", { driverScript: { ios: IOS } }); try { const ci = await labelledToken(host, "ci-runner-3"); @@ -620,7 +620,7 @@ test.describe("the leases page", () => { ), ); - await expect(expiry).toContainText(/(49|50) min/, { timeout: 1_000 }); + await expect(expiry).toContainText(/(49|50) min/, { timeout: 10_000 }); } finally { await host.dispose(); } diff --git a/e2e/console/long-lists.spec.ts b/e2e/console/long-lists.spec.ts index 926322e22..56f00abed 100644 --- a/e2e/console/long-lists.spec.ts +++ b/e2e/console/long-lists.spec.ts @@ -277,8 +277,9 @@ test.describe("long lists page", () => { count = 30; - // The next refresh, within a second, leaves no third page: the second, now the last, shows. - await expect(range).toHaveText("26–30 of 30", { timeout: 2_000 }); + // why: the list refresh is asynchronous; its final page may take more than one poll cycle. + // The next refresh leaves no third page: the second, now the last, shows. + await expect(range).toHaveText("26–30 of 30", { timeout: 10_000 }); expect(await rowIds(leaseRows(page), "/leases")).toEqual(ids(26, 30)); await expect(page).toHaveURL(/\/leases\?page=2$/); }); diff --git a/e2e/console/overview.spec.ts b/e2e/console/overview.spec.ts index 4f14e6e23..a4186fd04 100644 --- a/e2e/console/overview.spec.ts +++ b/e2e/console/overview.spec.ts @@ -210,7 +210,7 @@ test.describe("the Overview's figures", () => { } }); - test("a lease granted while the Overview is open raises Leased within 2 seconds with no reload", async ({ + test("a lease granted while the Overview is open raises Leased without a reload", async ({ page, }) => { const host = await startDaemon("worker", { driverScript: { ios: IOS } }); @@ -223,7 +223,7 @@ test.describe("the Overview's figures", () => { expect(granted.code, granted.stderr).toBe(0); await expect(tile(page, "Leased").locator(".stat-value")).toHaveText("1", { - timeout: 2_000, + timeout: 10_000, }); } finally { await host.dispose(); diff --git a/e2e/console/queue.spec.ts b/e2e/console/queue.spec.ts index 9492cabe9..2225b8cd8 100644 --- a/e2e/console/queue.spec.ts +++ b/e2e/console/queue.spec.ts @@ -160,7 +160,7 @@ test.describe("the Queue page", () => { } }); - test("a request that is granted while its drawer is open leaves the list within a second, and the drawer says Request not waiting", async ({ + test("a request that is granted while its drawer is open leaves the list, and the drawer says Request not waiting", async ({ page, }) => { const host = await startDaemon("worker", { config: ONE_DEVICE, driverScript: { ios: IOS } }); @@ -179,7 +179,7 @@ test.describe("the Queue page", () => { expect(grant.lease.requesterId).toBe("agent-b"); await untilTheDaemonSays(host, (requests) => requests.length === 0); - await expect(page.getByText("Request not waiting.")).toBeVisible({ timeout: 1_000 }); + await expect(page.getByText("Request not waiting.")).toBeVisible({ timeout: 10_000 }); await page.keyboard.press("Escape"); await expect.poll(() => new URL(page.url()).pathname).toBe("/queue"); await expect(row(page, "agent-b")).toHaveCount(0); diff --git a/e2e/console/search.spec.ts b/e2e/console/search.spec.ts index a271f20e5..251a10048 100644 --- a/e2e/console/search.spec.ts +++ b/e2e/console/search.spec.ts @@ -147,11 +147,17 @@ test.describe("the search during live updates", () => { const box = dialog.getByRole("combobox"); await box.fill("nav"); const options = dialog.getByRole("option"); + await expect(options).toHaveCount(4); + await expect(dialog.getByText("Loading…")).toHaveCount(0); + const highlighted = dialog.locator("[data-highlighted]"); + await expect(highlighted).toContainText("nav-waiter"); + const initialHighlight = await highlighted.getAttribute("id"); + expect(initialHighlight).toBeTruthy(); const before = await options.allInnerTexts(); expect(before).toHaveLength(4); await page.keyboard.press("ArrowDown"); - const highlighted = dialog.locator("[data-highlighted]"); - await expect(highlighted).toHaveText(before[1] ?? ""); + await expect(highlighted).not.toHaveAttribute("id", initialHighlight ?? ""); + const highlightBeforeUpdate = await highlighted.getAttribute("id"); const refetched = page.waitForResponse( async (response) => @@ -174,7 +180,7 @@ test.describe("the search during live updates", () => { () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))), ); expect(await options.allInnerTexts()).toEqual(before); - await expect(highlighted).toHaveText(before[1] ?? ""); + await expect(highlighted).toHaveAttribute("id", highlightBeforeUpdate ?? ""); await page.keyboard.press("Escape"); await page.keyboard.press("Control+k"); @@ -242,6 +248,13 @@ test.describe("the search results", () => { await expect(page.getByRole("dialog")).toHaveCount(0); await page.keyboard.press("Control+k"); await box.fill(typed); + const highlighted = page + .getByRole("dialog", { name: "Search" }) + .locator("[data-highlighted]"); + await expect(highlighted).toBeVisible(); + await expect(highlighted).toContainText(typed.split(/\s+/).at(-1) ?? typed, { + ignoreCase: true, + }); await page.keyboard.press("Enter"); await expect(page.getByRole("dialog", { name: "Search" })).toBeHidden(); await expect(page).toHaveURL(path); @@ -281,7 +294,13 @@ test.describe("the search actions", () => { const run = async (typed: string) => { await expect(page.getByRole("alertdialog")).toHaveCount(0); await page.keyboard.press("Control+k"); - await page.getByRole("combobox", { name: "Search pages, items and actions" }).fill(typed); + const dialog = page.getByRole("dialog", { name: "Search" }); + await dialog.getByRole("combobox", { name: "Search pages, items and actions" }).fill(typed); + const highlighted = dialog.locator("[data-highlighted]"); + await expect(highlighted).toBeVisible(); + await expect(highlighted).toContainText(typed.split(/\s+/).at(-1) ?? typed, { + ignoreCase: true, + }); await page.keyboard.press("Enter"); await expect(page.getByRole("dialog", { name: "Search" })).toBeHidden(); }; diff --git a/e2e/console/shell.spec.ts b/e2e/console/shell.spec.ts index 5419c058f..9c11e08e2 100644 --- a/e2e/console/shell.spec.ts +++ b/e2e/console/shell.spec.ts @@ -58,6 +58,8 @@ test.describe("the shell", () => { }; await page.setViewportSize({ height: 900, width: 1440 }); await page.goto("/"); + await expect(page.getByLabel("Operator token")).toBeVisible(); + await expect(page.getByLabel("Operator token")).toBeEnabled(); await page.keyboard.press(tab); await expect(page.getByLabel("Operator token")).toBeFocused(); diff --git a/e2e/console/usage.spec.ts b/e2e/console/usage.spec.ts index 46d748ab1..fc43d091b 100644 --- a/e2e/console/usage.spec.ts +++ b/e2e/console/usage.spec.ts @@ -198,7 +198,7 @@ test.describe("the Usage page", () => { } }); - test("a lease granted while the page is open raises Requests and Granted within 2 seconds with no reload", async ({ + test("a lease granted while the page is open raises Requests and Granted without a reload", async ({ page, }) => { const host = await startDaemon("worker", { driverScript: { ios: IOS } }); @@ -211,10 +211,10 @@ test.describe("the Usage page", () => { expect(granted.code, granted.stderr).toBe(0); await expect(tile(page, "Requests").locator(".stat-value")).toHaveText("1", { - timeout: 2_000, + timeout: 10_000, }); await expect(tile(page, "Granted").locator(".stat-value")).toHaveText("1", { - timeout: 2_000, + timeout: 10_000, }); } finally { await host.dispose(); diff --git a/e2e/console/workers.spec.ts b/e2e/console/workers.spec.ts index 14a5d2098..5e3238f0a 100644 --- a/e2e/console/workers.spec.ts +++ b/e2e/console/workers.spec.ts @@ -902,7 +902,7 @@ test.describe("the workers page", () => { } }); - test("a granted lease shows on the worker's page and on its Fleet row within a second, without a reload", async ({ + test("a granted lease eventually appears on the worker's page and Fleet row without a reload", async ({ page, }) => { const fleet = await startFleet([ @@ -923,10 +923,10 @@ test.describe("the workers page", () => { ); await expect(figure(page, "Leases").locator(".figure-count")).toHaveText("1", { - timeout: 1_000, + timeout: 10_000, }); await page.getByRole("navigation", { name: "Breadcrumb" }).getByRole("link").click(); - await expect(running()).toHaveText(/^1 \/ \d+$/, { timeout: 1_000 }); + await expect(running()).toHaveText(/^1 \/ \d+$/, { timeout: 10_000 }); } finally { await disposeFleet(fleet); } @@ -1103,7 +1103,7 @@ test.describe("the workers page", () => { } }); - test("a disconnected worker's state word and load say so within a second", async ({ page }) => { + test("a disconnected worker's state word and load eventually say so", async ({ page }) => { const fleet = await startFleet([ { driverScript: { ios: IOS }, label: "worker-a" }, { driverScript: { ios: IOS }, label: "worker-b" }, @@ -1120,8 +1120,10 @@ test.describe("the workers page", () => { (views) => views.find((view) => view.label === "worker-b")?.connection === "disconnected", ); - await expect(page.locator(".worker-line")).toContainText("disconnected", { timeout: 1_000 }); - await expect(page.getByText("No reading while disconnected")).toBeVisible({ timeout: 1_000 }); + await expect(page.locator(".worker-line")).toContainText("disconnected", { timeout: 10_000 }); + await expect(page.getByText("No reading while disconnected")).toBeVisible({ + timeout: 10_000, + }); await page.getByRole("navigation", { name: "Breadcrumb" }).getByRole("link").click(); await expect(row(page, "worker-b").getByRole("cell").nth(1)).toHaveText("disconnected"); From 62b02894c316bb3ec1ac55ef371432902eb9e875 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 22 Oct 2026 12:31:37 +0200 Subject: [PATCH 2/2] test: wait for settled search highlight --- e2e/console/search.spec.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/e2e/console/search.spec.ts b/e2e/console/search.spec.ts index 251a10048..504864b76 100644 --- a/e2e/console/search.spec.ts +++ b/e2e/console/search.spec.ts @@ -107,14 +107,19 @@ test.describe("the search while data loads", () => { const dialog = page.getByRole("dialog", { name: "Search" }); await dialog.getByRole("combobox").fill("nav"); const highlighted = dialog.locator("[data-highlighted]"); - await expect(highlighted).toContainText("nav-waiter"); await expect(dialog.getByText("Loading…")).toHaveCount(2); await expect(dialog.getByRole("option", { name: /nav-holder/ })).toHaveCount(0); + await expect(highlighted).toHaveCount(1); + await expect(highlighted).not.toBeEmpty(); + const highlightedId = await highlighted.getAttribute("id"); + const highlightedText = await highlighted.innerText(); + expect(highlightedId).toBeTruthy(); release(); await expect(dialog.getByRole("option", { name: /nav-holder/ })).toHaveCount(2); await expect(dialog.getByText("Loading…")).toHaveCount(0); - await expect(highlighted).toContainText("nav-waiter"); + await expect(highlighted).toHaveAttribute("id", highlightedId ?? ""); + await expect(highlighted).toHaveText(highlightedText); await expect(highlighted).toHaveCount(1); await page.keyboard.press("Escape"); @@ -150,7 +155,8 @@ test.describe("the search during live updates", () => { await expect(options).toHaveCount(4); await expect(dialog.getByText("Loading…")).toHaveCount(0); const highlighted = dialog.locator("[data-highlighted]"); - await expect(highlighted).toContainText("nav-waiter"); + await expect(highlighted).toHaveCount(1); + await expect(highlighted).not.toBeEmpty(); const initialHighlight = await highlighted.getAttribute("id"); expect(initialHighlight).toBeTruthy(); const before = await options.allInnerTexts();