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