From be481767f0ff11b9a868841cb403d88f5f7e1782 Mon Sep 17 00:00:00 2001 From: david raistrick <1108844+keen99@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:50:48 -0400 Subject: [PATCH] tests: undo roundtrip (10 green) + reorderParticipants BUG-7 candidate turn.undo.test.js: every op with log.undo roundtrips to prior state. startEncounter, nextTurn, togglePause, applyHpChange, toggleCondition, toggleParticipantActive, addParticipant, removeParticipant, endEncounter. Found: reorderParticipants returns log:null. Cannot undo. Documents as BUG-7 candidate (test green now, asserts current behavior). --- shared/tests/turn.undo.test.js | 123 +++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 shared/tests/turn.undo.test.js diff --git a/shared/tests/turn.undo.test.js b/shared/tests/turn.undo.test.js new file mode 100644 index 0000000..58af83e --- /dev/null +++ b/shared/tests/turn.undo.test.js @@ -0,0 +1,123 @@ +// Undo roundtrip: every op that returns log.undo must restore prior state. +// Apply op → patch → apply undo → assert deepEqual original. + +const shared = require('@ttrpg/shared'); +const { + makeParticipant, startEncounter, nextTurn, togglePause, + addParticipant, removeParticipant, toggleParticipantActive, + applyHpChange, toggleCondition, reorderParticipants, endEncounter, +} = shared; + +function p(id, init, extra = {}) { + return makeParticipant({ + id, name: id, type: 'monster', + initiative: init, maxHp: 100, currentHp: 100, + ...extra, + }); +} +function enc(ps) { + return { name:'t', participants:ps, isStarted:false, isPaused:false, + round:0, currentTurnParticipantId:null, turnOrderIds:[] }; +} +const snap = (e) => JSON.parse(JSON.stringify(e)); + +describe('undo roundtrip', () => { + test('startEncounter undo restores pre-start', () => { + const before = enc([p('a',10),p('b',20)]); + const r = startEncounter(before); + expect(r.log.undo).toBeTruthy(); + const after = { ...before, ...r.patch, ...r.log.undo }; + expect(snap(after)).toEqual(snap(before)); + }); + + test('nextTurn undo restores prior currentTurn/round', () => { + let e = enc([p('a',10),p('b',20),p('c',5)]); + e = { ...e, ...startEncounter(e).patch }; + const before = snap(e); + const r = nextTurn(e); + expect(r.log.undo).toBeTruthy(); + const after = { ...e, ...r.patch, ...r.log.undo }; + expect(snap(after)).toEqual(before); + }); + + test('togglePause undo restores prior paused state', () => { + let e = enc([p('a',10),p('b',20)]); + e = { ...e, ...startEncounter(e).patch }; + const before = snap(e); + const r = togglePause(e); + expect(r.log.undo).toBeTruthy(); + const after = { ...e, ...r.patch, ...r.log.undo }; + expect(snap(after)).toEqual(before); + }); + + test('applyHpChange undo restores prior participants', () => { + let e = enc([p('a',10,{maxHp:100,currentHp:100}),p('b',20)]); + e = { ...e, ...startEncounter(e).patch }; + const before = snap(e); + const r = applyHpChange(e, 'a', 'damage', 20); + expect(r.log.undo).toBeTruthy(); + const after = { ...e, ...r.patch, ...r.log.undo }; + expect(snap(after)).toEqual(before); + }); + + test('toggleCondition undo restores prior participants', () => { + let e = enc([p('a',10),p('b',20)]); + e = { ...e, ...startEncounter(e).patch }; + const before = snap(e); + const r = toggleCondition(e, 'a', 'stunned'); + expect(r.log.undo).toBeTruthy(); + const after = { ...e, ...r.patch, ...r.log.undo }; + expect(snap(after)).toEqual(before); + }); + + test('toggleParticipantActive undo restores prior participants + turn order', () => { + let e = enc([p('a',10),p('b',20),p('c',5)]); + e = { ...e, ...startEncounter(e).patch }; + const before = snap(e); + const r = toggleParticipantActive(e, 'b'); + expect(r.log.undo).toBeTruthy(); + const after = { ...e, ...r.patch, ...r.log.undo }; + expect(snap(after)).toEqual(before); + }); + + test('addParticipant undo restores prior participants', () => { + let e = enc([p('a',10),p('b',20)]); + e = { ...e, ...startEncounter(e).patch }; + const before = snap(e); + const np = makeParticipant({ id:'z', name:'z', type:'monster', initiative:15, maxHp:50, currentHp:50 }); + const r = addParticipant(e, np); + expect(r.log.undo).toBeTruthy(); + const after = { ...e, ...r.patch, ...r.log.undo }; + expect(snap(after)).toEqual(before); + }); + + test('removeParticipant undo restores prior participants + turn order', () => { + let e = enc([p('a',10),p('b',20),p('c',5)]); + e = { ...e, ...startEncounter(e).patch }; + const before = snap(e); + const r = removeParticipant(e, 'b'); + expect(r.log.undo).toBeTruthy(); + const after = { ...e, ...r.patch, ...r.log.undo }; + expect(snap(after)).toEqual(before); + }); + + test('endEncounter undo restores prior state', () => { + let e = enc([p('a',10),p('b',20)]); + e = { ...e, ...startEncounter(e).patch }; + const before = snap(e); + const r = endEncounter(e); + expect(r.log.undo).toBeTruthy(); + const after = { ...e, ...r.patch, ...r.log.undo }; + expect(snap(after)).toEqual(before); + }); + + test('reorderParticipants has no undo (log: null) — BUG candidate', () => { + const ps = [p('a',10),p('b',20),p('c',20)]; // b,c tie + let e = enc(ps); + e = { ...e, ...startEncounter(e).patch }; + const r = reorderParticipants(e, 'c', 'b'); + // Documents: reorderParticipants returns log: null. Cannot undo. + // If undo expected here, this is BUG-7. + expect(r.log).toBeNull(); + }); +});