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, 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 }; } 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(); }); });