2a045a5b37
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 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
In a local game the human seat's account id was a synthetic 'local:human:0',
so seatName's `accountId === session.userId` check never matched: the game
header rendered BOTH seats as 🤖 (the vs_ai fallback), and the lobby's
groupGames could not find the viewer's seat, so a human's turn read as
'Their turn' with the hourglass. Carry the real account id on the human seat
(create -> record -> GameView); the robot keeps its synthetic id.
Local games created before this fix keep the old display (no migration).
76 lines
2.9 KiB
TypeScript
76 lines
2.9 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import type { LocalGameRecord } from './serialize';
|
|
|
|
// An in-memory stand-in for the IndexedDB store, so list() — which reads the *persisted* records,
|
|
// not the source's in-memory cache — can be exercised in the node env (the real store is a
|
|
// best-effort no-op without IndexedDB). Declared before the vi.mock factory that closes over it.
|
|
const store = new Map<string, LocalGameRecord>();
|
|
vi.mock('./store', () => ({
|
|
saveLocalGame: async (r: LocalGameRecord) => void store.set(r.id, r),
|
|
getLocalGame: async (id: string) => store.get(id) ?? null,
|
|
listLocalGames: async () => [...store.values()],
|
|
deleteLocalGame: async (id: string) => void store.delete(id),
|
|
}));
|
|
vi.mock('../dict', () => ({
|
|
getDawg: async () => {
|
|
const { Dawg } = await import('../dict/dawg');
|
|
const { readFileSync } = await import('node:fs');
|
|
return new Dawg(new Uint8Array(readFileSync(new URL('../dict/testdata/sample_en.dawg', import.meta.url))));
|
|
},
|
|
}));
|
|
|
|
import { LocalSource } from './source';
|
|
import type { Seat } from './serialize';
|
|
|
|
const seats: Seat[] = [
|
|
{ kind: 'human', name: 'You' },
|
|
{ kind: 'robot', name: 'Robot' },
|
|
];
|
|
const opts = (id: string, seed: bigint) => ({
|
|
id,
|
|
variant: 'scrabble_en' as const,
|
|
dictVersion: 'sample',
|
|
seed,
|
|
multipleWords: true,
|
|
seats,
|
|
});
|
|
|
|
beforeEach(() => store.clear());
|
|
|
|
describe('LocalSource.list', () => {
|
|
it('is empty when no games are stored', async () => {
|
|
expect(await new LocalSource().list()).toEqual([]);
|
|
});
|
|
|
|
it('returns a lobby GameView for every stored local game', async () => {
|
|
const src = new LocalSource();
|
|
await src.create(opts('local:a', 1n));
|
|
await src.create(opts('local:b', 2n));
|
|
const list = await src.list();
|
|
expect(new Set(list.map((g) => g.id))).toEqual(new Set(['local:a', 'local:b']));
|
|
expect(list.every((g) => g.vsAi && g.status === 'active')).toBe(true);
|
|
});
|
|
|
|
it('carries the human seat account id into the GameView so the client identifies "you"', async () => {
|
|
const src = new LocalSource();
|
|
await src.create({
|
|
...opts('local:me', 3n),
|
|
seats: [
|
|
{ kind: 'human', name: 'You', accountId: 'acct-42' },
|
|
{ kind: 'robot', name: 'Robot' },
|
|
],
|
|
});
|
|
const [g] = await src.list();
|
|
expect(g.seats[0].accountId).toBe('acct-42'); // the human's real id, matched against the session
|
|
expect(g.seats[1].accountId).not.toBe('acct-42'); // the robot keeps a synthetic id
|
|
});
|
|
|
|
it('reconstructs a game persisted by a previous session (not in the source cache)', async () => {
|
|
// Seed the store via one source, then list from a fresh source whose in-memory cache is empty.
|
|
await new LocalSource().create(opts('local:old', 7n));
|
|
const list = await new LocalSource().list();
|
|
expect(list.map((g) => g.id)).toEqual(['local:old']);
|
|
expect(list[0].variant).toBe('scrabble_en');
|
|
});
|
|
});
|