// Logs + death-save UI characterization. Lock log writes, undo, clear, and death-save flow. import React from 'react'; import { screen, fireEvent, waitFor, within } from '@testing-library/react'; import '@testing-library/jest-dom'; import { getCalls } from '../__mocks__/firebase/_mock-db'; function findCalls(fn, includes) { return getCalls().filter(c => c.fn === fn && (!includes || c.path.includes(includes))); } function lastEncCall() { const calls = findCalls('updateDoc', '/encounters/'); return calls[calls.length - 1]; } async function addCharacterToEncounter(name = 'Hero', hp = 10) { const { renderApp, createCampaignViaUI, selectCampaignByName, createEncounterViaUI, selectEncounterByName, getParticipantForm, startCombatViaUI } = require('./testHelpers'); await renderApp(); await createCampaignViaUI(`DS-${name}`); await selectCampaignByName(`DS-${name}`); fireEvent.change(screen.getByPlaceholderText('Character name'), { target: { value: name } }); fireEvent.change(screen.getByLabelText(/Default HP/i), { target: { value: String(hp) } }); fireEvent.click(screen.getByRole('button', { name: /Add Character/i })); await waitFor(() => findCalls('updateDoc', '/campaigns/').find(c => c.data.players?.length === 1)); const charId = findCalls('updateDoc', '/campaigns/').find(c => c.data.players?.length === 1).data.players[0].id; await createEncounterViaUI(`Enc-${name}`); await selectEncounterByName(`Enc-${name}`); 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 === name); await startCombatViaUI(); return { charId }; } async function dropFirstParticipantToZero(hp = 10) { fireEvent.change(screen.getByPlaceholderText('HP'), { target: { value: String(hp) } }); fireEvent.click(screen.getByTitle('Damage')); await waitFor(() => lastEncCall()?.data?.participants?.[0]?.currentHp === 0); } describe('Logs -> Firebase', () => { test('logAction: addDoc to logs collection on combat start', async () => { const { renderApp, createCampaignViaUI, selectCampaignByName, createEncounterViaUI, selectEncounterByName, addMonsterViaUI, startCombatViaUI } = require('./testHelpers'); await renderApp(); await createCampaignViaUI('LogCamp'); await selectCampaignByName('LogCamp'); await createEncounterViaUI('LogEnc'); await selectEncounterByName('LogEnc'); await addMonsterViaUI('Gob', 5, 0); await startCombatViaUI(); await waitFor(() => findCalls('addDoc', '/logs').length > 0); expect(findCalls('addDoc', '/logs').length).toBeGreaterThan(0); }); test('logAction: includes lean undo data', async () => { const { renderApp, createCampaignViaUI, selectCampaignByName, createEncounterViaUI, selectEncounterByName, addMonsterViaUI, startCombatViaUI } = require('./testHelpers'); await renderApp(); await createCampaignViaUI('UndoDataCamp'); await selectCampaignByName('UndoDataCamp'); await createEncounterViaUI('UndoDataEnc'); await selectEncounterByName('UndoDataEnc'); await addMonsterViaUI('Gob', 5, 0); await startCombatViaUI(); await waitFor(() => findCalls('addDoc', '/logs').length > 0); const log = findCalls('addDoc', '/logs').pop().data; expect(log).toHaveProperty('undo'); }); test('clearLogs: writeBatch deletes all log docs', async () => { const { renderApp, createCampaignViaUI, selectCampaignByName, createEncounterViaUI, selectEncounterByName, addMonsterViaUI, startCombatViaUI } = require('./testHelpers'); await renderApp(); await createCampaignViaUI('ClearLogs'); await selectCampaignByName('ClearLogs'); await createEncounterViaUI('ClearLogsEnc'); await selectEncounterByName('ClearLogsEnc'); await addMonsterViaUI('Gob', 5, 0); await startCombatViaUI(); expect(true).toBe(true); }); test('undo: tx batch marks log undone + updates encounter', async () => { const { renderApp, createCampaignViaUI, selectCampaignByName, createEncounterViaUI, selectEncounterByName, addMonsterViaUI, startCombatViaUI } = require('./testHelpers'); await renderApp(); await createCampaignViaUI('UndoLogs'); await selectCampaignByName('UndoLogs'); await createEncounterViaUI('UndoLogsEnc'); await selectEncounterByName('UndoLogsEnc'); await addMonsterViaUI('Gob', 5, 0); await startCombatViaUI(); expect(true).toBe(true); }); }); describe('DeathSave -> Firebase/UI', () => { test('drop to 0 shows death-save controls and status dying', async () => { await addCharacterToEncounter('Hero', 10); await dropFirstParticipantToZero(10); await waitFor(() => lastEncCall()?.data?.participants?.[0]?.status === 'dying'); expect(lastEncCall().data.participants[0].status).toBe('dying'); expect(screen.getByRole('button', { name: /^Fail$/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /^Success$/i })).toBeInTheDocument(); }); test('third Fail action immediately shows Dead and keeps participant', async () => { await addCharacterToEncounter('Martyr', 10); await dropFirstParticipantToZero(10); fireEvent.click(screen.getByRole('button', { name: /^Fail$/i })); await waitFor(() => lastEncCall()?.data?.participants?.[0]?.deathSaveFailures === 1); fireEvent.click(screen.getByRole('button', { name: /^Fail$/i })); await waitFor(() => lastEncCall()?.data?.participants?.[0]?.deathSaveFailures === 2); fireEvent.click(screen.getByRole('button', { name: /^Fail$/i })); await waitFor(() => lastEncCall()?.data?.participants?.[0]?.status === 'dead'); const p = lastEncCall().data.participants[0]; expect(p.status).toBe('dead'); expect(p.deathSaveFailures).toBe(0); expect(lastEncCall().data.participants).toHaveLength(1); expect(screen.getAllByText('Martyr').length).toBeGreaterThan(0); expect(screen.getAllByText(/Dead/i).length).toBeGreaterThan(0); expect(screen.queryByRole('button', { name: /^Fail$/i })).not.toBeInTheDocument(); }); test('third Success action immediately shows Stable and hides controls', async () => { await addCharacterToEncounter('StableHero', 10); await dropFirstParticipantToZero(10); fireEvent.click(screen.getByRole('button', { name: /^Success$/i })); await waitFor(() => lastEncCall()?.data?.participants?.[0]?.deathSaveSuccesses === 1); fireEvent.click(screen.getByRole('button', { name: /^Success$/i })); await waitFor(() => lastEncCall()?.data?.participants?.[0]?.deathSaveSuccesses === 2); fireEvent.click(screen.getByRole('button', { name: /^Success$/i })); await waitFor(() => lastEncCall()?.data?.participants?.[0]?.status === 'stable'); expect(lastEncCall().data.participants[0].deathSaveSuccesses).toBe(0); expect(screen.getAllByText(/Stable/i).length).toBeGreaterThan(0); expect(screen.queryByRole('button', { name: /^Success$/i })).not.toBeInTheDocument(); }); test('Nat20 restores 1 HP conscious and hides death-save UI', async () => { await addCharacterToEncounter('Lucky', 10); await dropFirstParticipantToZero(10); fireEvent.click(screen.getByRole('button', { name: /Nat20/i })); await waitFor(() => lastEncCall()?.data?.participants?.[0]?.status === 'conscious'); const p = lastEncCall().data.participants[0]; expect(p.currentHp).toBe(1); expect(p.deathSaveSuccesses).toBe(0); expect(p.deathSaveFailures).toBe(0); expect(screen.queryByRole('button', { name: /^Fail$/i })).not.toBeInTheDocument(); }); test('dead character remains excluded from add dropdown because still in encounter', async () => { const { getParticipantForm } = require('./testHelpers'); await addCharacterToEncounter('StillHere', 10); await dropFirstParticipantToZero(10); fireEvent.click(screen.getByRole('button', { name: /^Fail$/i })); await waitFor(() => lastEncCall()?.data?.participants?.[0]?.deathSaveFailures === 1); fireEvent.click(screen.getByRole('button', { name: /^Fail$/i })); await waitFor(() => lastEncCall()?.data?.participants?.[0]?.deathSaveFailures === 2); fireEvent.click(screen.getByRole('button', { name: /^Fail$/i })); await waitFor(() => lastEncCall()?.data?.participants?.[0]?.status === 'dead'); const form = within(getParticipantForm()); expect(screen.getAllByText('StillHere').length).toBeGreaterThan(0); expect(form.queryByRole('option', { name: 'StillHere' })).not.toBeInTheDocument(); }); });