fix(BUG-4): updateDoc patch on activeDisplay (not setDoc replace)

All 5 storage.setDoc(activeDisplay, {...}, {merge:true}) →
storage.updateDoc(activeDisplay, {...}).

setDoc merge:true worked in prod (firebase honors merge) but ws adapter
+ mock ignore opts arg entirely → clobbers doc. updateDoc uses PATCH
across all adapters (firebase real updateDoc, ws PATCH endpoint, mock
merge). Consistent, no clobber.

Sites fixed:
- hidePlayerHp toggle
- startEncounter (set active ids)
- endEncounter (null active ids)
- deactivate active display
- activate new display

TDD: HideHpToggle.test RED first (assert updateDoc patch, impl still
setDoc → 0 calls found). GREEN after switch.

Char tests updated: Encounter.characterization (2) + Combat.characterization
(2) assert updateDoc on activeDisplay, not setDoc.

BUG-4: prod was already fixed (merge:true), test was RED due to mock
ignoring opts. Now all 3 adapters consistent via updateDoc.
This commit is contained in:
david raistrick
2026-07-01 23:32:23 -04:00
parent d00cc104c9
commit c0998da0a7
5 changed files with 37 additions and 30 deletions
+9 -9
View File
@@ -42,7 +42,7 @@ describe('Encounter -> Firebase', () => {
expect(call.path).toMatch(/campaigns\/[^/]+\/encounters\//);
});
test('togglePlayerDisplay: setDoc merge on activeDisplay/status', async () => {
test('togglePlayerDisplay: updateDoc patch on activeDisplay/status', async () => {
await setupCampaignAndEncounter('Camp D', 'Enc D');
await selectEncounterByName('Enc D');
@@ -50,33 +50,33 @@ describe('Encounter -> Firebase', () => {
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
await waitFor(() => findCall('updateDoc', 'activeDisplay/status'));
const call = findCall('updateDoc', 'activeDisplay/status');
// BUG-4 fix: updateDoc patch, not setDoc replace (was clobbering fields)
expect(call.data).toMatchObject({
activeCampaignId: expect.any(String),
activeEncounterId: expect.any(String),
});
});
test('togglePlayerDisplay off: setDoc nulls active ids', async () => {
test('togglePlayerDisplay off: updateDoc 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'));
await waitFor(() => findCall('updateDoc', '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 calls = findCalls('updateDoc', 'activeDisplay/status');
const last = calls[calls.length - 1];
return last.data.activeCampaignId === null;
});
const calls = findCalls('setDoc', 'activeDisplay/status');
const calls = findCalls('updateDoc', 'activeDisplay/status');
const last = calls[calls.length - 1];
expect(last.data).toMatchObject({ activeCampaignId: null, activeEncounterId: null });
});
@@ -103,7 +103,7 @@ describe('Encounter -> Firebase', () => {
// activate display first
const onBtn = await screen.findByTitle('Activate for Player Display');
fireEvent.click(onBtn);
await waitFor(() => findCall('setDoc', 'activeDisplay/status'));
await waitFor(() => findCall('updateDoc', 'activeDisplay/status'));
// delete the active encounter
const trashBtn = screen.getAllByTitle('Delete Encounter')[0];