// 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, screen } 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(participants = []) { 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, round: 1, currentTurnParticipantId: participants[0]?.id || null }); MOCK_DB.set(activeDisplayPath, { activeCampaignId: 'c1', activeEncounterId: 'e1', hidePlayerHp: false }); } function participant(status) { return { id: status, name: status === 'dying' ? 'Rogue' : status, type: 'character', initiative: 18, maxHp: 160, currentHp: status === 'conscious' ? 10 : 0, isActive: true, status, deathSaveSuccesses: 0, deathSaveFailures: 0, conditions: status === 'stable' || status === 'dying' ? ['unconscious'] : [], }; } describe('DisplayView characterization', () => { beforeEach(() => { window.history.replaceState({}, '', '/display'); global.alert = jest.fn(); window.open = jest.fn(); resetAdapterCalls(); Element.prototype.scrollIntoView = jest.fn(); }); afterEach(() => { window.history.replaceState({}, '', '/'); }); test('DisplayView subscribes via adapter.subscribeDoc (not raw SDK)', async () => { seedActiveDisplay(); render(); // 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(); await waitFor(() => { const subs = getAdapterCalls().filter(c => c.fn === 'subscribeDoc' && c.path.includes('activeDisplay')); expect(subs.length).toBeGreaterThanOrEqual(1); }, { timeout: 3000 }); }); test('DisplayView shows Dying for dying character with Unconscious condition', async () => { seedActiveDisplay([participant('dying')]); render(); await waitFor(() => expect(screen.getByText('Rogue')).toBeInTheDocument()); expect(screen.getByText(/Dying/i)).toBeInTheDocument(); expect(screen.getAllByText(/Unconscious/i).length).toBeGreaterThan(0); }); test('DisplayView shows Stable as Unconscious, and Dead as Dead', async () => { seedActiveDisplay([participant('stable'), participant('dead')]); render(); await waitFor(() => expect(screen.getByText('stable')).toBeInTheDocument()); expect(screen.getAllByText(/Unconscious/i).length).toBeGreaterThan(0); expect(screen.queryByText('(Stable)')).not.toBeInTheDocument(); expect(screen.getAllByText(/Dead/i).length).toBeGreaterThan(0); }); test('DisplayView hides inactive participants for all types after fade-out', async () => { seedActiveDisplay([ { ...participant('conscious'), id: 'active-pc', name: 'Active PC', isActive: true }, { ...participant('conscious'), id: 'inactive-pc', name: 'Inactive PC', isActive: false }, { id: 'inactive-monster', name: 'Inactive Monster', type: 'monster', initiative: 10, maxHp: 10, currentHp: 10, isActive: false, status: 'conscious', conditions: [] }, ]); render(); await waitFor(() => expect(screen.getByText('Active PC')).toBeInTheDocument()); // inactive held during exit animation, removed after transition await waitFor(() => expect(screen.queryByText('Inactive PC')).not.toBeInTheDocument(), { timeout: 1500 }); expect(screen.queryByText('Inactive Monster')).not.toBeInTheDocument(); }); });