Files
ttrpg-initiative-tracker/src/tests/Logs.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

172 lines
8.3 KiB
JavaScript

// Logs + deathSave characterization. Lock paths for log writes, undo, clear, death save.
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 findLogCalls() {
return getCalls().filter(c => c.fn === 'addDoc' && c.path.includes('/logs'));
}
function lastEncCall() {
const calls = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/'));
return calls[calls.length - 1];
}
// Navigate to /logs view. App reads pathname at mount; must re-render with path preset.
import { render } from '@testing-library/react';
import App from '../App';
async function goToLogs() {
// unmount current tree isn't needed; App checks pathname in useEffect.
// Re-render a fresh App instance in same container.
window.history.replaceState({}, '', '/logs');
document.body.innerHTML = '';
render(<App />);
await waitFor(() => screen.getByText(/Combat Log/i));
}
describe('Logs -> Firebase', () => {
test('logAction: addDoc to logs collection on combat start', async () => {
await setupReady('LogCamp', 'LogEnc');
await addMonsterViaUI('Mob', 10, 2);
await startCombatViaUI();
await waitFor(() => findLogCalls().some(c => /Combat started/.test(c.data.message)));
const logCall = findLogCalls().find(c => /Combat started/.test(c.data.message));
expect(logCall.data).toHaveProperty('message');
expect(logCall.data).toHaveProperty('timestamp');
expect(logCall.data.message).toMatch(/Combat started/);
});
test('logAction: includes undo payload', async () => {
await setupReady('UndoCamp', 'UndoEnc');
await addMonsterViaUI('Mob', 10, 2);
await startCombatViaUI();
await waitFor(() => findLogCalls().some(c => /Combat started/.test(c.data.message)));
const logCall = findLogCalls().find(c => /Combat started/.test(c.data.message));
expect(logCall.data.undo).toBeTruthy();
expect(logCall.data.undo).toHaveProperty('updates');
});
test('clearLogs: writeBatch deletes all log docs', async () => {
const { renderApp } = require('./testHelpers');
// seed a log entry via combat start
await setupReady('ClearCamp', 'ClearEnc');
await addMonsterViaUI('Mob', 10, 2);
await startCombatViaUI();
await waitFor(() => findLogCalls().length > 0);
await goToLogs();
const clearBtn = await screen.findByRole('button', { name: /Clear Log/i });
fireEvent.click(clearBtn);
fireEvent.click(await screen.findByRole('button', { name: /Confirm/i }));
await waitFor(() => {
const batchDeletes = getCalls().filter(c => c.fn === 'batch.delete' && c.path.includes('/logs'));
return batchDeletes.length > 0;
});
const batchDeletes = getCalls().filter(c => c.fn === 'batch.delete' && c.path.includes('/logs'));
expect(batchDeletes.length).toBeGreaterThan(0);
});
test('undo: updateDoc on encounter path + marks log undone', async () => {
// seed log via combat start
await setupReady('UndoFlowCamp', 'UndoFlowEnc');
await addMonsterViaUI('Mob', 10, 2);
await startCombatViaUI();
await waitFor(() => findLogCalls().length > 0);
const logId = findLogCalls()[0].path.split('/').pop();
await goToLogs();
const undoBtns = await screen.findAllByRole('button', { name: /Undo/i });
fireEvent.click(undoBtns[0]);
await waitFor(() => {
const und = getCalls().find(c => c.fn === 'updateDoc' && c.path.endsWith(`/logs/${logId}`) && c.data.undone === true);
return und;
});
const markUndone = getCalls().find(c => c.fn === 'updateDoc' && c.path.endsWith(`/logs/${logId}`));
expect(markUndone.data.undone).toBe(true);
// encounter path updated with undo payload (any encounter update after undo click)
const encUndo = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/'));
expect(encUndo.length).toBeGreaterThan(0);
});
});
describe('DeathSave -> Firebase', () => {
test('first death save: updateDoc increments deathSaves', async () => {
const { renderApp, createCampaignViaUI, selectCampaignByName, createEncounterViaUI, selectEncounterByName, getParticipantForm, startCombatViaUI } = require('./testHelpers');
const { within } = require('@testing-library/react');
await renderApp();
await createCampaignViaUI('DSC2');
await selectCampaignByName('DSC2');
fireEvent.change(screen.getByPlaceholderText('Character name'), { target: { value: 'Hero' } });
fireEvent.change(screen.getByLabelText(/Default HP/i), { target: { value: '10' } });
fireEvent.click(screen.getByRole('button', { name: /Add Character/i }));
await waitFor(() => {
const c = getCalls().find(c => c.fn === 'updateDoc' && c.path.includes('/campaigns/') && c.data.players?.length === 1);
return c;
});
const charId = getCalls().find(c => c.fn === 'updateDoc' && c.path.includes('/campaigns/') && c.data.players?.length === 1).data.players[0].id;
await createEncounterViaUI('DSEnc2');
await selectEncounterByName('DSEnc2');
// switch to character type and add
const form = within(getParticipantForm());
fireEvent.change(form.getByDisplayValue('Monster'), { target: { value: 'character' } });
fireEvent.change(form.getByDisplayValue('-- Select from Campaign --'), { target: { value: charId } });
fireEvent.click(form.getByRole('button', { name: /Add to Encounter/i }));
await waitFor(() => lastEncCall()?.data?.participants?.[0]?.name === 'Hero');
await startCombatViaUI();
// damage to 0
fireEvent.change(screen.getByPlaceholderText('HP'), { target: { value: '10' } });
fireEvent.click(screen.getByTitle('Damage'));
await waitFor(() => lastEncCall()?.data?.participants?.[0]?.currentHp === 0);
// death save buttons appear
const save1 = screen.getByTitle('Death save 1');
fireEvent.click(save1);
await waitFor(() => lastEncCall()?.data?.participants?.[0]?.deathSaves === 1);
expect(lastEncCall().data.participants[0].deathSaves).toBe(1);
});
test('third death save: marks isDying true', async () => {
const { renderApp, createCampaignViaUI, selectCampaignByName, createEncounterViaUI, selectEncounterByName, getParticipantForm } = require('./testHelpers');
const { within } = require('@testing-library/react');
await renderApp();
await createCampaignViaUI('DSDie');
await selectCampaignByName('DSDie');
fireEvent.change(screen.getByPlaceholderText('Character name'), { target: { value: 'Martyr' } });
fireEvent.change(screen.getByLabelText(/Default HP/i), { target: { value: '10' } });
fireEvent.click(screen.getByRole('button', { name: /Add Character/i }));
await waitFor(() => {
const c = getCalls().find(c => c.fn === 'updateDoc' && c.path.includes('/campaigns/') && c.data.players?.length === 1);
return c;
});
const charId = getCalls().find(c => c.fn === 'updateDoc' && c.path.includes('/campaigns/') && c.data.players?.length === 1).data.players[0].id;
await createEncounterViaUI('DSEncDie');
await selectEncounterByName('DSEncDie');
const form = within(getParticipantForm());
fireEvent.change(form.getByDisplayValue('Monster'), { target: { value: 'character' } });
fireEvent.change(form.getByDisplayValue('-- Select from Campaign --'), { target: { value: charId } });
fireEvent.click(form.getByRole('button', { name: /Add to Encounter/i }));
await waitFor(() => lastEncCall()?.data?.participants?.[0]?.name === 'Martyr');
await startCombatViaUI();
fireEvent.change(screen.getByPlaceholderText('HP'), { target: { value: '10' } });
fireEvent.click(screen.getByTitle('Damage'));
await waitFor(() => lastEncCall()?.data?.participants?.[0]?.currentHp === 0);
fireEvent.click(screen.getByTitle('Death save 1'));
await waitFor(() => lastEncCall()?.data?.participants?.[0]?.deathSaves === 1);
fireEvent.click(screen.getByTitle('Death save 2'));
await waitFor(() => lastEncCall()?.data?.participants?.[0]?.deathSaves === 2);
fireEvent.click(screen.getByTitle('Death save 3'));
await waitFor(() => lastEncCall()?.data?.participants?.[0]?.isDying === true);
expect(lastEncCall().data.participants[0].isDying).toBe(true);
expect(lastEncCall().data.participants[0].deathSaves).toBe(3);
});
});