diff --git a/packages/chess/e2e/solo-smoke.spec.ts b/packages/chess/e2e/solo-smoke.spec.ts index 25b2e2c..0abd067 100644 --- a/packages/chess/e2e/solo-smoke.spec.ts +++ b/packages/chess/e2e/solo-smoke.spec.ts @@ -209,4 +209,70 @@ test.describe('Solo-play smoke (T2 preview tests)', () => { timeout: 3000, }); }); + + // Regression guard for the "play-solo lands on blank screen" bug. + // + // Before the fix, a stale multiplayer session (room-code/room-token in + // sessionStorage from a previous room.create) would make Play Solo + // navigate to /game → GameRoute's Case 1 canonicalises to + // /game/ → MultiplayerGameView mounts → WS handshake to a + // long-dead room silently fails → blank screen. + // + // The fix: handlePlaySolo() explicitly clears room-code, room-token, + // player-color, layout-name, and modifier-profile-name from + // sessionStorage before navigating, guaranteeing a fresh solo mount. + test('play-solo with stale MP creds in sessionStorage lands on /game, not /game/', async ({ + page, + }) => { + // Seed sessionStorage to simulate a previous room.create or room.join + // whose creds were never cleared (e.g. user closed the tab after + // playing multiplayer, then reopened the lobby later). + await page.evaluate(() => { + sessionStorage.setItem('room-code', 'STALE1'); + sessionStorage.setItem('room-token', 'stale-token-deadbeef'); + sessionStorage.setItem('player-color', 'white'); + sessionStorage.setItem('layout-name', 'Some Previous Layout'); + sessionStorage.setItem('modifier-profile-name', 'Old Profile'); + }); + + const errors: string[] = []; + page.on('pageerror', (e) => errors.push(`PAGE: ${e.message}`)); + page.on('console', (msg) => { + if (msg.type() === 'error') errors.push(`CONSOLE: ${msg.text()}`); + }); + + await page.locator('[data-action="play-solo"]').click(); + + // URL must settle on /game WITHOUT a code suffix. Using a short + // timeout so a regression surfaces as a test failure rather than + // the suite hanging on the blank-screen state. + await page.waitForURL('**/game', { timeout: 3000 }); + await expect(page).toHaveURL(/\/game$/); + + // Board must render a real solo game (e2 pawn present, not the + // "Joining room …" placeholder). + await expect(page.locator('[data-testid="mp-joining"]')).toHaveCount(0); + await expect(page.locator('[data-square="e2"] [data-piece]')).toBeVisible({ + timeout: 3000, + }); + + // All stale sessionStorage entries must be wiped so a page reload + // doesn't re-trigger the bug on the refreshed /game route. + const staleCreds = await page.evaluate(() => ({ + code: sessionStorage.getItem('room-code'), + token: sessionStorage.getItem('room-token'), + color: sessionStorage.getItem('player-color'), + layout: sessionStorage.getItem('layout-name'), + profile: sessionStorage.getItem('modifier-profile-name'), + })); + expect(staleCreds).toEqual({ + code: null, + token: null, + color: null, + layout: null, + profile: null, + }); + + if (errors.length > 0) throw new Error(errors.join('; ')); + }); }); diff --git a/packages/chess/src/ui/Lobby.tsx b/packages/chess/src/ui/Lobby.tsx index f36714d..87c7ef4 100644 --- a/packages/chess/src/ui/Lobby.tsx +++ b/packages/chess/src/ui/Lobby.tsx @@ -324,6 +324,17 @@ export function Lobby({ chessState }: LobbyProps = {}) { }; const handlePlaySolo = () => { + // Clear any stale multiplayer creds from a previous session. Without + // this, a user who played multiplayer earlier (creds still in + // sessionStorage) would hit GameRoute's Case 1 — /game with creds + // present gets canonicalised to /game/, which then + // tries to reconnect to a long-dead room and renders a blank + // screen when the WS handshake silently fails. + sessionStorage.removeItem('room-code'); + sessionStorage.removeItem('room-token'); + sessionStorage.removeItem('player-color'); + sessionStorage.removeItem('layout-name'); + sessionStorage.removeItem('modifier-profile-name'); resetToFreshGame(); navigate('/game'); };