Files
idlegame/src/state/__tests__/persistence.test.ts
T
ginnoir 91348ed42f refactor(ui): finalize review fixes for action cards and view model
- Guard ActionCard click-outside listener behind open state
- Add aria-expanded to ActionGroup collapse toggle
- Use optional chaining and drop non-null assertions in column projection
2026-06-11 22:16:40 -05:00

122 lines
3.9 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { buildContent } from '../../content/schema';
import { createGameState, enqueueAction, startAction } from '../../engine/game';
import { createSave, serializeSave } from '../../engine/save';
import { createMemoryBackend, loadGame, saveGame } from '../persistence';
const DEFAULT_GROUP = { id: 'test', label: 'Test' };
function testContent() {
return buildContent({
resources: [{ id: 'gold', name: 'Gold', startAmount: 0 }],
actions: [
{
id: 'forage',
name: 'Forage',
group: DEFAULT_GROUP,
durationMs: 3000,
yields: [{ resourceId: 'gold', amount: 1 }],
},
],
});
}
function queueTestContent() {
return buildContent({
resources: [{ id: 'gold', name: 'Gold', startAmount: 0 }],
actions: [
{
id: 'a',
name: 'A',
group: DEFAULT_GROUP,
durationMs: 3000,
yields: [{ resourceId: 'gold', amount: 1 }],
},
{
id: 'b',
name: 'B',
group: DEFAULT_GROUP,
durationMs: 3000,
yields: [{ resourceId: 'gold', amount: 1 }],
},
],
});
}
describe('loadGame()', () => {
it('returns a fresh game when no save exists', async () => {
const content = testContent();
const result = await loadGame(content, createMemoryBackend(), 1000);
expect(result.state.resources.gold).toBe(0);
expect(result.offlineMs).toBe(0);
});
it('round-trips a saved game and credits offline progress', async () => {
const content = testContent();
const state = createGameState(content);
state.resources.gold = 10;
startAction(state, content, 'forage');
const backend = createMemoryBackend();
await saveGame(state, backend, 1000);
const result = await loadGame(content, backend, 1000 + 9000); // 9s offline
expect(result.offlineMs).toBe(9000);
expect(result.state.resources.gold).toBe(11); // 10 + one completion
expect(result.state.activeActionId).toBeNull();
});
it('falls back to a fresh game on a corrupt save instead of throwing', async () => {
const content = testContent();
const result = await loadGame(content, createMemoryBackend('@@@garbage@@@'), 1000);
expect(result.state.resources.gold).toBe(0);
expect(result.offlineMs).toBe(0);
});
it('restores actionQueue on load', async () => {
const content = queueTestContent();
const state = createGameState(content);
enqueueAction(state, content, 'a');
enqueueAction(state, content, 'b');
const backend = createMemoryBackend();
await saveGame(state, backend, 1000);
const result = await loadGame(content, backend, 1000);
expect(result.state.activeActionId).toBe('a');
expect(result.state.actionQueue).toEqual(['b']);
});
it('restores story fields on load', async () => {
const content = testContent();
const backend = createMemoryBackend();
const state = createGameState(content);
state.storyFlags = { route_b: true };
state.currentStoryNodeId = 'fork_choice';
state.seenStoryNodeIds = ['boot_intro'];
await saveGame(state, backend, 1000);
const loaded = await loadGame(content, backend, 1000);
expect(loaded.state.storyFlags.route_b).toBe(true);
expect(loaded.state.currentStoryNodeId).toBe('fork_choice');
});
it('drops an active action that no longer exists in content', async () => {
const content = testContent();
const stale = serializeSave(
createSave(
{
resources: { gold: 1 },
activeActionId: 'ghost-action',
actionElapsedMs: 0,
actionQueue: [],
storyFlags: {},
currentStoryNodeId: '',
seenStoryNodeIds: [],
enabledLoopActionIds: {},
},
1000,
),
);
const result = await loadGame(content, createMemoryBackend(stale), 1000);
expect(result.state.activeActionId).toBeNull();
});
});