Your Playwright component tests intermittently render a stale build of a design-system toast and the story page registers a service worker; how do you make the runs deterministic?
answer
- The harness page is a real origin
- Re-mounting reuses the same worker
- Prove which build actually loaded
- Pin it in config, not per test
basics
~20 sStop the story page from registering one. Setting serviceWorkers to 'block' in the config's use block means no worker installs for the run, so every mount loads the bundle you just built instead of a cached copy.
solid answer
~50 sFirst confirm the diagnosis rather than assuming it: run the failing test headed or open its trace, check what the page loaded, and see whether `navigator.serviceWorker.controller` is set on the story page. Also rule out the cause with the identical symptom — a `baseURL` aimed at a dev server nobody rebuilt. Once a worker is confirmed, the fix belongs in config: set `serviceWorkers: 'block'` under `use`, so no worker registers for the contexts the run creates and every mount fetches the freshly built bundle. Blocking beats unregistering in the test, which runs too late for mounts that already happened and has to be repeated in every file. Note why re-mounting never helped: a new mount reuses the same origin and therefore the same worker, so the cached bundle survives it. This is environment pinning, not a component bug.
code
typescript · 8 linesimport { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: 'http://localhost:5173',
serviceWorkers: 'block',
},
});go deeper
Take away one fact: the story page is a real page on a real origin, so browser caching applies to it. If a component renders an old version, suspect the environment before rewriting the test.
Know the option and where it goes: serviceWorkers set to 'block' in the config's use block stops workers registering for the run. Be able to say why a second mount does not fix a cached bundle.
Show the diagnosis, not just the fix. Prove which build loaded, check whether a worker controls the origin, rule out a stale baseURL, and only then change config — and explain why blocking beats unregistering.
Treat it as environment policy. Decide which browser-level variables a component run pins by default, and make an unpinned or stale harness fail loudly rather than surface as intermittently failing component tests.
## Why a component run can go stale at all It is tempting to think a component test is hermetic — it renders one component, so what could be cached? But a Playwright component test is a real browser on a real origin: the runner navigates to the story page served under `baseURL` and calls that page's `window.mount`. Everything a browser normally does on that origin still happens, including service worker registration and its cache. Once the harness registers a worker, that worker sits in front of the origin's requests. Subsequent mounts get the worker's cached copy of the story bundle rather than the file the dev server just rebuilt, and nothing in the test says so. The symptom is the worst kind: assertions failing against behaviour the source clearly implements, intermittently, and never on the first ever run. ## Confirm the diagnosis before changing config 1. Establish which build the page is actually running — put a build stamp or version string in the story page and assert or log it, so "stale" stops being a hypothesis. 2. Run the failing test headed or open its trace and look at what the page loaded and where it loaded from. 3. Check whether a worker is in charge of the origin at all: `navigator.serviceWorker.controller` being non-null on the story page settles it. 4. Rule out the simpler cause with the identical symptom: a `baseURL` pointing at a dev server nobody rebuilt, or at a stale preview build. Note what does **not** help, and why proving that is useful: mounting again, calling `update`, or starting a new test all reuse the same origin and therefore the same worker, so the stale bundle survives every one of them. ## The fix Set `serviceWorkers` to `'block'` in the config's `use` block. With it in place no service worker registers for the contexts the run creates, so the page always fetches its assets from the server: - The story page loads the bundle your build just produced, on every mount, every run. - Nothing sits between the page and the network, so request behaviour during the test is the behaviour you configured. - The result no longer depends on the order tests ran in, or on whether some earlier run installed a worker. Blocking is preferable to unregistering after the fact: an unregister step runs too late for the mounts that already happened, and it has to be repeated everywhere a context is created. | Option | Effect on the story page | Fit for a component run | |---|---|---| | Default behaviour | the harness may register a worker | leaves caching in the test's path | | `serviceWorkers: 'block'` | no worker registers | deterministic assets on every mount | | Manual unregister in the test | worker is gone from the next mount on | too late, and repeated in every file | ## Hardening the rest of the run Blocking workers removes one variable; a component suite that must be trustworthy usually pins the others too. - Make `baseURL` point at a server the run itself started, so nobody tests against a leftover process. - Fail loudly when the story page is not the expected build, rather than letting assertions fail obscurely. - Keep the harness bundle small and rebuilt from source, so "did it rebuild" has a cheap answer. - Treat any intermittent failure that disappears after a hard reload as a caching hypothesis until disproved. ## Why this belongs to the mount model rather than to the component The lesson worth carrying out of the incident is about the shape of component testing itself. Because Playwright drives a page you host instead of rendering in-process, your component tests inherit the whole browser environment — origin, storage, network path, workers. That is what buys the fidelity: real CSS, real layout, real browser APIs. The price is that the environment must be pinned deliberately, and a toast that renders yesterday's markup is not a flaky component but an unpinned environment. Teams that configure the run once, at config level, stop meeting this class of failure entirely.
- Why does mounting the story again not clear the stale render?Because a new mount is not a new environment. It re-renders on the same origin in the same context, so the registered worker is still in charge of requests and still answers with its cached bundle. Only preventing registration, or removing the worker, changes what the page fetches.
- What would you check first to distinguish a caching problem from a genuine component bug?Which build the page is running. A version stamp on the story page, or the asset list in the trace, turns the question into a fact. If the loaded bundle predates the fix, it is an environment problem; if the current bundle really renders that markup, the component is at fault.
- Why block service workers rather than unregister them in a beforeEach?An unregister step is a race and a maintenance burden: it runs after the context exists, so mounts before it can still be served from cache, and every new spec file has to remember it. Blocking at config level applies to the whole run with nothing to forget.
saying these in an interview costs you the question
- Blames the component for what is a caching problem
- Thinks re-mounting clears a registered service worker
- Adds retries instead of removing the nondeterminism
- Unregisters workers per test instead of blocking them
- Never verifies which build the story page loaded