Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 1e928a6e48cef0c9652dc80e477f9a72e170fc06
parent 607b04dfc664b90042702a0b493dfcf874511e77
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sun, 20 Sep 2026 18:33:02 -0400

e2e: match storage headers by text, and retry the clicks React has not wired yet

A column header's accessible name comes from its sort button's aria-label, as
channels-sort.spec.ts already found; and a click that lands before hydration
fires nothing at all, which is this suite's long-standing flake. Both actions
are idempotent, so the poll is safe.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Meditor/e2e/channels-storage-columns.spec.ts | 51+++++++++++++++++++++++++++++++++++++++------------
1 file changed, 39 insertions(+), 12 deletions(-)

diff --git a/editor/e2e/channels-storage-columns.spec.ts b/editor/e2e/channels-storage-columns.spec.ts @@ -93,7 +93,11 @@ test("the Location and Size columns say which disk and how much", async ({ await expect(volumes.getByLabel("volume cold")).toContainText("free"); // FREE SPACE IS NOT A COLUMN. It is a fact about a disk; one read-out per // volume, not one per row. - await expect(page.getByRole("columnheader", { name: /free/i })).toHaveCount(0); + // (Matched on TEXT, not on the accessible name: each header's name comes from + // its sort button's aria-label, as channels-sort.spec.ts discovered.) + await expect( + page.getByRole("columnheader").filter({ hasText: /free/i }), + ).toHaveCount(0); }); test("a channel with no report reads as unmeasured, never as empty", async ({ @@ -158,24 +162,47 @@ test("filter by volume, sort by size, and free up N GB", async ({ // /storage links here with it, so the "largest first" list it promised is the // list that renders. await page.goto("/channels?sort=size"); - await expect( - page.getByRole("columnheader", { name: /^Size/ }), - ).toHaveAttribute("aria-sort", "descending"); + const sizeHeader = page + .getByRole("columnheader") + .filter({ hasText: /^Size/ }); + await expect(sizeHeader).toHaveAttribute("aria-sort", "descending"); // And clicking the header flips it, like every other sortable column. - await page.getByRole("button", { name: "sort by Size" }).click(); - await expect( - page.getByRole("columnheader", { name: /^Size/ }), - ).toHaveAttribute("aria-sort", "ascending"); + await expect + .poll( + async () => { + await page + .getByRole("button", { name: "sort by Size" }) + .click({ timeout: 5_000 }) + .catch(() => {}); + return sizeHeader.getAttribute("aria-sort"); + }, + { timeout: 30_000, intervals: [250, 500, 1_000, 1_000, 2_000] }, + ) + .toBe("ascending"); // --- free up N GB -------------------------------------------------------- await page.goto("/channels"); // Nothing ticked, so the deck is not there — which is why the control lives // in the volume bar and not on the deck. await expect(page.getByLabel("channel priority bulk")).toHaveCount(0); - await page.getByLabel("free up target GB").fill("1"); - await page - .getByLabel("select largest channels to free up") - .click(); + // CLICK UNTIL IT TOOK. Every control here is a React handler attached after + // the server HTML arrives; Playwright's actionability check is satisfied by + // the BUTTON, not by the listener, so a click landing in that window is + // silently dropped. The action is idempotent — the same selection is computed + // from the same rows — so retrying until the consequence appears is both safe + // and the honest way to wait for a listener the DOM cannot advertise. + const target = page.getByLabel("free up target GB"); + const selectLargest = page.getByLabel("select largest channels to free up"); + await expect + .poll( + async () => { + await target.fill("1").catch(() => {}); + await selectLargest.click({ timeout: 5_000 }).catch(() => {}); + return page.getByLabel(`select ${SLUG}`).isChecked(); + }, + { timeout: 30_000, intervals: [250, 500, 1_000, 1_000, 2_000] }, + ) + .toBe(true); // It ticked the in-place channel and NOT the one already on the platter — // moving that frees nothing on the disk being emptied.