cf70e6b1fc
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 48s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 58s
The per-screen in-memory caches (lobbycache, gamecache) were refreshed only by the screen that owns them while it was mounted, so a state change that crossed screens left the other screen's cache stale and it visibly redrew on the next navigation: - game -> lobby: the player's own move advanced the game cache but not the lobby snapshot, and an own move carries no self-directed push event, so returning to the lobby painted the pre-move status until the background refresh corrected it. - lobby -> game (and from any other screen): an opponent's move / game-over refreshed the lobby (while mounted) but never the per-game cache, so opening that game flashed the pre-move board. Make cache freshness cross-screen, owned by the single global stream handler that runs for every live event regardless of the mounted screen: - patchLobbyGame upserts the affected game's GameView into the lobby snapshot; the global handler calls it on opponent_moved / game_over / opponent_joined and on a match_found / game_started seed (so a game started elsewhere is present too). The game board still mirrors the player's own move and its own load() — the two updates no live event carries. - advanceCached (a pure wrapper over the existing delta reducers) advances a not-currently-viewed game's cache from opponent_moved / game_over; the game in view is skipped so its mounted board stays the sole owner (no double apply). End-state behaviour is unchanged (the background refresh always reconciled); this removes the transient stale frame. Unit-tested patchLobbyGame and advanceCached; docs/UI_DESIGN.md updated.
62 lines
2.3 KiB
TypeScript
62 lines
2.3 KiB
TypeScript
import { beforeEach, describe, expect, it } from 'vitest';
|
|
import { clearLobby, getLobby, patchLobbyGame, setLobby } from './lobbycache';
|
|
import type { AccountRef, GameView } from './model';
|
|
|
|
function gameView(id: string, status: GameView['status'] = 'active', toMove = 0): GameView {
|
|
return {
|
|
id,
|
|
variant: 'scrabble_en',
|
|
dictVersion: 'v1',
|
|
status,
|
|
players: 2,
|
|
toMove,
|
|
turnTimeoutSecs: 300,
|
|
multipleWordsPerTurn: true,
|
|
moveCount: 0,
|
|
endReason: '',
|
|
lastActivityUnix: 0,
|
|
seats: [],
|
|
};
|
|
}
|
|
|
|
beforeEach(() => clearLobby());
|
|
|
|
describe('patchLobbyGame', () => {
|
|
it('replaces the matching game by id and leaves the others untouched', () => {
|
|
setLobby({ games: [gameView('a', 'active', 0), gameView('b', 'active', 0)], invitations: [], incoming: [] });
|
|
// The player's own move flipped game "a" to the opponent's turn.
|
|
patchLobbyGame(gameView('a', 'active', 1));
|
|
const snap = getLobby();
|
|
expect(snap?.games.map((g) => g.id)).toEqual(['a', 'b']);
|
|
expect(snap?.games.find((g) => g.id === 'a')?.toMove).toBe(1);
|
|
expect(snap?.games.find((g) => g.id === 'b')?.toMove).toBe(0);
|
|
});
|
|
|
|
it('preserves invitations and incoming when patching a game', () => {
|
|
const incoming: AccountRef[] = [{ accountId: 'u9', displayName: 'Nine' }];
|
|
setLobby({ games: [gameView('a')], invitations: [], incoming });
|
|
patchLobbyGame(gameView('a', 'finished'));
|
|
expect(getLobby()?.incoming).toEqual(incoming);
|
|
expect(getLobby()?.games[0].status).toBe('finished');
|
|
});
|
|
|
|
it('adds the game when it is not yet in the cached lobby (a game started elsewhere)', () => {
|
|
setLobby({ games: [gameView('a')], invitations: [], incoming: [] });
|
|
patchLobbyGame(gameView('z', 'active'));
|
|
// Order is irrelevant — the lobby re-groups and re-sorts on render — so assert membership.
|
|
expect(getLobby()?.games.map((g) => g.id).sort()).toEqual(['a', 'z']);
|
|
});
|
|
|
|
it('is a no-op when there is no cached lobby yet', () => {
|
|
patchLobbyGame(gameView('a'));
|
|
expect(getLobby()).toBeNull();
|
|
});
|
|
|
|
it('does not mutate the previous snapshot array', () => {
|
|
const games = [gameView('a', 'active', 0)];
|
|
setLobby({ games, invitations: [], incoming: [] });
|
|
patchLobbyGame(gameView('a', 'active', 1));
|
|
expect(games[0].toMove).toBe(0); // the original array/object is left intact
|
|
});
|
|
});
|