Skip to content

Flaky: ExpensesPage pagination test times out at 5000ms on loaded CI runners #563

Closed as not planned

Description

@mforce

Observed on PR #561, which changes no web/ files at all (10 files, all src/Cluckwork.Infrastructure and tests/Cluckwork.Api.IntegrationTests).

What happened

Web typecheck, test, and build failed:

FAIL src/routes/ExpensesPage.test.tsx > ExpensesPage pagination
     > appends the next page when 'load more' is clicked (offset advances by a full page)
Error: Test timed out in 5000ms.
 ❯ src/routes/ExpensesPage.test.tsx:265:3

Test Files  1 failed | 82 passed (83)
     Tests  1 failed | 1778 passed (1779)

Re-run of the same commit, with no code change, passed — and finished faster (1m45s vs 2m6s). Non-deterministic by definition.

Why it is fragile

The test builds a 100-row first page:

const page1: Expense[] = Array.from({ length: 100 }, (_, i) => ({ ... }));

then does two findByRole("row", { name: /…/ }) lookups against it. A role query with a name regex walks the accessibility tree across every row, and it runs twice (once before the click, once after). That is a lot of work under Vitest's default 5000ms timeout.

The failing run's own totals show how loaded the suite is: tests 188.49s, environment 72.27s, wall clock 105s for 1779 tests. On a busy runner, that query exceeding 5s does not require anything to be wrong.

Why this matters beyond one red run

It is a coverage-gated job, so a flake here blocks unrelated PRs — this one is a tenancy slice that touches no frontend code. The failure mode is also the least informative kind: a timeout, with no assertion to point at, so the first instinct is to go looking for a regression that is not there.

Options

  1. Give the test an explicit timeout — smallest change, but it treats the symptom and the next slow runner moves the goalposts again.
  2. Shrink the fixture. The assertion needs items.length === PAGE to make the component set hasMore; if PAGE is a constant the test can import, a smaller page size in the test would exercise the same branch with a fraction of the DOM. This is the fix worth pricing first.
  3. Query by something cheaper than a row-name regex — e.g. findByText on the sentinel, then assert row membership — so the a11y-tree walk is not on the hot path twice.

Option 2 keeps the guarantee the test exists for (offset advances by a full page, rows append rather than replace) while removing the reason it is slow.

Not fixed in #561

Deliberately. It is a frontend test unrelated to that PR's tenancy work and outside its file allow-list; patching an unrelated flake inside a tenant-isolation change is how scope leaks. Filed here so the re-run does not simply bury it.

Related: Playwright prefill-wipe race territory — async UI assertions that pass locally and fail on a loaded CI runner.

Activity

  1. mforce commented on Aug 31, 2026

    @mforce
    OwnerAuthor

    Duplicate of #557, which reports the same test (ExpensesPage pagination > appends the next page when 'load more' is clicked) timing out at 5000ms under CI load, and was filed first.

    The root-cause analysis in this body — the 100-row first page making each name-filtered findByRole walk a large accessibility tree — has been copied into #557 so it is not lost. Track the fix there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions