Files
scrabble-game/ui/src/lib/gamecache.test.ts
T
Ilia Denisov 6e77de4c1e
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 53s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m26s
feat: sparser robot nudges, typed unread badge, lobby unread bump
Three owner-requested polish changes:

- robot: replace the lengthening 60-90 min -> 6 h proactive-nudge ramp with a
  flat uniform 9-12 h wait before every nudge; the existing sleep-window gate
  still skips and defers a nudge that would land in the robot's night.
- ui: colour the lobby/in-game unread dot by type -- the regular danger colour
  when a chat message is unread, a softer amber (--warn) when only nudges are.
  Adds a per-viewer unread_messages flag (chat_messages.kind='message') across
  the backend DTO, FlatBuffers wire, gateway transcode and the UI store.
- ui: float games with any unread notification to the top of the lobby's
  your-turn and opponent-turn sections (finished keeps its order), reusing the
  existing unread_chat flag.

Docs (ARCHITECTURE 7, FUNCTIONAL + _ru) updated. No DB migration; the new wire
field is backward-compatible.
2026-06-19 16:50:48 +02:00

91 lines
2.9 KiB
TypeScript

import { beforeEach, describe, expect, it } from 'vitest';
import { clearGameCache, getCachedGame, setCachedDraft, setCachedGame } from './gamecache';
import type { GameView, MoveRecord, StateView } from './model';
function gameView(id: string): GameView {
return {
id,
variant: 'scrabble_en',
dictVersion: 'v1',
vsAi: false,
unreadChat: false,
unreadMessages: false,
status: 'active',
players: 2,
toMove: 0,
turnTimeoutSecs: 300,
multipleWordsPerTurn: true,
moveCount: 0,
endReason: '',
lastActivityUnix: 0,
seats: [],
};
}
function view(id: string, rack: string[] = ['A', 'B']): StateView {
return { game: gameView(id), seat: 0, rack, bagLen: 50, hintsRemaining: 1, walletBalance: 0 };
}
function move(player: number): MoveRecord {
return { player, action: 'play', dir: 'H', mainRow: 7, mainCol: 7, tiles: [], words: ['AB'], count: 0, score: 10, total: 10 };
}
beforeEach(() => clearGameCache());
describe('setCachedGame', () => {
it('stores state, history and draft', () => {
setCachedGame('g1', view('g1'), [move(0)], '{"x":1}');
const c = getCachedGame('g1');
expect(c?.view.game.id).toBe('g1');
expect(c?.moves).toHaveLength(1);
expect(c?.draft).toBe('{"x":1}');
});
it('leaves draft undefined when none is given for a new entry', () => {
setCachedGame('g1', view('g1'), []);
expect(getCachedGame('g1')?.draft).toBeUndefined();
});
it('preserves the cached draft when draft is omitted (the live-event delta path)', () => {
setCachedGame('g1', view('g1'), [], 'DRAFT');
// A delta advances view+moves without refetching the draft; it must not be dropped.
setCachedGame('g1', view('g1', ['C', 'D']), [move(1)]);
const c = getCachedGame('g1');
expect(c?.draft).toBe('DRAFT'); // preserved
expect(c?.view.rack).toEqual(['C', 'D']); // view advanced
expect(c?.moves).toHaveLength(1);
});
it('clears the draft when passed an empty string (a committed move)', () => {
setCachedGame('g1', view('g1'), [], 'DRAFT');
setCachedGame('g1', view('g1'), [move(0)], '');
expect(getCachedGame('g1')?.draft).toBe('');
});
});
describe('setCachedDraft', () => {
it('updates only the draft of an already-cached game', () => {
setCachedGame('g1', view('g1', ['A']), [move(0)], 'OLD');
setCachedDraft('g1', 'NEW');
const c = getCachedGame('g1');
expect(c?.draft).toBe('NEW');
expect(c?.view.rack).toEqual(['A']); // unchanged
expect(c?.moves).toHaveLength(1);
});
it('is a no-op when the game is not cached', () => {
setCachedDraft('absent', 'X');
expect(getCachedGame('absent')).toBeUndefined();
});
});
describe('clearGameCache', () => {
it('drops every cached game', () => {
setCachedGame('g1', view('g1'), []);
setCachedGame('g2', view('g2'), []);
clearGameCache();
expect(getCachedGame('g1')).toBeUndefined();
expect(getCachedGame('g2')).toBeUndefined();
});
});