Skip to content

test(e2e): fix the version-history flake — require rows inside the toPass() retry - #232

Merged
rubenvdlinde merged 2 commits into
developmentfrom
fix/openbuild-version-history-flake
Aug 17, 2026
Merged

test(e2e): fix the version-history flake — require rows inside the toPass() retry#232
rubenvdlinde merged 2 commits into
developmentfrom
fix/openbuild-version-history-flake

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Follow-up to #229 (merged). This commit was pushed to fix/green-openbuild after that PR merged, so it never reached development. Rebuilt as a one-commit branch off current origin/development (7002e4f8).

The finding: two runs of the same sha disagreed

version-rollback.spec.ts:314 is non-deterministic, and I nearly filed it as a regression from my own change. What settled it was the paired run — every PR here produces both a push and a pull_request run, i.e. two independent executions of identical code:

sha 6349aa6a
  push run 31977864672 → 192 passed, 0 failed, 65 skipped   ← fully green
  PR   run 31977866585 → 191 passed, 1 failed, 65 skipped

Same commit, same suite, same 65 skips, opposite outcomes. The spec took 36.6 s on the run that failed and 49.7 s on the run that passed. It also passes on development and passed at the previous sha. A single red run is one sample.

Why it races, and why the existing hardening did not cover it

openVersionHistory() was already an idempotent open-and-click under expect(...).toPass() — the right pattern, and it works: it returns only once the panel body is on screen. The gap was that the caller then re-asserted the same condition after it returned:

await openVersionHistory(page, await appUuid(page))
await expect(page.locator('.version-history__row').first()).toBeVisible({ timeout: 20_000 })

The detail page re-mounts the sidebar after first paint (the helper's own comment block records this). When that re-mount lands in the window between the helper returning and the caller asserting, nothing can re-open it any more — the retry that exists for exactly this is one statement upstream. The failure then reads as the app hiding its own rows: the locator resolved to a real <li class="version-history__row version-history__row--current"> on all 36 polls of the 20 s wait, hidden every time.

The change

Move the same assertion inside the retry via an opt-in requireRows, and drop the now-redundant external one at both affected call sites.

  • Nothing is weakened. The condition asserted is identical; it is merely recoverable now. For these two callers .version-history__empty correctly stops counting as success.
  • The third call site keeps the either/or default on purpose. It asserts toHaveCount(3), and toHaveCount() counts DOM nodes regardless of visibility — which is precisely why it has never flaked. Harmonising it with the other two would import the flake into the stable test.

Weakening the assertion to silence the flake would have been the wrong trade; this does the opposite.

Verification, and its limits

  • tsc --noEmit on the edited spec: exit 0, no output.
  • Positive control, because a clean tsc run and a zero-file run print the same nothing: the same file with requireRows misspelled fails TS2561, exit 2. So the check genuinely ran. (playwright test --list would not have caught this — it is a parse check, not a type check.)

Two caveats I am not glossing over:

  • check:manifest has never run locally here — the worktree has no node_modules, so the command errors on a missing module. That is "did not run", not a pass. CI is the first real measurement.
  • This branch's own CI has not been observed yet. The flake is intermittent by definition, so a single green run on this PR is weak evidence; the paired push/PR runs are the stronger signal, and even they cannot prove absence.

…nt after it

The two E2E runs of the SAME sha (6349aa6) disagreed on
`version-rollback.spec.ts:314`: the pull_request run failed it in 36.6s,
the push run passed it in 49.7s with 192/192. Identical code, so this is a
race, not a defect — and the failing shape is the one this file already
documents.

`openVersionHistory()` is already an idempotent open-and-click under
toPass(), and it works: it returns only once the panel body is on screen.
What it could not cover was the caller's OWN assertion, made after it
returned:

    await openVersionHistory(page, await appUuid(page))
    await expect(page.locator('.version-history__row').first()).toBeVisible(...)

The detail page re-mounts the sidebar after first paint — the helper's own
comment block records this — and when that re-mount lands in the window
between the helper returning and the caller asserting, there is no longer
anything that can re-open it. The caller then polls a hidden element for 20s
and reports the app hiding its rows. The CI log shows exactly that: the
locator resolved to a real
`<li class="version-history__row version-history__row--current">` on all 36
polls, `hidden` every time.

So move the same assertion inside the loop via an opt-in `requireRows`, and
drop the now-redundant external one at both call sites. Nothing is weakened:
the condition asserted is identical, and for these two callers
`.version-history__empty` correctly stops counting as success. The third
call site keeps the either/or default — it asserts toHaveCount(), which
counts DOM nodes regardless of visibility, which is why it never flaked.

Type-checked with tsc --noEmit (exit 0). Positive control, because a clean
tsc run and a zero-file run print the same nothing: the same file with
`requireRows` misspelled fails TS2561, exit 2.
`quality / Frontend Check (format)` was the one red job on this PR. Fixed by
running the project's OWN command (`npm run format`, i.e. the quoted
`prettier --check "**/*.{js,ts,vue,css,scss}"`), not a hand-rolled prettier
invocation — an unquoted glob in an npm script silently narrows what is
checked, and this fleet has been bitten by that before.

The check named exactly one file, this one, and the fix is layout only: the
ternary assigning `panelBody` is reflowed onto its own lines. Verified from
the DIFF rather than the exit code, because an autofix that rewrites a string
literal is a semantic change wearing a formatting costume — on shillinq that
shape orphaned 17 translations across 36 locales by editing a `t()` key. Here
both selector literals are byte-identical, no `t()` is involved, and no
Playwright selector text moved.

Re-verified after the reflow: `npm run format` exit 0, `tsc --noEmit` exit 0.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openbuild @ 0cdabc8

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
test-l10n
check-gitignore
check-nc-floor
format
composer ✅ 106/106
npm ✅ 626/626
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-17 00:07 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openbuild @ ae2cb4b

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
test-l10n
check-gitignore
check-nc-floor
format
composer ✅ 106/106
npm ✅ 626/626
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-17 00:45 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 6401e1e into development Aug 17, 2026
80 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants