Frontend Testing
Grounded in flowbase/apps/frontend/e2e — ~200 Playwright specs across 16 projects, retries: 3.
Assessed 2026-08-24 and 2026-09-08 · A1 completed 2026-08-25 · MISSION.md · RESOURCES.md
Renamed 2026-09-08. This path was concurrency/ — same mission, narrower name. The 2026-09-08 session widened scope to determinism, test-level allocation and the canvas editor, and nearly created a duplicate path before the overlap was caught. A1–A4, the original assessment and all suite findings carry forward unchanged.
Knowledge graph
Green = mastered. Amber = inner fringe (just learnt, still fragile — A1). Blue = outer fringe / zone of proximal development, lessons written and ready (six of them: A2–A4, C1–C3). Grey = locked. Click a node to open its lesson.
graph TD A[Async/await mechanics]:::got B[Playwright auto-waiting
web-first assertions]:::got C[Locator strategy]:::got D[Worker model:
process per worker]:::got E[Flake taxonomy:
async wait / concurrency / TOD]:::inner F[Symptom-first diagnosis]:::inner G[Retry policy as signal]:::inner H[The isolation ladder]:::zpd I[Fixture scope as a
sharing decision]:::zpd J[Collision surface audit:
what is actually shared]:::zpd K[Per-tenant test isolation]:::locked L[Serialisation primitives:
serial / mutex / lease]:::locked M[Cleanup contracts &
failure-safe teardown]:::locked N[Convergent assertions over
shared collections]:::locked O[Flake budgets & quarantine]:::locked P[Actionability: the five checks]:::zpd Q[Determinism inventory:
time / network / locale]:::zpd R[Size vs scope:
what belongs in e2e/]:::zpd S[Flakiness score:
entropy + flip rate]:::locked T[Trace-driven diagnosis]:::locked U[Network stubbing &
fault injection]:::locked V[Canvas behaviour testing
without pixels]:::locked W[Sharding & CI economics]:::locked A --> B --> C B --> E D --> J E --> F --> G E --> H E --> J D --> I H --> I H --> J H --> K H --> L J --> M J --> N G --> O B --> P P --> T P --> V E --> Q Q --> U U --> V R --> U R --> W G --> S O --> S J --> W click E "lessons/a1-naming-the-flake.html" "A1" click F "lessons/a1-naming-the-flake.html" "A1" click G "lessons/a1-naming-the-flake.html" "A1" click H "lessons/a2-the-isolation-ladder.html" "A2" click J "lessons/a3-collision-surface-audit.html" "A3" click I "lessons/a4-fixture-scope.html" "A4" click P "lessons/c1-the-five-checks.html" "C1" click Q "lessons/c2-the-determinism-inventory.html" "C2" click R "lessons/c3-size-and-scope.html" "C3" classDef got fill:#dcfce7,stroke:#16a34a,color:#14532d classDef inner fill:#fed7aa,stroke:#ea580c,color:#7c2d12,stroke-width:2px classDef zpd fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:3px classDef locked fill:#f5f5f4,stroke:#a8a29e,color:#78716c
Assessment · 2026-08-24
| Probe | Result |
|---|---|
| Q1 · slow vs. racy | Partial. Defined racy purely as inter-test contention. Missing: a solo test can race the application's own asynchrony. Conflates the ~45% category with the ~20% one. |
| Q2 · worker collision surface | Partial. Strong on the system under test. Correct instinct that separate processes share little memory. Missed the on-disk and cross-cutting surfaces: storageState, seed artefacts, ports, output dirs. |
Q3 · await expect placement | Correct and precise. Named the polling distinction unprompted. Treated as mastered. |
| Q4 · retry legitimacy | Healthy instinct, absolutist. "Never acceptable" is right about retry-as-fix, misses retry-as-instrument (counted, budgeted, ratcheted). Refined in A1 §6. |
| Q5 · isolation strategies | Blank — the clearest gap. Produced two of six-ish rungs and no cost analysis. This defines the ZPD. |
Assessment · 2026-09-08
Five single-part probes after the scope widened. Answers were terse and honest; three clean blanks, which is what defined the C-block.
| Probe | Result |
|---|---|
| Q1 · which actionability check fails under an overlay | Blank. The check list is not held at all — which means every timeout is currently read as an undifferentiated timeout. Foundational, and independent of A2–A4, so it became C1. (Also: I said six checks; there are five.) |
| Q2 · name for a test that fails only in the suite | Mechanism right, vocabulary missing. Named order dependency via shared mutable state, and load, unprompted — A1 landed. Did not produce victim, and the polluter/cleaner triple is not yet available. Cheap to close inside A2/A3 rather than needing its own lesson. |
| Q3 · how a 9th worker reddens a correct test | Correct. "Resource contention, e.g. lower CPU." Treat as mastered. Escalation path is the literature term — resource-affected flakes, Parry et al. TSE 2024 — and the config consequence, not the mechanism. |
Q4 · how refetchOnWindowFocus flakes a spec | Blank. The TanStack cache is not yet modelled as a nondeterminism source. Notable because the suite already has app-idle absorbing the symptom, so the gap is invisible in day-to-day work. Became C2. |
Q5 · criterion for keeping a behaviour out of e2e/ | Blank — the most expensive one. No level-allocation model, which is consistent with a suite that has exactly one level. This is the only lever that moves flake rate and CI wall-clock together. Became C3. |
Suite findings feeding the path
- Zero
waitForTimeoutacross 233 specs; tightactionTimeout/expect.timeout. Async Wait discipline is already strong — lessons should not spend time there. - One user, one company, one
storageState, shared by all eleven projects. Codebase comment: "shared, JWT-bound company — there is no per-test company." - Guard fixtures (
companyNameGuard) and a documented virtualised-list seeding bug: existing scar tissue from shared-tenant design. retries: 3(four attempts) with no flake budget in evidence.fullyParallelunset → parallel across files, serial within. Relevant to A2.
Read on 2026-09-08, after the scope widened:
- Zero
page.clock, fourpage.route, zero screenshots. Time and network are entirely uncontrolled — not by decision, by default. The two largest unpulled determinism levers. (C2.) localeandtimezoneIdunset inplaywright.config.ts. EveryIntl-derived assertion — money, dates, i18n — silently asserts the runner's ICU defaults. Two-line fix, latent rather than currently biting. (C2 §2.)- Twenty narrow-scope specs at large size.
e2e/tests/forms/holds single components responsible while paying for a DB reset, auth setup, Vite build and real HTTP. Google's data says flakiness tracks size and dependency count, so this is the structural floor under the flake rate. (C3 §2.) - The medium tier is empty. No level runs real components with real browser events and no backend.
route.fulfillagainst the existing app is the cheapest way to create one — no new tooling. (C3 §5.) app-idleguards the boundary, not the interior. It holds a test open until the app is quiescent at teardown, so it cannot see a refetch that starts and lands between two assertions within a test. Correct as built; commonly mistaken for a fix for intra-test races. (C2 §4.)- Flake rate is measured in Sentry (per Martin), but no CI → Sentry test-result pipeline is visible in the repo — only product-error wiring. Per-spec scoring needs outcome history;
playwright-report/results.jsonis the obvious feed. Unresolved. (Blocks the D-block.) pressanddispatchEventrun no actionability checks. ThepressHotkeyhelper therefore inherits no auto-waiting, which is exactly whyactivateToolmust wait ondata-active-tool. The convention is right and its reason is undocumented. (C1 §3.)
Lessons
Taxonomy, glossary, symptom-first diagnosis, retry policy. Ends with your suite classified.
Covers: E · F · G
Seven rungs from share-everything to a tenant per test, priced. A rung per resource, not per suite. Places your six main resources on it.
Covers: H · closes the Q5 blank
Four questions, seven layers. The procedure that catches the non-database surfaces — auth artefacts, filesystem, ports, background jobs. Applied to this repo.
Covers: J · closes the Q2 gap
Run / worker / test / ambient scope as blast radius. Scope contagion, the three teardown failures, and your suite's scope map.
Covers: I
Playwright's actionability checks, which action runs which, and the row that explains why hotkey helpers must own their waits. Ends with a force: audit of your repo.
Covers: P · closes the Q1 blank · prerequisite for T and V
The five environmental inputs to a test result. TanStack's defaults read as a hazard list, why app-idle absorbs rather than fixes, and the trap in page.clock for gesture specs.
Covers: Q · closes the Q4 blank
e2e/Size vs scope as independent axes, the five-clause criterion for an e2e spec, Khorikov's four pillars as the honest counter-argument, and the empty medium tier priced three ways.
Covers: R · closes the Q5 blank · unlocks the team-standards work
K, L, M, N, O. Pick which after A2–A4.
S, T, U, V, W. S additionally blocked on resolving where per-spec outcome history lives.
C1 → A2 → C3 → C2 → A3 → A4. C1 first because it is 15 minutes and every later diagnosis depends on it. A2 second because the isolation ladder was already the biggest single win before today and still is. C3 third because it is the only lever that moves flake rate and CI time together, and because the team-standards goal blocks on it.
Findings surfaced while writing A2–A4
Read from the suite; each is used as a worked example in the lesson noted.
- Slot partitioning is unpinned.
findSeededProductresolves a worker's slot withitems[index % items.length]over a query with no explicit sort. Rung 3 in appearance, rung 0 in effect. (A2 §5, hunt A — cheapest real fix available.) seededProductis scope contagion. Declared test-scoped, resolves the same row for every test in the worker and again on retry. Safe today only by the convention that all consumers treat it as read-only. (A4 §2, §7.)authedRequestdisposes outsidefinally. A throwing test leaks the request context for the life of the worker. Low blast radius, two-line fix. (A4 §5, hunt E.)- Identity is a separate hot surface from data. One
DEFAULT_USERbehind every worker's JWT — any spec that changes its password, role or language fails the whole run at once. Highest-risk directory in the repo. (A3 §7.) - Renders are an unlisted concurrent writer.
globalSetuppoints the backend at the preview server, so a server-side headless browser works against the same shared company as the tests. (A3 §7, layer 7.)
Teaching preferences
- Answers are terse and technically literal — pitch at that register, no hand-holding prose.
- Says "don't know" cleanly rather than bluffing. Take blanks at face value; they mark real edges.
- Learning is in service of a live broken suite. Every lesson must end in something applicable this week.
- TypeScript for all code examples.
- One question at a time. No compound questions — never bundle two asks behind one question mark, in calibration or in exercises. Added 2026-09-08 at his request.
- Corrections are welcome and should be stated plainly rather than hedged (e.g. five actionability checks, not six).