fix(ui): poll/refetch fallback for a missed opponent_joined in the open-game wait
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 57s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 57s
PR #51 moved the auto-match "wait for an opponent" from the lobby matchmaking screen into the open game but did not carry over that screen's poll fallback. The notify hub is best-effort and never replays, the live-stream resubscribe sends no cursor, and the game screen refreshed only from push events — so an opponent_joined dropped while the stream was down (e.g. a mobile suspend) left the starter stuck on "searching for opponent" until they re-entered the game. Unlike opponent_moved/game_over, opponent_joined has no follow-up event to trigger the existing move-count gap refetch. Recover it in Game.svelte: (A) refetch once on stream reconnect (covers the common suspend/resume case and rescues a missed move/game_over too), and (B) poll game.state every 2.5s while still waiting with the stream down (mirrors the old matchmaking startPoll). Add a mock-mode __stream e2e seam and two specs isolating each path, fix the now-stale streamAlive comment, and document the fallback in ARCHITECTURE §10.
This commit is contained in:
@@ -29,3 +29,38 @@ test('quick game: enter immediately, wait for an opponent, then it joins', async
|
||||
await expect(page.getByText(/Searching for opponent/)).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: 'Drop game' })).toBeEnabled();
|
||||
});
|
||||
|
||||
// The opponent_joined push is best-effort and never replayed, so a join that lands while the live
|
||||
// stream is down is lost. The waiting game screen recovers it without a push: a poll while the
|
||||
// stream is down, and a refetch on stream reconnect. The __stream hook (lib/app.svelte.ts) drops /
|
||||
// restores the stream; while it is dropped the mock has no subscriber, so joinOpponent emits to
|
||||
// no one — exactly the missed-event case.
|
||||
async function enterOpenGame(page: import('@playwright/test').Page): Promise<void> {
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: /guest/i }).click();
|
||||
await page.getByRole('button', { name: /New/ }).click();
|
||||
await page.locator('.variant').first().click();
|
||||
await page.getByRole('button', { name: /Start game/i }).click();
|
||||
await expect(page.getByText(/Searching for opponent/)).toBeVisible();
|
||||
}
|
||||
|
||||
test('quick game: a poll recovers a join missed while the live stream is down', async ({ page }) => {
|
||||
await enterOpenGame(page);
|
||||
// Drop the stream (no auto-reconnect), then let the opponent join: the push reaches no one, so
|
||||
// only the poll fallback can restore the UI.
|
||||
await page.evaluate(() => (window as unknown as { __stream: { drop(): void } }).__stream.drop());
|
||||
await page.evaluate(() => (window as unknown as { __mock: { joinOpponent(): void } }).__mock.joinOpponent());
|
||||
await expect(page.getByText('Robo')).toBeVisible();
|
||||
await expect(page.getByText(/Searching for opponent/)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('quick game: a stream reconnect recovers a join missed while it was down', async ({ page }) => {
|
||||
await enterOpenGame(page);
|
||||
// The opponent joins while the stream is down (the push is lost), then it reconnects before the
|
||||
// poll could tick — the reconnect refetch is what catches up.
|
||||
await page.evaluate(() => (window as unknown as { __stream: { drop(): void } }).__stream.drop());
|
||||
await page.evaluate(() => (window as unknown as { __mock: { joinOpponent(): void } }).__mock.joinOpponent());
|
||||
await page.evaluate(() => (window as unknown as { __stream: { restore(): void } }).__stream.restore());
|
||||
await expect(page.getByText('Robo')).toBeVisible();
|
||||
await expect(page.getByText(/Searching for opponent/)).toHaveCount(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user