From d979b03f2e74ab15565bde10d588891d9faf17c7 Mon Sep 17 00:00:00 2001 From: david raistrick <1108844+keen99@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:55:14 -0400 Subject: [PATCH] tests: BUG-4 RED locked (hide-HP clobbers activeDisplay), add write recorder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/tests/HideHpToggle.test.js: renders App, selects campaign, toggles hide-player-HP switch, asserts setDoc data includes activeCampaignId + activeEncounterId. RED: data only {hidePlayerHp:true}, both clobbered. Root cause proven with evidence (recorder): setDoc(activeDisplay/status, {hidePlayerHp:true}, {merge:true}) data written = {hidePlayerHp:true} ONLY activeCampaignId = undefined activeEncounterId = undefined setDoc = replace per contract. {merge:true} arg ignored. Toggle wipes encounter pointer → DisplayView reads null → 'Game Session Paused'. Fix: use updateDoc (patch), not setDoc. src/storage/firebase.js: adapter recorder now captures setDoc + updateDoc (data + opts). Was subscribe-only. Enables write-path assertions. --- src/storage/firebase.js | 2 ++ src/tests/HideHpToggle.test.js | 63 ++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/tests/HideHpToggle.test.js diff --git a/src/storage/firebase.js b/src/storage/firebase.js index eafcd96..2dc2047 100644 --- a/src/storage/firebase.js +++ b/src/storage/firebase.js @@ -86,10 +86,12 @@ export function createFirebaseStorage() { }, async setDoc(path, data, opts = {}) { + recordAdapterCall({ fn: 'setDoc', path, data, opts }); await setDoc(doc(db, path), data, opts.merge ? { merge: true } : undefined); }, async updateDoc(path, patch) { + recordAdapterCall({ fn: 'updateDoc', path, patch }); await updateDoc(doc(db, path), patch); }, diff --git a/src/tests/HideHpToggle.test.js b/src/tests/HideHpToggle.test.js new file mode 100644 index 0000000..0e44614 --- /dev/null +++ b/src/tests/HideHpToggle.test.js @@ -0,0 +1,63 @@ +// BUG-4 repro: toggling hidePlayerHp must not clobber activeDisplay doc. +// setDoc = replace (contract). {merge:true} arg ignored. +// Toggling hide-HP writes {hidePlayerHp:X} alone → activeCampaignId + activeEncounterId → null. +// Display reads null → "Game Session Paused". Recover requires re-activating encounter. +// Fix: use updateDoc (patch), not setDoc. + +import React from 'react'; +import { render, waitFor, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import App from '../App'; +import { MOCK_DB } from '../__mocks__/firebase/_mock-db'; +import { getAdapterCalls, resetAdapterCalls } from '../storage/firebase'; +import { selectCampaignByName } from './testHelpers'; + +function seedAdminWithActiveEncounter() { + const campaignPath = 'artifacts/ttrpg-initiative-tracker-default/public/data/campaigns/c1'; + const encounterPath = 'artifacts/ttrpg-initiative-tracker-default/public/data/campaigns/c1/encounters/e1'; + const activeDisplayPath = 'artifacts/ttrpg-initiative-tracker-default/public/data/activeDisplay/status'; + + MOCK_DB.set(campaignPath, { name: 'Camp', playerDisplayBackgroundUrl: '' }); + MOCK_DB.set(encounterPath, { name: 'Enc', participants: [], isStarted: true }); + // active encounter set, HP NOT hidden + MOCK_DB.set(activeDisplayPath, { + activeCampaignId: 'c1', + activeEncounterId: 'e1', + hidePlayerHp: false, + }); +} + +describe('BUG-4: hide-player-HP toggle preserves activeDisplay', () => { + beforeEach(() => { + window.history.replaceState({}, '', '/'); + global.alert = jest.fn(); + resetAdapterCalls(); + }); + + test('toggling hidePlayerHp does NOT clear activeCampaignId/activeEncounterId', async () => { + seedAdminWithActiveEncounter(); + render(); + + // wait for admin to mount + load active display + await waitFor(() => screen.getByText('Camp'), { timeout: 3000 }); + await selectCampaignByName('Camp'); + + // find the hide-player-HP toggle (role switch) + const toggle = await screen.findByRole('switch', { name: /hide/i }, { timeout: 3000 }); + + // toggle ON + fireEvent.click(toggle); + + await waitFor(() => { + const writes = getAdapterCalls().filter( + c => c.fn === 'setDoc' && c.path.includes('activeDisplay/status') + ); + expect(writes.length).toBeGreaterThan(0); + const last = writes[writes.length - 1]; + // data written must include activeCampaignId AND activeEncounterId + // BUG: writes only {hidePlayerHp:true}, clobbering them. + expect(last.data.activeCampaignId).toBe('c1'); + expect(last.data.activeEncounterId).toBe('e1'); + }, { timeout: 3000 }); + }); +});