v4.46 — Storybook play() CI Enforcement
Every Storybook
play()function — and every story's mere render, whether or not it defines one — now runs as a real Vitest test on every PR, in headless Chromium, as a required check with nocontinue-on-error.
Summary
Before this milestone, a Storybook play() function ran only when a human opened Storybook and clicked that specific story. npm run test:unit, npm run test:cov, .github/workflows/ci.yml, and storybook.yml (which builds and deploys Storybook but never executes a story) all never touched it. That gap was not hypothetical: Tess's OBJ-2919 fix added outro-reachability assertions to FeatureTour.stories.ts after a story silently rendered the wrong state with nothing to catch it — and those very assertions were unenforced from the moment they shipped, because nothing in CI ran play() at all.
This milestone wires @storybook/addon-vitest's storybookTest() plugin into a required CI job. The wiring itself is small; the risk is not. storybookTest() doesn't only run play()-bearing stories — it generates a render-and-assert-no-error smoke test for every story it discovers, so the change takes CI coverage from 0 to 2,543 tests across 429 files in one commit's worth of enforcement, none of which had ever been executed headlessly before. Shipping that safely, with zero tolerance for the soft-launch shortcut (continue-on-error) this repo's own CLAUDE.md explicitly forbids, is the actual difficulty this milestone solves.
Phase 1 built the harness with no CI changes at all and turned "what breaks?" from speculation into a number: 85 of 2,503 stories failing across 22 files, plus four deviations that reshaped everything downstream — storybookTest() never loads the root vite.config.ts, a new dependency was required, the suite hung indefinitely at teardown, and one story file silently vanished from the full run while passing standalone. Phase 2 sized itself against that inventory and grew from a planned two waves into six, not from scope creep but from one causal chain only live execution could surface: seeding a signed-in Storybook session un-gated dozens of previously-inert queries, which exposed dead decorators, which required making unhandled rejections attributable, which surfaced a residue sweep, which led to a real @ionic/core teardown-race fix. Phase 3 made the job required, closed two more pre-requisite flakes first under the rule that "a job born required cannot be born flaky," and — re-measuring on real CI hardware rather than trusting the local baseline — discovered the suite could exhaust a hosted runner's disk before it ever exhausted the runtime budget, shipping a 4-way file-list shard as the fix.
Goal
Make every Storybook story's
play()function a real, CI-enforced regression test — without turningmasterred on 2,508 stories that have never once been executed by a machine.
Scope — What Shipped
- Harness:
ionic_frontend/.storybook/vitest.setup.ts(setProjectAnnotations) andionic_frontend/vitest.storybook.config.ts— a standalone Vitest config runningstorybookTest()in browser mode against real headless Chromium, deliberately separate from the unit-test config so the app's hand-ratchetedtest:covcoverage thresholds (80/73/73/78) never move. - Harness fixes: the Clerk Vue plugin installed in
.storybook/preview.ts's mock setup, a missing mock-router route, a deterministic fix for a story file that silently vanished from full-suite discovery, and avuemodule-realm dedupe fix for severaldefineComponent is not a functionfailures. - Component + story fixes: a genuine infinite reactive-update loop in
SupporterUpgradeModal, a 610-event GraphQL retry loop inWidgetSetupSheetthat was the suite's teardown-hang root cause, a sharedSyntaxErroracross three settings sections, an Ionicpresent()timing bug inStreakDetailsModal, plus mechanical story-wiring and selector fixes across a dozen more files. - Tier C remediation: all 11 empty
play()bodies (CommunityAllyInviteInterstitial×5,BetterTogetherInterstitial×6) replaced with real assertions or removed; ~30 optional-chained?.click()no-ops converted to throwinggetBy*/findBy*queries so a wrong selector fails instead of silently passing. - CI: a new
storybook-testjob in.github/workflows/ci.yml, 4-way file-list sharded (ionic_frontend/scripts/partition-storybook-files.mjs), wrapped in both a job-level and command-level timeout, added to theREQUIRED_JOBScontinue-on-error guard allowlist.ionic_frontend/scripts/check-storybook-file-count.mjsenforces a discovered-vs-reported completeness invariant per shard. - Docs:
docs/development/testing.md§ Storybook (CI enforcement, local run commands, the semantic coverage policy, the Tier C anti-pattern) and a newdocs/development/gotchas.mdentry, cross-linked from the pre-existing OBJ-2919 entry;CHANGELOG.md [Unreleased]entry.
Phases
| Phase | Name | Status | Plans | Highlights |
|---|---|---|---|---|
| 1 | Harness + failure inventory | Shipped | 1 | Wired storybookTest() with zero CI changes; produced a counted 85-failure inventory across 22 of 433 files; found storybookTest() doesn't load the root Vite config and the suite hangs indefinitely at teardown after every test reports. |
| 2 | Remediate | Shipped | 1 (8 PRs, 6 waves) | Fixed all inventoried failures plus a fully unplanned Wave C causal chain surfaced by seeding a signed-in session; killed every Tier C structurally-unfailable play(); verified 429/429 files green on three consecutive runs with unchanged coverage thresholds. |
| 3 | Enforce + document | Shipped | 1 (2 PRs) | Made storybook-test a required CI job with no continue-on-error; re-measured on CI hardware and discovered a disk-exhaustion failure mode invisible to the runtime-based sizing, fixed via 4-way sharding; wrote the gotcha and testing docs OBJ-2969 asked for. |
Key Decisions
- A separate
vitest.storybook.config.ts, not Storybook's scaffoldedtest.projectsentry — merging 2,503 browser-mode story renders into the app's coverage-gated unit-test run would move every one of its 12 hand-ratcheted per-file thresholds and force a Playwright browser install onto everynpm run test:unitinvocation. - No
continue-on-errorsoft-launch at any point — the obvious de-risking move (land the job non-blocking, watch it, then flip it) is forbidden by this repo's own CLAUDE.md gotcha that "acontinue-on-errorgate is an absent gate." Phase 1 shipped zero workflow changes; the job was born required in Phase 3, only after Phase 2 made it green. - Enforce all discovered stories, not just
play()-bearing ones —storybookTest()has no supported "only stories withplay()" mode, and the render-only smoke test is the exact mechanism that would have caught OBJ-2919's original bug (a story rendering the wrong state). Narrowing scope was explicitly ruled out at kickoff. - 4-way explicit file-list sharding, not Vitest's own
--shard— partitioning by an explicit, committed file list letscheck-storybook-file-count.mjsverify each shard's exact assigned subset, so the discovered-vs-reported completeness invariant survives sharding instead of being skipped for a "scoped" run. - Render-only stories are legitimate coverage, not a gap with a numeric target — Phase 3 resolved this explicitly as policy rather than leaving it implied: every story is executed by the harness regardless of whether it defines a
play(), catching mount crashes and provider/teardown issues on its own. The rule going forward is semantic, not a percentage: a story that claims a specific rendered state in its name or args must assert that state actually rendered. - The
master CI gateruleset promotion is a request, recorded — not a silent gap. Promotingstorybook-testinto the branch-protection ruleset's required contexts is a repo-admin action outside the crew's authority. The milestone's own acceptance criterion was written as "request it and record the outcome," and that is what shipped — stated honestly here rather than rounded up to a flat "done."
Requirements Coverage
5 / 5 milestone success criteria satisfied, 3 / 3 phase acceptance sets satisfied (see .planning/milestones/v4.46-storybook-play-ci-enforcement-MILESTONE-AUDIT.md).
| Success criterion | Status |
|---|---|
Every play() function executes on every PR | ✅ (job required in ci.yml's own guard; branch-protection ruleset promotion requested, not yet applied — non-blocking) |
OBJ-2919's FeatureTour outro-reachability assertion is enforced | ✅ |
Zero play() functions that structurally cannot fail | ✅ |
| Coverage thresholds byte-identical to pre-milestone values | ✅ |
| The gotcha is written down where the next person will hit it | ✅ |
Outcomes
A regression that silently renders the wrong Storybook state — the exact OBJ-2919 shape — now fails a required PR check instead of waiting for a human to notice it in a dev Storybook tab. 2,543 tests across 429 files execute on every PR in real headless Chromium; 133 play() definitions across 50 files run as real Vitest assertions, with zero structurally-unfailable ones remaining. The suite runs in 221–280s locally and completes reliably on hosted CI runners via 4-way sharding, after two host-level failure modes (a teardown hang, a disk-exhaustion kill) were root-caused and fixed rather than retried away. The one thing not yet true: GitHub's own merge gate does not yet block on a red storybook-test result, because the master CI gate branch-protection ruleset hasn't been flipped — the job reports honestly on every PR today, and the promotion is requested and tracked (OBJ-3390), not silently dropped.
Tech Debt
- (Phase 3, ENFORCE-2) The
master CI gateruleset's required-contexts promotion forstorybook-testis requested-and-recorded with Josh, not yet applied by a repo admin. Tracked on OBJ-3390; no separate follow-up issue needed since the request itself is the tracked artifact. - (Phase 2,
play()coverage breadth) 379 of 429 story files define noplay()at all. Resolved as policy, not carried forward as a gap: render-only stories are legitimate smoke coverage under the semantic rule documented indocs/development/testing.md.
Related Artifacts
- Roadmap: v4.46-storybook-play-ci-enforcement-ROADMAP.md
- Failure inventory: v4.46-story-failure-inventory.md
- Milestone Audit: v4.46-storybook-play-ci-enforcement-MILESTONE-AUDIT.md
- Source issue: OBJ-2969 — filed by Maggie off Tess's finding while shipping OBJ-2919
- Coordination anchor: OBJ-2997 (parent milestone issue)
- Ruleset promotion request: OBJ-3390 (2026-09-05T07:46:12Z, ruleset
17927598)
Related Commits
3f9f99e2b— Phase 1: Harness + failure inventory (PR #2690)08cc1f70f— Phase 2 final wave:<ion-refresher>async-teardown race fix (PR #2836)205707be9— Phase 3:storybook-testrequired CI job (PR #2917)d5b5536eb— Phase 3: Storybookplay()CI enforcement docs (PR #2920)
Last updated: 2026-09-05