// Characterization for reorderParticipants correct usage. // replay-combat.js calls it with wrong signature (swallowed by try/catch), // so real behavior untested. Lock what it actually does. // // New API: reorderParticipants is async, takes ctx last, writes encounter + // log internally, returns newEnc. A blocked move (cross-init / same id / // cross-pointer) returns the SAME enc ref (no write). Missing id rejects. const shared = require('@ttrpg/shared'); const { makeParticipant, startEncounter, nextTurn, reorderParticipants } = shared; const { mockCtx } = require('./_helpers'); 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:[] }; } describe('reorderParticipants', () => { test('drag upward inserts before target (1-list model, pre-combat)', async () => { const { ctx } = mockCtx(); const ps = [p('a', 10), p('b', 20), p('c', 20)]; // b,c tie let e = enc(ps); // pre-combat, no pointer const newEnc = await reorderParticipants(e, 'c', 'b', ctx); // drag c upward onto b: insert before b → [a,c,b] expect(newEnc.participants.map(p => p.id)).toEqual(['a', 'c', 'b']); }); test('drag downward inserts after target (1-list model, pre-combat)', async () => { const { ctx } = mockCtx(); const ps = [p('a', 20), p('b', 20), p('c', 10)]; // a,b tie let e = enc(ps); // pre-combat, no pointer const newEnc = await reorderParticipants(e, 'a', 'b', ctx); // drag a downward onto b: insert after b → [b,a,c] expect(newEnc.participants.map(p => p.id)).toEqual(['b', 'a', 'c']); }); test('cross-init drag blocked (no-op)', async () => { const { storage, ctx } = mockCtx(); const ps = [p('a', 10), p('b', 20)]; let e = enc(ps); e = await startEncounter(e, ctx); // [b,a] const before = storage.calls.filter(c => c.fn === 'updateDoc').length; const newEnc = await reorderParticipants(e, 'a', 'b', ctx); expect(newEnc).toBe(e); // same ref = no write expect(storage.calls.filter(c => c.fn === 'updateDoc')).toHaveLength(before); // no new write }); test('throws if id not found', async () => { const { ctx } = mockCtx(); const ps = [p('a', 10), p('b', 20)]; let e = enc(ps); e = await startEncounter(e, ctx); await expect(reorderParticipants(e, 'a', 'zzz', ctx)).rejects.toThrow(); }); test('paused combat allows reorder across current-turn pointer', async () => { const { ctx } = mockCtx(); const ps = [p('a', 20), p('b', 20), p('c', 20), p('d', 20)]; let e = await startEncounter(enc(ps), ctx); // current=a e = { ...e, isPaused: true }; const newEnc = await reorderParticipants(e, 'd', 'a', ctx); expect(newEnc).not.toBe(e); expect(newEnc.participants.map(p => p.id)).toEqual(['d', 'a', 'b', 'c']); expect(newEnc.turnOrderIds).toEqual(['d', 'a', 'b', 'c']); }); test('syncs turnOrderIds = participants order (1-list, fixes BUG-6)', async () => { const { ctx } = mockCtx(); const ps = [p('a', 10), p('b', 20), p('c', 20)]; let e = enc(ps); e = await startEncounter(e, ctx); // started, but reorder pre-pointer advance // startEncounter sets current=b (idx0). reorder c before b crosses pointer. // Use pre-combat to test syncTurnOrder. let pre = enc(ps); pre = await reorderParticipants(pre, 'c', 'b', ctx); expect(pre.turnOrderIds).toEqual(['a','c','b']); expect(pre.turnOrderIds).toEqual(pre.participants.map(p => p.id)); }); // BUG-6 candidate: reorder should affect turnOrderIds so mid-combat // drag-drop changes who goes next within same-initiative tie. // Currently RED (turnOrderIds not in patch). test('reorder updates turnOrderIds to reflect new participant order (pre-combat)', async () => { const { ctx } = mockCtx(); const ps = [p('a', 10), p('b', 20), p('c', 20)]; let e = enc(ps); // pre-combat, no pointer e = await reorderParticipants(e, 'c', 'b', ctx); expect(e.turnOrderIds).toEqual(['a', 'c', 'b']); }); });