Files
ttrpg-initiative-tracker/src/tests/Combat.characterization.test.js
T
david raistrick f81308a0df tests: consolidate into tests/ dirs, fix import paths
Move all test files out of source dirs into per-workspace tests/:
- shared/tests/   (3 unit test files)
- server/tests/   (1 integration test)
- src/tests/      (8 characterization + scenario tests + testHelpers)

Fix all relative import paths (App, storage, __mocks__, testHelpers).
Fix jest.config testMatch globs in shared/ and server/ (rootDir +
<rootDir>/tests pattern).

Delete scripts/repro-pause-bug.js (debug scratch, superseded by
turn.pause-add.test.js).

Keep scripts/replay-combat.js + scripts/audit-rotation.js as manual
demo/exploratory tools (NOT unit tests, not deterministic).

No logic changes. All green: shared 49 + 1 validated RED, server 23,
FE 62. Scenario test unchanged (240s timeout, pre-existing slow).
2026-06-29 16:02:22 -04:00

138 lines
5.2 KiB
JavaScript

// Combat characterization. Lock updateDoc/setDoc patch for combat controls.
import React from 'react';
import { screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import { getCalls } from '../__mocks__/firebase/_mock-db';
import { setupReady, addMonsterViaUI, startCombatViaUI } from './testHelpers';
function findCallsEnc() {
return getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/'));
}
function lastEncCall() {
const calls = findCallsEnc();
return calls[calls.length - 1];
}
function findCallActiveDisplay(fn) {
return getCalls().filter(c => c.fn === fn && c.path.includes('activeDisplay/status'));
}
async function setupWithMonsters(names = ['A', 'B', 'C']) {
await setupReady('CombatCamp', 'CombatEnc');
for (const n of names) {
await addMonsterViaUI(n, 20, Number(n.charCodeAt(0) % 10));
}
}
describe('Combat -> Firebase', () => {
test('startEncounter: updateDoc sets isStarted/round/turn/current', async () => {
await setupWithMonsters();
await startCombatViaUI();
const call = lastEncCall();
expect(call.data).toMatchObject({
isStarted: true,
isPaused: false,
round: 1,
});
expect(call.data.currentTurnParticipantId).toBeTruthy();
expect(call.data.turnOrderIds).toHaveLength(3);
});
test('startEncounter: also sets activeDisplay to this encounter', async () => {
await setupWithMonsters();
await startCombatViaUI();
const adCalls = findCallActiveDisplay('setDoc');
const last = adCalls[adCalls.length - 1];
expect(last.data.activeCampaignId).toBeTruthy();
expect(last.data.activeEncounterId).toBeTruthy();
});
test('nextTurn: advances currentTurnParticipantId', async () => {
await setupWithMonsters();
await startCombatViaUI();
const beforeId = lastEncCall().data.currentTurnParticipantId;
fireEvent.click(screen.getByRole('button', { name: /Next Turn/i }));
await waitFor(() => lastEncCall()?.data?.currentTurnParticipantId !== beforeId);
expect(lastEncCall().data.currentTurnParticipantId).not.toBe(beforeId);
});
test('nextTurn wrapping to round 1->2 increments round', async () => {
await setupWithMonsters(['A', 'B']);
await startCombatViaUI();
// advance through all participants to wrap
fireEvent.click(screen.getByRole('button', { name: /Next Turn/i })); // A->B (or 2nd)
await waitFor(() => lastEncCall()?.data?.currentTurnParticipantId);
fireEvent.click(screen.getByRole('button', { name: /Next Turn/i })); // wrap
await waitFor(() => lastEncCall()?.data?.round === 2);
expect(lastEncCall().data.round).toBe(2);
});
test('pause: updateDoc sets isPaused true', async () => {
await setupWithMonsters();
await startCombatViaUI();
fireEvent.click(screen.getByRole('button', { name: /Pause Combat/i }));
await waitFor(() => lastEncCall()?.data?.isPaused === true);
expect(lastEncCall().data.isPaused).toBe(true);
});
test('resume: updateDoc sets isPaused false + recomputes turnOrder', async () => {
await setupWithMonsters();
await startCombatViaUI();
fireEvent.click(screen.getByRole('button', { name: /Pause Combat/i }));
await waitFor(() => lastEncCall()?.data?.isPaused === true);
fireEvent.click(screen.getByRole('button', { name: /Resume Combat/i }));
await waitFor(() => lastEncCall()?.data?.isPaused === false);
const call = lastEncCall();
expect(call.data.isPaused).toBe(false);
expect(call.data.turnOrderIds).toHaveLength(3);
});
test('endEncounter: updateDoc resets all combat state', async () => {
await setupWithMonsters();
await startCombatViaUI();
fireEvent.click(screen.getByRole('button', { name: /End Combat/i }));
fireEvent.click(await screen.findByRole('button', { name: /Confirm/i }));
await waitFor(() => lastEncCall()?.data?.isStarted === false);
const call = lastEncCall();
expect(call.data).toMatchObject({
isStarted: false,
isPaused: false,
round: 0,
currentTurnParticipantId: null,
turnOrderIds: [],
});
});
test('endEncounter: clears activeDisplay', async () => {
await setupWithMonsters();
await startCombatViaUI();
fireEvent.click(screen.getByRole('button', { name: /End Combat/i }));
fireEvent.click(await screen.findByRole('button', { name: /Confirm/i }));
await waitFor(() => {
const adCalls = findCallActiveDisplay('setDoc');
const last = adCalls[adCalls.length - 1];
return last && last.data.activeCampaignId === null;
});
const adCalls = findCallActiveDisplay('setDoc');
const last = adCalls[adCalls.length - 1];
expect(last.data).toMatchObject({ activeCampaignId: null, activeEncounterId: null });
});
test('toggleHidePlayerHp: setDoc merge on activeDisplay/status', async () => {
await setupWithMonsters();
await startCombatViaUI();
const switchBtn = screen.getByRole('switch');
fireEvent.click(switchBtn);
await waitFor(() => {
const adCalls = findCallActiveDisplay('setDoc');
const last = adCalls[adCalls.length - 1];
return last && 'hidePlayerHp' in last.data;
});
const adCalls = findCallActiveDisplay('setDoc');
const last = adCalls[adCalls.length - 1];
expect(last.data).toHaveProperty('hidePlayerHp');
});
});