If the user played multiplayer earlier in the tab session, room-code,
room-token, and player-color persisted in sessionStorage. Clicking Play
Solo then:
1. navigate('/game') — no code param
2. GameRoute reads sessionStorage, finds stale creds → Case 1
canonicalises the URL to /game/<stale-code>
3. MultiplayerGameView mounts, opens a WS to a dead room, handshake
fails silently → blank white screen with a live URL like
/game/OSJBJY in the address bar.
Fix: handlePlaySolo explicitly wipes room-code, room-token, player-color,
layout-name, and modifier-profile-name before navigating. The solo path
then goes through GameRoute's Case 2 (no code, no creds) and mounts
GameView cleanly.
Regression test in solo-smoke.spec.ts seeds sessionStorage with stale
MP creds, clicks Play Solo, and asserts:
- URL settles on /game (not /game/<stale>)
- No 'mp-joining' placeholder
- Board renders (e2 pawn visible)
- All stale keys are wiped from sessionStorage
- No console errors
Verified the test fails without the fix (Playwright hits the blank
screen / Joining placeholder) and passes with it.
Adds 8 Playwright scenarios to modifier-profiles.spec.ts under a new
'T2 polish' describe block:
P1 editor undo/redo across 3 distinct type-modifier adds
P2 copy / paste wire: Copy lights the Paste button with a count
P3 paste-type-modifier disabled when clipboard empty (baseline)
P4 conflict panel: seed an invuln-king profile via localStorage,
bind layout=classic, Load, observe error + Fix clears it
P5 modifier-indicator rendered without hover (create-room path,
with the same no-WS-server test.skip fallback T26 uses)
P6 source-chain in pinned panel — test.fixme; ModifierPinnedPanel
computes row.source but does not render it yet
P7 multiplayer propose->approve e2e — test.fixme; needs a
two-context harness this spec doesn't have today. Protocol
coverage lives at packages/server/src/ws.modifier-profile-
consent.test.ts.
P8 multiplayer propose->reject e2e — same harness gap as P7.
Adds 2 regression tests to solo-smoke.spec.ts:
- Rules drawer: clicking the backdrop (far-left of viewport)
closes the drawer and leaves the board interactive. Regression
guard for the stuck-overlay pointer-events bug.
- Modifier editor: Esc closes the editor but leaves the drawer
open (capture-phase stopImmediatePropagation); a second Esc
then closes the drawer. Documents the nested-Esc ordering
contract and guards against a future change that would cascade
both closes on one keystroke.
Result: 55 Playwright passing, 3 skipped (all documented fixme).
bun run check green.
The T1 ModifierProfileEditor installed a window-level Esc handler that
closed the modal but the RulesDrawer had no Esc handler of its own.
Users hitting Esc with the drawer open (no modal) saw nothing happen;
worse, with both open+modal, closing the modal left the drawer's
pointer-events-blocking backdrop in place, silently breaking all
board drag-interaction afterward.
Fix:
- Add useEffect-based Esc handler to RulesDrawer that closes it when
no nested modal is active.
- ModifierProfileEditor now uses capture-phase + stopImmediatePropagation
so the drawer's Esc handler does NOT also fire on the same keystroke,
preventing double-close.
Add packages/chess/e2e/solo-smoke.spec.ts — 5 regression scenarios
that would have caught this at T1 CI time. Test 4 specifically
reproduces the original bug (drawer open → Esc → drag board pieces).
Also queue 2 additional scenarios in the T2 plan since T2 work extends
both drawer + editor further.
All tests green: 1217 unit tests (94 files), 48 Playwright e2e in 1.3m.