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 }); + }); +});