How we get a Playwright suite where any spec can run alone, in parallel, and in any order — without tests stepping on each other's data.
Our app is already multi-tenant: two customers can never see each other's data. We reuse that exact wall for tests. Every Playwright worker (an OS process running spec files) provisions its own disposable company in the backend and does all its work inside it. Two workers can't collide for the same reason two customers can't.
Everything else — recipes, fixtures, sharding — is machinery to make this principle cheap and automatic.
Spec files declare a company profile (a seeding recipe). Playwright assigns files to workers from a queue; a worker only runs files matching its profile, and bakes one fresh company from that recipe for all the files it executes. Watch the provision counter — that's the money slide: companies scale with workers, not with spec files.
Deliberately visible in the simulation: files of the same color may end up split across two workers → two identical copies of that recipe's company exist. Specs may rely on what the recipe guarantees, never on “the same physical company” as a sibling file.
test.use({ companyProfile: 'measurements' }) does not mean “connect me to the measurements company”. There is no such single company. It means: run me inside some disposable company baked from the measurements recipe — and Playwright bakes as many identical copies as its scheduling needs.
Three guarantees follow, and they're the only things a spec may rely on:
m-007), never by ID.Every test answers a single question: “where does the data I touch come from?” There are exactly three legal answers — that's the entire discipline.
Pagination, sort, filter, search. The seed is shared furniture that nobody ever mutates, so any number of tests can assert on it — “60 rows, 3 pages” — in any order, forever.
Create / edit / delete tests manufacture their own uniquely-named entity via one API call, then drive the UI on it. Nobody else knows rename-me-a1b3 exists — mutate it freely. Retry-proof by construction: a retried test re-runs its factory line and gets a fresh entity.
Empty states, delete-all sweeps, exact unfiltered totals. The file takes a profile name nobody else uses → own worker → own company, untouched by structure, not by politeness. Rare by design.
What's forbidden falls out automatically: editing seeded m-007 (breaks answer 1 for everyone), and using another test's leftovers (breaks answer 2's retry story — the retry runs alone, its supplier doesn't).
| You want | You write |
|---|---|
| Run in whatever company the worker has | nothing (default profile) |
| “This file needs 60 measurements first” | test.use({ companyProfile: 'measurements' }) |
| “This file needs a parent business metric” | beforeAll(({ api }) => createBusinessMetric(api)) |
| “This test edits something” | create it inside the test via a factory |
| “This file needs a company nobody touched” | a profile name used by this file only |
Write every it as order-independent even though we keep fullyParallel: false (in-file order preserved): CI retries re-run a failed test alone in a fresh worker, so hidden test-to-test dependencies always break there first.
Selector knowledge is layered so that DOM changes hit exactly one file. The foundation is fragments: one driver class per shared UI component (cad-table, dropdown, tree, date-picker…) that hides the component's internal DOM and exposes an intent-level API — table.sortBy('Name'), dropdown.select('CO₂e'), tree.expand('Scope 1').
page.locator(…) in a spec is a review flag.
uses ↓
cad-* components get a data-testid pass-through so fragments attach to stable hooks.
The Storybook tests are the clever part — one suite, two guarantees:
baseURL = the built Storybook, runnable on component PRs).See the table.fragment.ts / measurements.page.ts / table.fragment.spec.ts tabs below for how the three layers look side by side.
All infrastructure lives in one fixture file; spec files stay one-liner-declarative. Snippets are editable — click into them during the demo.
Two multiplying levels: GitLab parallel: 4 gives four runner machines, each running one shard of the suite (--shard=$CI_NODE_INDEX/$CI_NODE_TOTAL); inside each shard, Playwright spawns its workers. The architecture needs zero changes — more workers simply bake more disposable companies.
4 shards × 4 workers = 16 tests running at once, 16 tenants, one backend.
nx serve build; pass dist/ as an artifact.e2e-* companies.merge-reports job produces one HTML report.The suite runs against a real backend — whose?
This single number decides how much isolation we can afford.
Maturity ladder, in order.