Playwright e2e suite covering the Room feature at the browser seam #191

Closed
opened 2026-09-01 13:28:07 +00:00 by root · 9 comments
root commented 2026-09-01 13:28:07 +00:00 (Migrated from gitlab.core-project.space)

Imported from GitHub amaurycoudr/chess-project#191, opened by @amaurycoudr on 2026-09-01 13:28 UTC.
Closed on GitHub on 2026-09-24 13:06 UTC as completed.


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

  1. As a developer about to merge the Room vertical into deep modules, I want a test suite that imports none of that vertical's modules, so that re-cutting them cannot invalidate my safety net.
  2. As a developer, I want the suite to live in a workspace package with no dependency on the front-end package, so that reaching into the implementation is structurally impossible rather than merely discouraged.
  3. As a developer, I want the suite to drive a real API with real SSE Connections, so that logic I move across the client/server seam is still checked end to end.
  4. As a developer, I want a green suite to mean "a Player can still play a game in a Room", so that I can refactor in confidence rather than by inspection.
  5. As a developer, I want the suite excluded from the pre-commit hook, so that committing a slice mid-refactor stays fast and I am never tempted to bypass the hooks.
  6. As a developer, I want the suite to block a pull request, so that a regression cannot merge even though it does not run on every commit.
  7. As a developer, I want each test to create its own Room, so that tests never interfere and the suite can run fully parallel.
  8. As a developer, I want no test-only procedure added to the API, so that production code has no branch that exists solely for tests.
  9. As a developer, I want the selector rules written down in the package, so that a later contributor cannot quietly reintroduce coupling to component structure.
  10. As a developer, I want failing tests to leave a trace artifact, so that a CI-only failure is diagnosable without reproducing it locally.
  11. As a developer, I want a test that flakes to be fixed or deleted within the week, so that the suite never trains the team to rerun until green.
  12. As a developer, I want the suite to run against the built bundle in CI, so that a pass tells me about the artifact a Player actually receives.
  13. As a developer, I want the suite's base URL derived from the worktree's assigned front-end port, so that running it in one worktree never silently exercises another.

A Player creating and sharing a Room

  1. As a Player, I want to create a Room from the home page, so that I can start a game with a friend.
  2. As a Player, I want to land in my new Room already holding a Seat, so that I can move as soon as my opponent arrives.
  3. As a Player, I want the Room code shown prominently while a Seat is still free, so that I know what to send and that I am still waiting.
  4. As a Player, I want to copy an invite link in one click and be told it was copied, so that I can paste it to a friend without transcribing the Room code.
  5. As a Player, I want the create button to refuse a second press while it is working, so that I do not orphan a Room.

A Player joining a Room

  1. As a Player, I want opening an invite link to seat me automatically, so that joining costs me no steps.
  2. As a Player, I want to type a Room code my friend read aloud, so that I can join without a link.
  3. As a Player, I want a mistyped code to be corrected for ambiguous glyphs, so that hearing "oh" instead of "queue" does not cost me the game.
  4. As a Player, I want an unknown or expired code to tell me so and return me home, so that I am never stranded on a broken Room.
  5. As a Player, I want both Players to see the Seats filled once my opponent joins, so that we both know the game can start.

A Player playing a Game

  1. As a Player, I want to select a piece and see its legal targets, so that I can move without knowing the rules perfectly.
  2. As a Player, I want my move to appear on my own board immediately, so that the game feels responsive.
  3. As a Player, I want my move to appear on my opponent's board without either of us reloading, so that we can play in real time.
  4. As a Player, I want to be told whose turn it is in terms of me, so that I do not have to work out which Color I am.
  5. As a Player, I want an illegal or out-of-turn move refused and my board restored, so that the position I see is always the real one.
  6. As a Player, I want the last move highlighted on both boards, so that I can see what my opponent just did.
  7. As a Player, I want to be told when a King is in check, so that I do not miss it.
  8. As a Player, I want checkmate announced to both Players, so that we agree the Game is over.
  9. As a Player, I want the move list to show the Game in readable notation, so that I can review what has been played.
  10. As a Player, I want captured pieces shown, so that I can see the material balance.
  11. As a Player, I want my board oriented with my own Color at the bottom, so that I am not playing upside down.

A Player ending a Game

  1. As a Player, I want to resign behind a confirmation, so that I cannot concede by a stray click.
  2. As a Player, I want my resignation to take effect immediately and be named to both Players, so that neither of us is left guessing.
  3. As a Player, I want to offer a draw, so that I can end a dead position by agreement.
  4. As a Player, I want a draw offer to reach my opponent as a dialog they must answer, so that it cannot be missed.
  5. As a Player, I want to see that my own offer is waiting, so that I know it went out.
  6. As a Player, I want to withdraw my own offer, so that I can change my mind.
  7. As a Player, I want an accepted draw reflected for both Players as a draw by agreement, so that the record is right.
  8. As a Player, I want a declined offer announced to me, so that I know where I stand.
  9. As a Player, I want moving to withdraw my own live offer, so that playing on is not ambiguous.

A Player restarting

  1. As a Player, I want to offer a restart, so that we can play again in the same Room.
  2. As a Player, I want the offer to read "Play again" once the Game is over, so that the next step is obvious.
  3. As a Player, I want an accepted restart to swap our Colors, so that neither of us always plays White.
  4. As a Player, I want to keep my Seat and the Room code across a restart, so that we do not have to re-share anything.

A Player leaving and returning

  1. As a Player, I want reloading the page to return me to my Seat, so that a refresh does not demote me to a Spectator.
  2. As a Player, I want my Seat kept while I am briefly disconnected, so that a dropped connection is not a forfeit.
  3. As a Player, I want to be told my opponent is disconnected and reconnecting, so that I do not think they have abandoned the game.
  4. As a Player, I want to be told once my opponent has actually left, so that I stop waiting.
  5. As a Player, I want to leave a Room deliberately and free my Seat at once, so that someone else can take it.
  6. As a Player, I want a second tab of the same Room not to count as a disconnect when I close it, so that having two tabs open is harmless.

A Spectator

  1. As a Spectator, I want a third opener of a Room code to be able to watch, so that friends can follow the game.
  2. As a Spectator, I want to be told plainly that I am watching rather than playing, so that I am not confused by an inert board.
  3. As a Spectator, I want to see moves as they are played, so that following the game is worth doing.
  4. As a Spectator, I want the board oriented White at the bottom, so that the view is conventional when I have no Color.
  5. As a Spectator, I want no way to move or act, so that I cannot disturb the Game.
  6. As a Spectator, I want to see the Game's ending and the offer resolutions, so that I understand how it finished.
  7. As a Spectator, I want to be able to leave, so that I am not trapped on the page.

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:slow task. The Turbo pipeline already declares an uncached test:slow task that no package implements. The suite implements it. It stays out of test, 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:

  1. Create a Room, take the invite link from the first Player's browser, open it in a second, and see both Players observe two filled Seats.
  2. Play a Game to checkmate across two contexts, alternating moves, asserting after each that both boards agree and finally that both name the winner.
  3. Offer a draw, answer the resulting dialog in the opponent's context with accept, and see both Players shown a draw by agreement.
  4. Resign through its confirmation dialog and see both Players told who resigned.
  5. Offer a restart after a Game ends, accept it, and see the Colors swapped between the Seats while the Room code and both Seats persist.
  6. Open a Room in a third context and confirm it watches: it is told so, it sees moves arrive, its board is oriented White at the bottom, and it can only leave.
  7. Reload mid-Game and confirm the Seat is preserved through the stored Seat token rather than demoted to a Spectator.
  8. Join by typing a Room code on the home page, exercising the ambiguous-glyph normalisation.
  9. Open an unknown Room code and be told and returned home.
  10. Copy the invite link and see the confirmation.

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.

> _Imported from GitHub [amaurycoudr/chess-project#191](https://github.com/amaurycoudr/chess-project/issues/191), opened by [@amaurycoudr](https://github.com/amaurycoudr) on 2026-09-01 13:28 UTC._ > _Closed on GitHub on 2026-09-24 13:06 UTC as completed._ <!-- github-issue:191 --> --- ## 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 1. As a developer about to merge the Room vertical into deep modules, I want a test suite that imports none of that vertical's modules, so that re-cutting them cannot invalidate my safety net. 2. As a developer, I want the suite to live in a workspace package with no dependency on the front-end package, so that reaching into the implementation is structurally impossible rather than merely discouraged. 3. As a developer, I want the suite to drive a real API with real SSE Connections, so that logic I move across the client/server seam is still checked end to end. 4. As a developer, I want a green suite to mean "a Player can still play a game in a Room", so that I can refactor in confidence rather than by inspection. 5. As a developer, I want the suite excluded from the pre-commit hook, so that committing a slice mid-refactor stays fast and I am never tempted to bypass the hooks. 6. As a developer, I want the suite to block a pull request, so that a regression cannot merge even though it does not run on every commit. 7. As a developer, I want each test to create its own Room, so that tests never interfere and the suite can run fully parallel. 8. As a developer, I want no test-only procedure added to the API, so that production code has no branch that exists solely for tests. 9. As a developer, I want the selector rules written down in the package, so that a later contributor cannot quietly reintroduce coupling to component structure. 10. As a developer, I want failing tests to leave a trace artifact, so that a CI-only failure is diagnosable without reproducing it locally. 11. As a developer, I want a test that flakes to be fixed or deleted within the week, so that the suite never trains the team to rerun until green. 12. As a developer, I want the suite to run against the built bundle in CI, so that a pass tells me about the artifact a Player actually receives. 13. As a developer, I want the suite's base URL derived from the worktree's assigned front-end port, so that running it in one worktree never silently exercises another. ### A Player creating and sharing a Room 14. As a Player, I want to create a Room from the home page, so that I can start a game with a friend. 15. As a Player, I want to land in my new Room already holding a Seat, so that I can move as soon as my opponent arrives. 16. As a Player, I want the Room code shown prominently while a Seat is still free, so that I know what to send and that I am still waiting. 17. As a Player, I want to copy an invite link in one click and be told it was copied, so that I can paste it to a friend without transcribing the Room code. 18. As a Player, I want the create button to refuse a second press while it is working, so that I do not orphan a Room. ### A Player joining a Room 19. As a Player, I want opening an invite link to seat me automatically, so that joining costs me no steps. 20. As a Player, I want to type a Room code my friend read aloud, so that I can join without a link. 21. As a Player, I want a mistyped code to be corrected for ambiguous glyphs, so that hearing "oh" instead of "queue" does not cost me the game. 22. As a Player, I want an unknown or expired code to tell me so and return me home, so that I am never stranded on a broken Room. 23. As a Player, I want both Players to see the Seats filled once my opponent joins, so that we both know the game can start. ### A Player playing a Game 24. As a Player, I want to select a piece and see its legal targets, so that I can move without knowing the rules perfectly. 25. As a Player, I want my move to appear on my own board immediately, so that the game feels responsive. 26. As a Player, I want my move to appear on my opponent's board without either of us reloading, so that we can play in real time. 27. As a Player, I want to be told whose turn it is in terms of me, so that I do not have to work out which Color I am. 28. As a Player, I want an illegal or out-of-turn move refused and my board restored, so that the position I see is always the real one. 29. As a Player, I want the last move highlighted on both boards, so that I can see what my opponent just did. 30. As a Player, I want to be told when a King is in check, so that I do not miss it. 31. As a Player, I want checkmate announced to both Players, so that we agree the Game is over. 32. As a Player, I want the move list to show the Game in readable notation, so that I can review what has been played. 33. As a Player, I want captured pieces shown, so that I can see the material balance. 34. As a Player, I want my board oriented with my own Color at the bottom, so that I am not playing upside down. ### A Player ending a Game 35. As a Player, I want to resign behind a confirmation, so that I cannot concede by a stray click. 36. As a Player, I want my resignation to take effect immediately and be named to both Players, so that neither of us is left guessing. 37. As a Player, I want to offer a draw, so that I can end a dead position by agreement. 38. As a Player, I want a draw offer to reach my opponent as a dialog they must answer, so that it cannot be missed. 39. As a Player, I want to see that my own offer is waiting, so that I know it went out. 40. As a Player, I want to withdraw my own offer, so that I can change my mind. 41. As a Player, I want an accepted draw reflected for both Players as a draw by agreement, so that the record is right. 42. As a Player, I want a declined offer announced to me, so that I know where I stand. 43. As a Player, I want moving to withdraw my own live offer, so that playing on is not ambiguous. ### A Player restarting 44. As a Player, I want to offer a restart, so that we can play again in the same Room. 45. As a Player, I want the offer to read "Play again" once the Game is over, so that the next step is obvious. 46. As a Player, I want an accepted restart to swap our Colors, so that neither of us always plays White. 47. As a Player, I want to keep my Seat and the Room code across a restart, so that we do not have to re-share anything. ### A Player leaving and returning 48. As a Player, I want reloading the page to return me to my Seat, so that a refresh does not demote me to a Spectator. 49. As a Player, I want my Seat kept while I am briefly disconnected, so that a dropped connection is not a forfeit. 50. As a Player, I want to be told my opponent is disconnected and reconnecting, so that I do not think they have abandoned the game. 51. As a Player, I want to be told once my opponent has actually left, so that I stop waiting. 52. As a Player, I want to leave a Room deliberately and free my Seat at once, so that someone else can take it. 53. As a Player, I want a second tab of the same Room not to count as a disconnect when I close it, so that having two tabs open is harmless. ### A Spectator 54. As a Spectator, I want a third opener of a Room code to be able to watch, so that friends can follow the game. 55. As a Spectator, I want to be told plainly that I am watching rather than playing, so that I am not confused by an inert board. 56. As a Spectator, I want to see moves as they are played, so that following the game is worth doing. 57. As a Spectator, I want the board oriented White at the bottom, so that the view is conventional when I have no Color. 58. As a Spectator, I want no way to move or act, so that I cannot disturb the Game. 59. As a Spectator, I want to see the Game's ending and the offer resolutions, so that I understand how it finished. 60. As a Spectator, I want to be able to leave, so that I am not trapped on the page. ## 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:slow` task.** The Turbo pipeline already declares an uncached `test:slow` task that no package implements. The suite implements it. It stays out of `test`, 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: 1. Create a Room, take the invite link from the first Player's browser, open it in a second, and see both Players observe two filled Seats. 2. Play a Game to checkmate across two contexts, alternating moves, asserting after each that both boards agree and finally that both name the winner. 3. Offer a draw, answer the resulting dialog in the opponent's context with accept, and see both Players shown a draw by agreement. 4. Resign through its confirmation dialog and see both Players told who resigned. 5. Offer a restart after a Game ends, accept it, and see the Colors swapped between the Seats while the Room code and both Seats persist. 6. Open a Room in a third context and confirm it watches: it is told so, it sees moves arrive, its board is oriented White at the bottom, and it can only leave. 7. Reload mid-Game and confirm the Seat is preserved through the stored Seat token rather than demoted to a Spectator. 8. Join by typing a Room code on the home page, exercising the ambiguous-glyph normalisation. 9. Open an unknown Room code and be told and returned home. 10. Copy the invite link and see the confirmation. 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.
root (Migrated from gitlab.core-project.space) closed this issue 2026-09-24 13:06:26 +00:00
root commented 2026-10-13 08:39:08 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #192

mentioned in issue #192
root commented 2026-10-13 08:39:08 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #193

mentioned in issue #193
root commented 2026-10-13 08:39:08 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #194

mentioned in issue #194
root commented 2026-10-13 08:39:09 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #195

mentioned in issue #195
root commented 2026-10-13 08:39:09 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #198

mentioned in issue #198
root commented 2026-10-13 08:39:09 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #196

mentioned in issue #196
root commented 2026-10-13 08:39:09 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #197

mentioned in issue #197
root commented 2026-10-13 08:39:09 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #199

mentioned in issue #199
root commented 2026-10-13 08:39:09 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #204

mentioned in issue #204
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amaurycoudr/chess-project#191
No description provided.