From 0c1196aee106c5385f442791cfe923e84ceb60e2 Mon Sep 17 00:00:00 2001 From: david raistrick <1108844+keen99@users.noreply.github.com> Date: Sun, 28 Jun 2026 18:30:57 -0400 Subject: [PATCH] test: encounter characterization (6 tests) - Encounter.characterization.test.js: createEncounter, path nesting, togglePlayerDisplay on/off, deleteEncounter + clears activeDisplay - testHelpers.js: createEncounterViaUI, selectEncounterByName Locks encounter write paths + payload shapes. --- src/Encounter.characterization.test.js | 122 +++++++++++++++++++++++++ src/testHelpers.js | 19 ++++ 2 files changed, 141 insertions(+) create mode 100644 src/Encounter.characterization.test.js diff --git a/src/Encounter.characterization.test.js b/src/Encounter.characterization.test.js new file mode 100644 index 0000000..7fca5a3 --- /dev/null +++ b/src/Encounter.characterization.test.js @@ -0,0 +1,122 @@ +// Encounter characterization. Lock setDoc path + payload on encounter actions. + +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 { renderApp, createCampaignViaUI, selectCampaignByName, createEncounterViaUI, selectEncounterByName } from './testHelpers'; + +function findCall(fn, pathSub) { + return getCalls().find(c => c.fn === fn && (pathSub ? c.path.includes(pathSub) : true)); +} +function findCalls(fn, pathSub) { + return getCalls().filter(c => c.fn === fn && (pathSub ? c.path.includes(pathSub) : true)); +} + +async function setupCampaignAndEncounter(campName, encName) { + await renderApp(); + await createCampaignViaUI(campName); + await selectCampaignByName(campName); + await createEncounterViaUI(encName); +} + +describe('Encounter -> Firebase', () => { + test('createEncounter: setDoc with encounter path + payload', async () => { + await setupCampaignAndEncounter('Camp E', 'Boss Fight'); + const call = findCall('setDoc', '/encounters/'); + expect(call.path).toMatch(/encounters\/.+$/); + expect(call.data).toMatchObject({ + name: 'Boss Fight', + participants: [], + round: 0, + currentTurnParticipantId: null, + isStarted: false, + isPaused: false, + }); + expect(call.data).toHaveProperty('createdAt'); + }); + + test('createEncounter: path nested under campaign', async () => { + await setupCampaignAndEncounter('Camp N', 'Enc N'); + const call = findCall('setDoc', '/encounters/'); + expect(call.path).toMatch(/campaigns\/[^/]+\/encounters\//); + }); + + test('togglePlayerDisplay: setDoc merge on activeDisplay/status', async () => { + await setupCampaignAndEncounter('Camp D', 'Enc D'); + await selectEncounterByName('Enc D'); + + // Eye button (icon-only, title attr) + const eyeBtn = await screen.findByTitle('Activate for Player Display'); + fireEvent.click(eyeBtn); + + await waitFor(() => findCall('setDoc', 'activeDisplay/status')); + const call = findCall('setDoc', 'activeDisplay/status'); + // activeDisplay/status setDoc is called with merge option in App + expect(call.data).toMatchObject({ + activeCampaignId: expect.any(String), + activeEncounterId: expect.any(String), + }); + }); + + test('togglePlayerDisplay off: setDoc nulls active ids', async () => { + await setupCampaignAndEncounter('Camp O', 'Enc O'); + await selectEncounterByName('Enc O'); + + // turn ON + const onBtn = await screen.findByTitle('Activate for Player Display'); + fireEvent.click(onBtn); + await waitFor(() => findCall('setDoc', 'activeDisplay/status')); + + // turn OFF + const offBtn = await screen.findByTitle('Deactivate for Player Display'); + fireEvent.click(offBtn); + await waitFor(() => { + const calls = findCalls('setDoc', 'activeDisplay/status'); + const last = calls[calls.length - 1]; + return last.data.activeCampaignId === null; + }); + const calls = findCalls('setDoc', 'activeDisplay/status'); + const last = calls[calls.length - 1]; + expect(last.data).toMatchObject({ activeCampaignId: null, activeEncounterId: null }); + }); + + test('deleteEncounter: deleteDoc on encounter path', async () => { + await setupCampaignAndEncounter('Camp X', 'Enc X'); + await selectEncounterByName('Enc X'); + + // trash icon on encounter card + const trashBtn = screen.getAllByTitle('Delete Encounter')[0]; + fireEvent.click(trashBtn); + // confirm modal + fireEvent.click(await screen.findByRole('button', { name: /Confirm/i })); + + await waitFor(() => findCall('deleteDoc', '/encounters/')); + const del = findCall('deleteDoc', '/encounters/'); + expect(del.path).toMatch(/campaigns\/[^/]+\/encounters\//); + }); + + test('deleteEncounter clears activeDisplay if it was active', async () => { + await setupCampaignAndEncounter('Camp A', 'Enc A'); + await selectEncounterByName('Enc A'); + + // activate display first + const onBtn = await screen.findByTitle('Activate for Player Display'); + fireEvent.click(onBtn); + await waitFor(() => findCall('setDoc', 'activeDisplay/status')); + + // delete the active encounter + const trashBtn = screen.getAllByTitle('Delete Encounter')[0]; + fireEvent.click(trashBtn); + fireEvent.click(await screen.findByRole('button', { name: /Confirm/i })); + + await waitFor(() => { + const adCalls = findCalls('updateDoc', 'activeDisplay/status'); + const last = adCalls[adCalls.length - 1]; + return last.data.activeEncounterId === null; + }); + const adCalls = findCalls('updateDoc', 'activeDisplay/status'); + const last = adCalls[adCalls.length - 1]; + expect(last.data).toMatchObject({ activeCampaignId: null, activeEncounterId: null }); + }); +}); diff --git a/src/testHelpers.js b/src/testHelpers.js index a84388e..86a420e 100644 --- a/src/testHelpers.js +++ b/src/testHelpers.js @@ -34,4 +34,23 @@ export async function selectCampaignByName(name) { await waitFor(() => screen.getByText(/Managing:/i)); } +// Open create-encounter modal, fill name, submit. Assumes campaign selected. +export async function createEncounterViaUI(name = 'Test Encounter') { + fireEvent.click(screen.getByRole('button', { name: /Create Encounter/i })); + await waitFor(() => screen.getByLabelText(/Encounter Name/i)); + fireEvent.change(screen.getByLabelText(/Encounter Name/i), { target: { value: name } }); + fireEvent.click(screen.getByRole('button', { name: /^Create$/i })); + const { getCalls } = require('./__mocks__/firebase/_mock-db'); + await waitFor(() => getCalls().find(c => c.fn === 'setDoc' && c.path.includes('/encounters/'))); + const call = getCalls().find(c => c.fn === 'setDoc' && c.path.includes('/encounters/')); + return call.path.split('/').pop(); +} + +// Click encounter card by name. Assumes campaign selected. +export async function selectEncounterByName(name) { + const card = await waitFor(() => screen.getByText(name)); + fireEvent.click(card); + await waitFor(() => screen.getByText(/Managing Encounter:/i)); +} + export { MOCK_DB };