Playwright e2e suite covering the Room feature at the browser seam #191
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
spec
wayfinder:grilling
wayfinder:map
wayfinder:prototype
wayfinder:research
wayfinder:task
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
amaurycoudr/chess-project#191
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem Statement
The Room feature works, but its code is hard to hold in your head. The front-end Room vertical is spread across roughly forty small modules — seven hooks, ten files in its API layer, and a scatter of paired text/derivation modules — none of them large, and no one of them the whole story. Following a single behaviour, such as "what happens when my opponent accepts a draw", means opening eight files. This is the shallow-module failure from Zakirullin's cognitive-load notes: not a god object, but too many modules too thin to be worth their interface.
The intended cure is to merge that fragmentation into a few deep modules. The obstacle is that the Room feature's existing coverage sits at exactly the altitude the merge destroys. Every front-end Room test mounts the vertical's components directly and stubs the oRPC boundary, so it imports the very modules that are going to stop existing. Refactoring would mean deleting the safety net as a first step and re-deriving it afterwards, with no way to tell a behaviour regression from an intended re-cut.
There is no test anywhere that a Player would recognise. Nothing drives two real browsers through a Room and checks that both see the same Game.
Solution
A permanent end-to-end suite, in its own workspace package, that drives real browsers against the real running stack — front-end and API, real Seat tokens in browser storage, real SSE Connections — and binds only to what a Player can see: the URL, the accessible roles and names in the DOM, and the rendered text.
Because it knows nothing about the Room vertical's internals, it survives any re-cut of them. That gives the refactor a green-to-green target: merge the forty modules into a few deep ones, keep the suite green, and know the Room still works without having to trust a reading of the diff.
The suite also encodes the constraint that makes the refactor safe to attempt at all: the UI contract is frozen. Behaviour visible to a Player does not change while the internals move.
User Stories
The developer refactoring the Room
A Player creating and sharing a Room
A Player joining a Room
A Player playing a Game
A Player ending a Game
A Player restarting
A Player leaving and returning
A Spectator
Implementation Decisions
A new workspace package for the suite. The suite lives in its own package,
@chess/e2e, under the apps directory. It declares no dependency on the front-end package. This is the primary mechanism enforcing the seam: the modules under refactor are not importable, so no future contributor or agent can quietly couple a test to them. Its own coding standards file records the selector law below.Playwright, greenfield. No browser-driving tool exists in the repo today; nothing is being migrated. Chromium only. Cross-engine rendering is a different risk from the one this suite addresses, and one the refactor cannot introduce.
Playwright MCP is an authoring tool, not part of the suite. The Playwright MCP server is used while writing the tests — driving the real running app, reading its accessibility tree, and confirming a flow before it is committed to a spec file — because the locators it reports are Playwright-idiomatic and map directly onto the selector law above. It is configured at the project level so any contributor gets it, and pinned rather than floating. It is deliberately not a dependency of the suite: the committed tests are ordinary Playwright test files that run under the Playwright runner with no MCP server involved, in CI and locally alike. An interactive MCP session is never the oracle for a behaviour; it is how a test is discovered, after which the test must stand on its own.
Gate placement via the existing
test:slowtask. The Turbo pipeline already declares an uncachedtest:slowtask that no package implements. The suite implements it. It stays out oftest, and therefore out of the pre-commit hook, which runs typecheck and test on every commit; a browser run there would make committing refactor slices painful enough to invite hook bypass, which the repo forbids for agents outright. It gains a separate CI job on pull requests, which requires a Chromium install step the current single-job workflow does not have. The five-command gate is unchanged locally.Commitlint scope. The allowed commit scopes must gain
e2e. (They presently cover the engine, front, repo, ci and deps, which is also why the API package has no scope of its own.)Real stack, two run modes. Tests run against the real front-end and the real API over real SSE. In CI the servers serve built artifacts, so a pass speaks about the bundle a Player receives; locally they are the dev servers, for speed. The mode is chosen by environment variable. Playwright's own web-server support starts both.
Port derivation is mandatory, not cosmetic. Dev ports are assigned per worktree in blocks, and the API's configuration loader throws when its port is absent. The Playwright configuration therefore loads the worktree's environment (or runs the repo's port-sync script) and derives its base URL from the assigned front-end port. A hardcoded port would silently drive another worktree's servers. The front-end dev server proxies the RPC path to the API, so the browser stays same-origin and the API's Origin allowlist is satisfied without a CORS story.
Isolation by Room, not by reset. Room state is a process-local in-memory registry keyed by Room code; there is no database to truncate. Each test creates its own Room through the user interface, and the minted six-glyph code isolates it. The limits that could couple tests — the Spectator cap and the idle eviction window — are per-Room. Consequently the suite runs fully parallel against a single shared API process, and no test-only reset procedure is added to the API. Setup goes through the same seam as assertion: a test clicks "Create a game" rather than calling the create procedure, so fixtures cannot become a second thing the refactor breaks.
Multiple browser contexts model multiple Players. Each Player is a separate browser context, which gives each its own storage and therefore its own Seat token for free — the identity model requires nothing more. Two contexts for a Game, a third for the Spectator test. A shared fixture provides a seated pair in a fresh Room and the helper that waits for an SSE-driven update to land in the other context.
The selector law. In order of preference: role plus accessible name; then the domain-anchored test hooks where no accessible name exists; and never the layout-anchored ones. The DOM is already unusually amenable to the first: each square is a real button named for its algebraic square and its occupant or emptiness, every action button and dialog carries an accessible name, and the status line is a live region. The domain-anchored hooks that remain permitted identify the board and its orientation, each square by algebraic name, the move list, the captured trays, and the per-square state attributes for selection, legal target, last move and check. The banned hooks are the ones naming the Room's present composition — the rail and its four sub-regions, and the room layout wrapper. Those name today's structure, so asserting on them would constrain the very refactor this suite exists to enable. No new test hooks are added to the front-end package; the suite is built from what is already there.
Position assertion is targeted, not exhaustive. Nothing in the DOM exposes a machine-readable position — no FEN, no move line, no hidden node — and none is added: production code whose only consumer is a test is exactly the kind of speculative abstraction the standards forbid. A test asserts the squares its behaviour concerns, plus the status line text and the move-list notation. Reconstructing all sixty-four squares would produce unreadable tests and failures that diagnose nothing. Checkmate needs no position read at all: the status line names the winner and the mated King's square carries its check attribute.
Single viewport. Desktop only. The Room's action controls hide their labels visually above a breakpoint but keep them in the accessible name, so name-based queries behave identically at both sizes; a second full pass would re-test the same logic at double the cost and double the flake surface. Breakpoint behaviour stays the responsibility of the component stories, which already cover both sizes.
Clock control for the one timed behaviour. The flip from "opponent reconnecting" to "opponent has left" is a client-side timer, so the browser's clock can be advanced to reach it. This is the only hook below the DOM in the whole suite and is confined to one test. If it proves brittle it is deleted rather than replaced with a hook in production code. The Room's own idle-eviction window is server-side and lazy, so it is unreachable this way and is not attempted.
Two known DOM traps, recorded so they are not rediscovered as flakes. Toasts render as named dialogs with heading titles, not as alerts or status regions: they are invisible to alert and status queries, and their headings can collide with a heading query for the prominently displayed Room code. And the status line is the only status region on a screen only until a toast appears.
Failure policy in configuration. No retries, a per-test timeout, and a refusal to run a focused subset. No wall-clock ceiling on the job is set until the real duration has been measured: there is no database to seed and the transport is loopback, so the build is expected to dominate, and a budget invented before measurement would cut tests for no reason. Traces are kept on failure.
Testing Decisions
What makes a good test here. A test in this suite may reference only what a Player can perceive: the URL, roles and accessible names, rendered text, and the domain-anchored hooks named above. It may not import from the front-end package, name a component, or assert on the Room's present composition. Its assertions are behavioural — "both Players see that Black resigned" — never structural. Where a behaviour spans two Players it must be asserted in both contexts, since a one-sided assertion would pass against a broken fan-out.
What is tested. Ten scenarios, spanning the whole Room feature at the browser seam:
An eleventh, conditional on the clock control proving stable, advances the browser clock to see the opponent-disconnected message become opponent-left.
Prior art. The API package's integration tests are the closest existing model: they drive a real client against the real server in process, with a shared connection fixture, and are the reference for how a full-stack test is structured and named in this repo. The front-end's single router-level test is prior art for exercising routes and providers together rather than a component in isolation. The shared per-vertical fixture modules are the model for where the suite's own fixtures live and how they are named. The repo's testing standards apply unchanged: behaviour-describing test names, table-driven cases where the shape repeats, unhappy paths covered, and comments only for facts not derivable from the code.
Delivery order. The harness and the first four scenarios — create and join, a Game to checkmate, resignation, and reload preserving the Seat — are built and proven green first, then reviewed before the remaining six. The risk in this work is concentrated in the harness: starting two servers, deriving the port, and waiting correctly for an SSE-driven update to reach a second context. Four scenarios exercise all of it. Discovering a harness fault across ten scenarios would cost ten rewrites. This also matches the repo's vertical-slice rule: one test green, then the next.
Out of Scope
The refactor itself. This spec delivers only the suite. No module in the Room vertical is touched. The merge into deep modules, and the diagnosis of which modules to merge, are separate work that this suite exists to make safe.
Any change to the UI. The suite documents current behaviour. For it to be a valid oracle, the refactor must be behaviour-preserving at the browser seam, so the UI contract is frozen for its duration; intended user-visible changes belong in later work, after the internals have settled.
New test hooks in the front-end. Nothing is added to the application to make it more testable — no attributes, no machine-readable position, no test-only procedure in the API.
Three scenarios deliberately not covered, each for a reason that is not cost. The Spectator cap is redundant: the client handles reaching it through the same message-and-redirect path as an unknown Room code, which is covered, so testing it would need many contexts to re-prove one path. The race between withdrawing an offer and accepting it is nondeterministic by design — the client deliberately swallows the loser's refusal — so there is nothing stable to assert. The Room's idle eviction is server-side and lazily evaluated, so no browser-side clock control reaches it.
Playwright MCP in the delivered artifact. No committed test may require an MCP server, and the suite's package must not depend on one. MCP-driven browsing is nondeterministic and unavailable in CI; treating it as a test would produce a suite that cannot run unattended.
Cross-browser and multi-viewport coverage. Chromium and desktop only, for the reasons above.
Removing the stale WebSocket environment variable. The front-end's local environment file still defines a WebSocket URL, dead since the transport became SSE. Unrelated scope, noted here so it is not lost.
Further Notes
The diagnosis behind this work is worth recording, because it inverts the usual one. The Room's problem is not a large component: its screen component is under ninety lines and no module in the vertical exceeds a hundred. The problem is that there are around forty of them, each too thin to be worth its own interface — the shallow-module smell rather than the god-object one. That matters because the two call for opposite cures: a god object wants splitting, and shallow modules want merging. A well-intentioned tidy-up that added more files would make the Room harder to hold in your head, not easier.
The corollary of a single seam this high is that a green suite tells you the Room still works and nothing about where a break is. That is the trade being bought deliberately: refactor-survivability in exchange for diagnostic precision. The existing per-vertical tests are what localise failures today, and most of them are expected not to survive the merge. Whether their replacements sit at the boundary of the new deep modules is a question for the refactor, not for this spec.
Relevant decision records: the front-end's vertical-slice layout; query-cache ownership of server state; the third opener spectating; the Seat token as the identity model; resignation and offers as play-layer concepts; Colors swapping between Seats on restart; and the Online Game as a cache entry nudged over SSE, which supersedes the earlier WebSocket design.
mentioned in issue #192
mentioned in issue #193
mentioned in issue #194
mentioned in issue #195
mentioned in issue #198
mentioned in issue #196
mentioned in issue #197
mentioned in issue #199
mentioned in issue #204