test(fouc): sample first paint from an init script, assert control preconditions - #169
Conversation
BackToTop is hidden by opacity-0 and carries transition-opacity, so on any frame where the stylesheet is absent it paints at full opacity and fades out once the sheet lands. That frame is produced by the React #423 hydration recovery (#126), which re-renders the head and briefly drops the stylesheet after the component has mounted — so gating the transition on mount, as #141 tried, cannot prevent it. Measured in WebKit with the stylesheet delayed: 17-18 animating frames without this rule, none with it. The FOUC guard asserts the rule: the main test expects opacity 0, the control expects 1 with the inline block stripped. Supersedes the Outline.tsx change in #141. Relates to #126. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…econditions The React #423 hydration recovery restores the inlined critical <style> 150-240ms after DOMContentLoaded (#126). The control strips that block from the served HTML, so a post-domcontentloaded sample can see it restored and every control assertion flips at once. Sample in-page on DOMContentLoaded instead, before hydration is scheduled, and have both cases assert their own preconditions so a reshaped CRITICAL_CSS reports as a stale strip pattern. Carries the test half of #141 onto the post-#144 probes. Relates to #126. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
🎭 Visual regression resultsDetails
Skipped testsmobile-chrome › theme.spec.ts › QuantEcon theme — visual regression › without JavaScript › drawer-opens-without-javascript |
There was a problem hiding this comment.
🔵 Needs a closer look
It changes the behavior of a PR-gating test whose correctness now hinges on an init-script-vs-hydration ordering the author explicitly describes as "empirical rather than guaranteed," a reliability judgment best confirmed by a human maintainer.
Pull request overview
This PR hardens the WebKit FOUC guard test (tests/visual/fouc.spec.ts) so it stops being susceptible to a latent race: React hydration fails on every page load (#126), and the #423 recovery re-render restores the inlined critical <style> ~150–240ms after DOMContentLoaded — which, in the control case that deliberately strips that block, would flip every assertion at once and turn the PR-gating fouc-guard job red for reasons unrelated to the critical CSS. It moves the measurement into an in-page init script that samples on DOMContentLoaded before hydration is scheduled, and adds precondition assertions so a reshaped CRITICAL_CSS fails as "stale strip pattern" rather than as a confusing layout assertion. No production/component code changes.
Changes:
firstPaintStatenow installs apage.addInitScriptsampler that measures the DOM onDOMContentLoaded(parking the result onwindow.__qeFirstPaint), replacing the post-domcontentloadedpage.evaluatemeasurement.isolateInlineCssreturns astrippedCriticalprobe, and both tests assert their own preconditions (strippedCritical,criticalInline) so failures name their real cause.- CHANGELOG entry added under
## [Unreleased]>### Fixeddocumenting the desensitisation and linking #126/#141.
File summaries
| File | Description |
|---|---|
tests/visual/fouc.spec.ts |
Replaces post-DOMContentLoaded sampling with an init-script sampler; adds strip/inline preconditions to both cases via a returned probe and a criticalInline field. |
CHANGELOG.md |
Documents the FOUC-guard hardening under [Unreleased] > Fixed with links to the related issue/PR. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 0
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…cal-css # Conflicts: # CHANGELOG.md
…t-script # Conflicts: # CHANGELOG.md # tests/visual/fouc.spec.ts
|
FYI @DrDrij, since you have been working in this area: this hardens the WebKit FOUC guard so its control case samples the page from an init script on |
Carries the test-hardening half of #141 onto the current
fouc.spec.ts(post-#144, post-#168). No component changes.The control is racy
Hydration fails on every page load (React
#418/#423, #126), and the#423recovery re-renders the tree, which puts the inlined critical<style>back. Re-measured on currentmainwhile reviewing #141: the block reappears 150–240ms afterDOMContentLoaded, and at that moment.qe-toc-toggle__closealso flips to hidden. The control strips that block from the served HTML precisely to prove the guard is meaningful, so when React restores it, every control assertion flips at once. The measurement ran from the test afterdomcontentloaded, leaving roughly a 150ms budget.fouc-guardhas never gone red for this onmain(25+ runs), so this is latent rather than observed, but it is a PR-gating job that would fail for reasons unrelated to the critical CSS.What changes
DOMContentLoaded, before hydration is even scheduled. Chosen over "bail early" because a control that strips the inline block guarantees a hydration mismatch by construction, so no repair to React hydration fails on every page load (#418/#423), which makes the FOUC control test racy #126 could ever make a post-hydration sample safe here. The ordering is empirical rather than guaranteed (Remix v1's inlinetype="module" asyncentry has to resolve its imports beforeentry.client.tsxscheduleshydrateRoot), but the earliest observed restoration is an order of magnitude later than the sample.CRITICAL_CSSfails as "stale strip pattern" rather than as "expected block, received grid". Each case also asserts whether the inline block is present as sampled.sidebarRendered,toggleCloseRendered) and fix: pin "back to top" to opacity 0 in the critical CSS #168 (backToTopOpacity) are carried into the sampler unchanged.Verification
The guard still guards: removing the grid rule from the built
CRITICAL_CSSfails the main test withExpected: "grid" / Received: "block"while the control still passes, so the hardening desensitises the test to hydration without desensitising it to regressions.npm run compileclean ·test:fouc2 passed.Stacked on #168: this branch contains #168's commit, since both touch the same region of
fouc.spec.tsand the back-to-top assertion needs its rule. It targetsmaindirectly so CI runs (ci.ymlonly triggers for PRs based onmain), which means the diff shows both commits until #168 merges and then shrinks to this one. Merge #168 first. Relates to #126 (step 1, the hydration failure itself, stays open there) and #141 (superseded).🤖 Generated with Claude Code