feat(mystery): save layer — round-trip, schema versioning, transcript cap
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { saveState, loadState, clearSave, SAVE_KEY } from './save'
|
||||
import type { GameState, TranscriptLine } from './types'
|
||||
import { SCHEMA_VERSION, TRANSCRIPT_CAP } from './types'
|
||||
|
||||
const baseState = (overrides: Partial<GameState> = {}): GameState => ({
|
||||
schemaVersion: SCHEMA_VERSION,
|
||||
location: 'foyer',
|
||||
inventory: [],
|
||||
roomState: {},
|
||||
flags: {},
|
||||
resolveLevel: 'steady',
|
||||
encounterState: {},
|
||||
lastNoun: null,
|
||||
pendingDisambiguation: null,
|
||||
transcript: [],
|
||||
theme: 'amber',
|
||||
endedWith: null,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('save — round trip', () => {
|
||||
let store: Record<string, string>
|
||||
|
||||
beforeEach(() => {
|
||||
store = {}
|
||||
vi.stubGlobal('localStorage', {
|
||||
getItem: (k: string) => (k in store ? store[k]! : null),
|
||||
setItem: (k: string, v: string) => { store[k] = v },
|
||||
removeItem: (k: string) => { delete store[k] },
|
||||
})
|
||||
})
|
||||
|
||||
it('round-trips an identical state', () => {
|
||||
const s = baseState({ location: 'cellar', flags: { gateOpened: true } })
|
||||
saveState(s)
|
||||
expect(loadState()).toEqual(s)
|
||||
})
|
||||
|
||||
it('returns null when nothing is saved', () => {
|
||||
expect(loadState()).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null and clears the slot on schema mismatch', () => {
|
||||
store[SAVE_KEY] = JSON.stringify({ ...baseState(), schemaVersion: SCHEMA_VERSION + 99 })
|
||||
expect(loadState()).toBeNull()
|
||||
expect(store[SAVE_KEY]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns null and clears the slot on malformed JSON', () => {
|
||||
store[SAVE_KEY] = 'not-json'
|
||||
expect(loadState()).toBeNull()
|
||||
expect(store[SAVE_KEY]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('truncates transcript to TRANSCRIPT_CAP on save', () => {
|
||||
const long: TranscriptLine[] = Array.from({ length: TRANSCRIPT_CAP + 50 }, (_, i) => ({
|
||||
kind: 'narration',
|
||||
text: `line ${i}`,
|
||||
}))
|
||||
saveState(baseState({ transcript: long }))
|
||||
const loaded = loadState()
|
||||
expect(loaded?.transcript).toHaveLength(TRANSCRIPT_CAP)
|
||||
// Keeps the most recent lines (the tail).
|
||||
expect(loaded?.transcript[0]?.text).toBe(`line ${50}`)
|
||||
expect(loaded?.transcript[TRANSCRIPT_CAP - 1]?.text).toBe(`line ${TRANSCRIPT_CAP + 49}`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('save — clear', () => {
|
||||
let store: Record<string, string>
|
||||
|
||||
beforeEach(() => {
|
||||
store = { [SAVE_KEY]: JSON.stringify(baseState()) }
|
||||
vi.stubGlobal('localStorage', {
|
||||
getItem: (k: string) => (k in store ? store[k]! : null),
|
||||
setItem: (k: string, v: string) => { store[k] = v },
|
||||
removeItem: (k: string) => { delete store[k] },
|
||||
})
|
||||
})
|
||||
|
||||
it('removes the save slot', () => {
|
||||
clearSave()
|
||||
expect(store[SAVE_KEY]).toBeUndefined()
|
||||
expect(loadState()).toBeNull()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user