M2: refactor DisplayView to storage adapter (red test first)

DisplayView missed in original M2 refactor — raw onSnapshot(doc(db,path))
survived. In ws/memory mode db is a stub sentinel, so raw SDK calls crash
('Expected first argument to collection() to be a CollectionReference...').
Reported by human testing player display after start combat.

TDD:
1. RED: DisplayView.characterization.test.js asserts adapter.subscribeDoc
   called for campaign + encounter + activeDisplay paths. Adapter recorder
   (getAdapterCalls/resetAdapterCalls in firebase.js) instruments subscribe
   calls — catches raw-SDK bypass that firebase mock alone cannot (mock db
   satisfies raw onSnapshot, hiding the bug).
2. Fix: 2 raw onSnapshot sites in DisplayView -> storage.subscribeDoc.
3. GREEN: 2 new tests pass, 116 total green.

Audit confirmed DisplayView was the ONLY remaining raw SDK site in App.js.
This commit is contained in:
david raistrick
2026-06-29 13:13:46 -04:00
parent 52866784b2
commit 84a8b78acd
3 changed files with 79 additions and 22 deletions
+60
View File
@@ -0,0 +1,60 @@
// DisplayView.characterization.test.js
// Lock DisplayView uses storage adapter (subscribeDoc), NOT raw SDK onSnapshot(doc(db)).
// Blind spot caught: M2 refactor missed DisplayView; raw SDK + ws stub db = crash.
// Test asserts adapter recorder shows subscribeDoc calls when player view boots.
import React from 'react';
import { render, waitFor } 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';
// Seed activeDisplay + campaign + encounter so DisplayView has data to subscribe to.
function seedActiveDisplay() {
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 });
MOCK_DB.set(activeDisplayPath, { activeCampaignId: 'c1', activeEncounterId: 'e1', hidePlayerHp: false });
}
describe('DisplayView characterization', () => {
beforeEach(() => {
window.history.replaceState({}, '', '/display');
global.alert = jest.fn();
window.open = jest.fn();
resetAdapterCalls();
});
afterEach(() => {
window.history.replaceState({}, '', '/');
});
test('DisplayView subscribes via adapter.subscribeDoc (not raw SDK)', async () => {
seedActiveDisplay();
render(<App />);
// wait for DisplayView to mount and attempt subscriptions
await waitFor(() => {
const subs = getAdapterCalls().filter(c => c.fn === 'subscribeDoc');
expect(subs.length).toBeGreaterThanOrEqual(1);
}, { timeout: 3000 });
// must subscribe to campaign doc (for background url) and encounter doc
const docSubs = getAdapterCalls().filter(c => c.fn === 'subscribeDoc').map(c => c.path);
expect(docSubs.some(p => p.includes('/campaigns/c1'))).toBe(true);
expect(docSubs.some(p => p.includes('/encounters/e1'))).toBe(true);
});
test('DisplayView also subscribes to activeDisplay status doc via adapter', async () => {
seedActiveDisplay();
render(<App />);
await waitFor(() => {
const subs = getAdapterCalls().filter(c => c.fn === 'subscribeDoc' && c.path.includes('activeDisplay'));
expect(subs.length).toBeGreaterThanOrEqual(1);
}, { timeout: 3000 });
});
});