Skip to content

test(e2e): tab-scope the toolbar-icon spec's action assertions#248

Merged
rejifald merged 1 commit into
mainfrom
test/toolbar-icon-spec-tab-scoped
Jul 17, 2026
Merged

test(e2e): tab-scope the toolbar-icon spec's action assertions#248
rejifald merged 1 commit into
mainfrom
test/toolbar-icon-spec-tab-scoped

Conversation

@rejifald

Copy link
Copy Markdown
Owner

What

De-flakes apps/e2e/src/offline/toolbar-icon.behavior.spec.ts ("blocking + count badge on a Russian feed" timed out on PR #247's CI run, which doesn't touch the toolbar path) by scoping every icon/badge assertion to the tab under test. Test-only — no product code changes.

Root cause

The failing poll was pinned at Received: "active" for its full 8s while the DOM guard had already confirmed concealment — so the blocking update did fire; the observation layer lost it:

  • lastIconState() read the globally last setIcon spy record, and actedTabBadge() derived its tab from the globally last setBadgeText record.
  • A non-web tab — the install-time onboarding tab or the launch-initial blank tab — resolves url: undefinednoPage hero → active posture in resolveActionIconState.
  • Under CI load the onboarding tab's onActivated/onUpdated repaint lands seconds late, after the youtube tab's blocking paint. Nothing repaints the youtube tab again, so both .at(-1) reads stay pinned to the wrong tab until timeout.

The mechanism was reproduced deterministically: forcing that late onboarding-tab activation after the blocking paint flips the old global read to active (the exact CI signature) while a tab-scoped read stays blocking.

Fix

  • Tab-scoped icon assertionstabIdFor() resolves the tab under test via chrome.tabs.query on the fixture URL (brief bounded retry); lastIconState(sw, tabId) filters spy records by it. (setIcon still needs the spy — there's no getIcon API.)
  • Badge read from browser truth — real per-tab getBadgeText({tabId}); the setBadgeText spy is removed.
  • off/exemptbringToFront() again after the settings flip: refreshActiveTabs() repaints only the active tab, so this guarantees an onActivated repaint of the test tab even if the onboarding tab held focus at flip time.
  • exempt badge clear is polledrefreshTabIcon writes icon-then-badge, so the icon reading exempt doesn't yet prove the badge write landed.
  • apps/e2e/AGENTS.md spec-table row updated to match (prettier renormalized the table's column padding, hence the whitespace-only line changes).

Reviewer notes

  • The SW's tabs.query returns no url for chrome-extension:// / about:blank tabs (<all_urls> host permissions don't cover those schemes; Movar deliberately lacks the tabs permission) — that's why tabIdFor matches the http(s) fixture URLs, which are visible under the e2e build's granted host permissions.
  • Product behavior is correct as-is: refreshTabIcon paints per-tab and Chrome holds per-tab icon/badge state; the global last-call read was simply the wrong observation point.

Verification

Run Result
Fixed spec, --repeat-each=10 --workers=2 (CI parity, 40 tests) 40/40
Blocking test, --repeat-each=25 --workers=6 (stress) 25/25
Deterministic mechanism repro (temp spec, not committed) old read pins active, tab-scoped stays blocking
tsc --noEmit, eslint, prettier clean

🤖 Generated with Claude Code

The blocking/badge assertions read the globally-last setIcon/setBadgeText
spy record, so a late repaint of a non-web tab — the install-time
onboarding tab or the launch-initial blank tab, both of which resolve to
the `active` posture (url undefined → noPage hero) — could mask the tab
under test's paint and pin the 8s poll at `active` for its full timeout.
Flaked exactly that way on CI run 29502943011 (PR #247): Expected
"blocking", Received "active", concealment confirmed by the DOM guard.

- Resolve the tab under test's id once (tabs.query on the fixture URL,
  brief bounded retry) and filter setIcon spy records by it; there is no
  getIcon API, so the spy stays — but scoped.
- Read the badge via the real per-tab getBadgeText({tabId}) instead of
  deriving the tab from the last setBadgeText record; the badge spy is
  gone entirely.
- off/exempt: bringToFront again after the settings flip —
  refreshActiveTabs repaints only the ACTIVE tab, so this guarantees an
  onActivated repaint of the test tab even if the onboarding tab held
  focus at flip time.
- Poll the exempt badge clear: refreshTabIcon writes icon-then-badge, so
  the icon flipping to `exempt` doesn't yet prove the badge write landed.

Verified: the mechanism reproduces deterministically (forcing the
onboarding-tab activation after the blocking paint pins the old global
read at `active` while the tab-scoped read stays `blocking`), and the
fixed spec passes 40/40 at --repeat-each=10 --workers=2 plus 25/25 of
the blocking test at --workers=6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rejifald
rejifald merged commit 665938b into main Jul 17, 2026
7 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.

1 participant