tests: BUG-4 RED locked (hide-HP clobbers activeDisplay), add write recorder
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.
This commit is contained in:
@@ -86,10 +86,12 @@ export function createFirebaseStorage() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async setDoc(path, data, opts = {}) {
|
async setDoc(path, data, opts = {}) {
|
||||||
|
recordAdapterCall({ fn: 'setDoc', path, data, opts });
|
||||||
await setDoc(doc(db, path), data, opts.merge ? { merge: true } : undefined);
|
await setDoc(doc(db, path), data, opts.merge ? { merge: true } : undefined);
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateDoc(path, patch) {
|
async updateDoc(path, patch) {
|
||||||
|
recordAdapterCall({ fn: 'updateDoc', path, patch });
|
||||||
await updateDoc(doc(db, path), patch);
|
await updateDoc(doc(db, path), patch);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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(<App />);
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user