Rework backend #1

Merged
robert merged 86 commits from rework-backend into main 2026-07-01 19:29:34 -04:00
3 changed files with 79 additions and 22 deletions
Showing only changes of commit 84a8b78acd - Show all commits
+10 -22
View File
@@ -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 {
+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 });
});
});
+9
View File
@@ -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);