fix(lobby): clear stale MP creds on Play Solo to avoid blank-screen trap
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.
This commit is contained in:
parent
7bee3cbaa9
commit
6e0479703d
2 changed files with 77 additions and 0 deletions
|
|
@ -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/<stale-code> → 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/<stale>', 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('; '));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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/<stale-code>, 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');
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue