test(engine): guard offline-loop safety and harden purity test
- save.test.ts: add regression test proving loop actions do NOT start or yield during offline catch-up (applyOfflineProgress replays tickGame directly; maybeStartLoopAction is never called, so an enabled loop with no active action stays idle and yields 0). - purity.test.ts: extend FORBIDDEN list to catch environment APIs (Date.now(), localStorage., indexedDB., idb-keyval import, document., window., requestAnimationFrame()) in addition to the existing React/react-dom/zustand import guards. Patterns are scoped to call sites and member-access forms so prose comments (e.g. save.ts's "Date scheduling" doc comment) do not false-positive. lz-string is intentionally omitted — it is a pure compression library. - NavRail.tsx: add sr-only "New story" span alongside the aria-hidden pulse dot so screen readers can perceive the unread-story badge.
This commit is contained in:
@@ -135,6 +135,35 @@ describe('invalid / tampered saves', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyOfflineProgress() — loop invariant', () => {
|
||||
it('does NOT start a loop action during offline catch-up even when it is enabled', () => {
|
||||
// Hardest invariant: maybeStartLoopAction is called by the runtime AFTER each live
|
||||
// tick, never from tickGame itself. Offline catch-up replays tickGame directly, so
|
||||
// loop actions must never start (and therefore never yield) during catch-up.
|
||||
const content = buildContent({
|
||||
resources: [{ id: 'wood', name: 'Wood', startAmount: 0 }],
|
||||
actions: [
|
||||
{
|
||||
id: 'chop',
|
||||
name: 'Chop Wood',
|
||||
kind: 'loop',
|
||||
group: { id: 'test', label: 'Test' },
|
||||
durationMs: 1000,
|
||||
yields: [{ resourceId: 'wood', amount: 1 }],
|
||||
},
|
||||
],
|
||||
});
|
||||
const state = createGameState(content);
|
||||
// Enable the loop — player has toggled it on — but do NOT make it active.
|
||||
state.enabledLoopActionIds.chop = true;
|
||||
// Simulate coming back online after 10 seconds (10 full loop durations).
|
||||
applyOfflineProgress(state, content, 0, 10_000);
|
||||
// The loop must NOT have started or yielded during offline catch-up.
|
||||
expect(state.activeActionId).toBeNull();
|
||||
expect(state.resources.wood).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyOfflineProgress()', () => {
|
||||
it('credits whole ticks of elapsed time to the active action', () => {
|
||||
const content = testContent();
|
||||
|
||||
Reference in New Issue
Block a user