Skip to content

test: Upgrade Playwright to 1.63 and cover soft navigation web vitals - #24273

Draft
logaretm wants to merge 7 commits into
awad/inp-element-name-first-writefrom
awad/soft-nav-web-vitals-e2e
Draft

test: Upgrade Playwright to 1.63 and cover soft navigation web vitals#24273
logaretm wants to merge 7 commits into
awad/inp-element-name-first-writefrom
awad/soft-nav-web-vitals-e2e

Conversation

@logaretm

@logaretm logaretm commented Sep 9, 2026

Copy link
Copy Markdown
Member

Upgrades Playwright to 1.63 so the soft navigation web vitals path is testable at all, and adds an e2e test for it.

Three existing tests needed fixing for the newer browser:

  • INP: web-vitals defers entry processing to requestIdleCallback(..., { timeout: 1000 }), which Chromium 153 only runs on the timeout. Hiding the page after 500ms reported an unset metric. hidePage now waits for its own idle callback first.
  • CLS: the subject reloaded to finalize CLS, which raced the envelope and looped. CLS already lands on the pageload span at the idle timeout, so the reload is gone.
  • LCP: web-vitals finalizes on trusted input only, so the synthetic visibilitychange never did. It passed purely because reportAllChanges reported every entry; soft nav reporting turns that off. Now finalized with a real click.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 28.81 kB added added
@sentry/browser - with treeshaking flags 27.12 kB added added
@sentry/browser - with treeshaking flags tracing without tracing 27.01 kB added added
@sentry/browser (incl. Tracing) 50.21 kB added added
@sentry/browser (incl. Tracing + Span Streaming) 50.23 kB added added
@sentry/browser (incl. Tracing, Profiling) 53.2 kB added added
@sentry/browser (incl. Tracing, Replay) 89.7 kB added added
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 78.8 kB added added
@sentry/browser (incl. Tracing, Replay with Canvas) 94.4 kB added added
@sentry/browser (incl. Tracing, Replay, Feedback) 107.4 kB added added
@sentry/browser (incl. Feedback) 46.3 kB added added
@sentry/browser (incl. sendFeedback) 33.87 kB added added
@sentry/browser (incl. FeedbackAsync) 38.99 kB added added
@sentry/browser (incl. Metrics) 29.84 kB added added
@sentry/browser (incl. Logs) 30.1 kB added added
@sentry/browser (incl. Metrics & Logs) 30.78 kB added added
@sentry/react 30.57 kB added added
@sentry/react (incl. Tracing) 52.54 kB added added
@sentry/vue 36.07 kB added added
@sentry/vue (incl. Tracing) 52.45 kB added added
@sentry/svelte 28.85 kB added added
CDN Bundle 30.56 kB added added
CDN Bundle (incl. Tracing) 50.7 kB added added
CDN Bundle (incl. Logs, Metrics) 32.83 kB added added
CDN Bundle (incl. Tracing, Logs, Metrics) 52.66 kB added added
CDN Bundle (incl. Replay, Logs, Metrics) 73.53 kB added added
CDN Bundle (incl. Tracing, Replay) 88.24 kB added added
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 90.2 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback) 94.27 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 96.28 kB added added
CDN Bundle - uncompressed 90.47 kB added added
CDN Bundle (incl. Tracing) - uncompressed 151.38 kB added added
CDN Bundle (incl. Logs, Metrics) - uncompressed 97.04 kB added added
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 157.35 kB added added
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 226.45 kB added added
CDN Bundle (incl. Tracing, Replay) - uncompressed 270.95 kB added added
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 276.9 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 284.65 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 290.59 kB added added
@sentry/nextjs (client) 54.86 kB added added
@sentry/sveltekit (client) 50.65 kB added added
@sentry/core/server 36.99 kB added added
@sentry/core/browser 13.55 kB added added
@sentry/node 127.95 kB added added
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.61 kB added added
@sentry/node - without tracing 88.73 kB added added
@sentry/node - without channel injection 107.17 kB added added
@sentry/aws-serverless 97.12 kB added added
@sentry/cloudflare (withSentry) - minified 202.05 kB added added
@sentry/cloudflare (withSentry) 502.91 kB added added

View base workflow run

@logaretm
logaretm force-pushed the awad/soft-nav-web-vitals-e2e branch from 6b626d2 to 6bf5302 Compare September 9, 2026 22:59
@logaretm
logaretm force-pushed the awad/soft-nav-web-vitals-e2e branch from 6bf5302 to edec2c5 Compare September 9, 2026 23:18
@logaretm
logaretm added this pull request to stack #24274 September 9, 2026 23:18
…itals

Playwright 1.56 bundles Chromium 141, which has no Soft Navigations API, so
`supportsSoftNavigations()` is false and the soft navigation web vitals path never
runs under test. 1.63 bundles Chromium 153, which supports it unflagged.

Adds an e2e test asserting that a soft navigation's web vitals are attributed to the
navigation span they were measured on, through `reactRouterV6BrowserTracingIntegration`,
which starts its navigation span from a layout effect rather than from the history change.

The INP test needed a longer wait on the newer Chromium. web-vitals defers processing an
interaction's event entries to `requestIdleCallback(..., { timeout: 1000 })`; Chromium 141
ran that well inside the test's 500ms wait, Chromium 153 only runs it on the timeout. Hiding
the page before then forced a report while the metric was still unset, so no INP was emitted.
…e hiding the page

web-vitals defers processing an interaction's event entries into
`requestIdleCallback(..., { timeout: 1000 })`. Chromium 141 ran that well before the tests
hid the page; Chromium 153 only runs it once the timeout elapses, so hiding first forced a
report while the metric was still unset and no vital was emitted at all.

`hidePage` now waits for an idle callback of its own first. Idle callbacks run in scheduling
order, so one queued at hide time running means web-vitals' earlier callback has too.
CLS lands on the pageload span when it ends on the idle timeout, so nothing has to force the
page away to finalize it. The reload raced the envelope instead: on Chromium 153 the page
reloads before the in-flight send leaves, so no transaction was ever received and the test
timed out. It also looped, since the reloaded page re-ran the same subject.
web-vitals finalizes LCP on the first trusted input or visibility change and checks
`isTrusted`, so the synthetic `visibilitychange` this test dispatched never finalized it. It
passed anyway while `reportAllChanges` was on, since every LCP entry was reported as it
arrived. Soft navigation reporting turns that off, so on a browser that supports the Soft
Navigations API no LCP span was emitted at all. Clicking finalizes it the way a real user does.
`page.route` resolves with a `Disposable` as of Playwright 1.63, so returning it from a
`Promise<void>` helper no longer type-checks.

The INP tests in react-17, react-17-static and the react-router 7/8 SPA apps hid the page
500ms after the interaction, which is the same race already fixed in react-router-6:
web-vitals defers entry processing to `requestIdleCallback(..., { timeout: 1000 })` and
Chromium 153 only runs it on the timeout, so the forced report found an unset metric.

solid-tanstack-router's LCP test dispatched a synthetic `visibilitychange`, which web-vitals
ignores for finalization because it checks `isTrusted`. It passed only while
`reportAllChanges` was on. It now finalizes with a real click, as in the browser integration
suite.
Playwright 1.63 ships a WebKit that reports `deliveryType`, so the attribute is no longer
Chromium-only and the expectation moves out of the non-WebKit branch. That WebKit also
resolves the intercepted routes without measurable delay, so the TTFB range check keeps its
upper bound (which is what verifies we report seconds, not milliseconds) but only holds the
other engines to a non-zero value.
Two assumptions in the bfcache app stopped holding on newer Chromium, and both traced back to
`unload`.

An unload listener still makes a page ineligible, but Chrome no longer hands out the
`unload-listener` reason for it, only the privacy-masked one. The test waited for both, so it
hung on a reason that never arrives. It now asserts the masked reason, which is what the
browser actually reports and which older versions reported too.

The child-frame test used an unload listener inside the iframe purely as a blocker, so it lost
its reason as well. It blocks with a held-up IndexedDB version upgrade instead, mirroring the
top-level botcher, which keeps the child-frame classification covered.
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