Rework backend #1
+10
-22
@@ -2453,35 +2453,23 @@ function DisplayView() {
|
||||
setIsLoadingEncounter(true);
|
||||
setEncounterError(null);
|
||||
|
||||
const campaignDocRef = doc(db, getPath.campaign(activeCampaignId));
|
||||
unsubscribeCampaign = onSnapshot(
|
||||
campaignDocRef,
|
||||
(campSnap) => {
|
||||
if (campSnap.exists()) {
|
||||
setCampaignBackgroundUrl(campSnap.data().playerDisplayBackgroundUrl || '');
|
||||
} else {
|
||||
setCampaignBackgroundUrl('');
|
||||
}
|
||||
},
|
||||
(err) => console.error("Error fetching campaign background:", err)
|
||||
unsubscribeCampaign = storage.subscribeDoc(
|
||||
getPath.campaign(activeCampaignId),
|
||||
(camp) => {
|
||||
setCampaignBackgroundUrl((camp && camp.playerDisplayBackgroundUrl) || '');
|
||||
}
|
||||
);
|
||||
|
||||
const encounterPath = getPath.encounter(activeCampaignId, activeEncounterId);
|
||||
unsubscribeEncounter = onSnapshot(
|
||||
doc(db, encounterPath),
|
||||
(encDocSnap) => {
|
||||
if (encDocSnap.exists()) {
|
||||
setActiveEncounterData({ id: encDocSnap.id, ...encDocSnap.data() });
|
||||
unsubscribeEncounter = storage.subscribeDoc(
|
||||
getPath.encounter(activeCampaignId, activeEncounterId),
|
||||
(enc) => {
|
||||
if (enc) {
|
||||
setActiveEncounterData({ id: activeEncounterId, ...enc });
|
||||
} else {
|
||||
setActiveEncounterData(null);
|
||||
setEncounterError("Active encounter data not found.");
|
||||
}
|
||||
setIsLoadingEncounter(false);
|
||||
},
|
||||
(err) => {
|
||||
console.error("Error fetching active encounter details:", err);
|
||||
setEncounterError("Error loading active encounter data.");
|
||||
setIsLoadingEncounter(false);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,13 @@ import {
|
||||
onSnapshot, updateDoc, deleteDoc, query, orderBy, limit, writeBatch, serverTimestamp,
|
||||
} from 'firebase/firestore';
|
||||
|
||||
// Adapter call recorder (instrumentation, no behavior change).
|
||||
// Tests assert adapter.subscribeDoc called (catches raw-SDK bypass like DisplayView).
|
||||
const ADAPTER_CALLS = [];
|
||||
function recordAdapterCall(entry) { ADAPTER_CALLS.push({ ...entry, ts: Date.now() }); }
|
||||
export function getAdapterCalls() { return [...ADAPTER_CALLS]; }
|
||||
export function resetAdapterCalls() { ADAPTER_CALLS.length = 0; }
|
||||
|
||||
// Path helpers mirror App.js getPath object.
|
||||
const APP_ID = process.env.REACT_APP_TRACKER_APP_ID || 'ttrpg-initiative-tracker-default';
|
||||
const PUBLIC_DATA_PATH = `artifacts/${APP_ID}/public/data`;
|
||||
@@ -112,12 +119,14 @@ export function createFirebaseStorage() {
|
||||
|
||||
// Subscribe = onSnapshot. cb fires immediately + on change. Returns unsubscribe.
|
||||
subscribeDoc(path, cb) {
|
||||
recordAdapterCall({ fn: 'subscribeDoc', path });
|
||||
return onSnapshot(doc(db, path), (snap) => {
|
||||
cb(snap.exists() ? { id: snap.id, ...snap.data() } : null);
|
||||
}, (err) => console.error(`subscribeDoc ${path}:`, err));
|
||||
},
|
||||
|
||||
subscribeCollection(collectionPath, cb, queryConstraints = []) {
|
||||
recordAdapterCall({ fn: 'subscribeCollection', path: collectionPath });
|
||||
const q = queryConstraints.length > 0
|
||||
? query(collection(db, collectionPath), ...queryConstraints)
|
||||
: collection(db, collectionPath);
|
||||
|
||||
Reference in New Issue
Block a user