Chrome Android forces the root URL into PWA installation when required manifest icon files exist. PWA names come from the manifest and cannot be edited, which breaks workflows using multiple tracker copies. Keep required 192/512 icon assets intentionally unavailable by committing generated files with .png-not suffixes. Removing the manifest link does not restore root shortcut naming; Chrome still installs the root as a PWA. Chrome handles /display differently: even with a working manifest it offers Install app or Create shortcut, and Create shortcut permits renaming. Keep both manifests intentionally non-installable for consistent user-controlled shortcut names. Also add separate player-display favicon/logo artwork and swap /display browser favicon to player-favicon.ico. Keep generated standard/player logo sources under renamed inert filenames.
124 lines
5.2 KiB
JavaScript
124 lines
5.2 KiB
JavaScript
// 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(() => {
|
|
const favicon = document.createElement('link');
|
|
favicon.rel = 'icon';
|
|
favicon.href = '/favicon.ico';
|
|
document.head.appendChild(favicon);
|
|
window.history.replaceState({}, '', '/display');
|
|
global.alert = jest.fn();
|
|
window.open = jest.fn();
|
|
resetAdapterCalls();
|
|
Element.prototype.scrollIntoView = jest.fn();
|
|
});
|
|
afterEach(() => {
|
|
document.querySelectorAll('link[rel="icon"]').forEach(link => link.remove());
|
|
window.history.replaceState({}, '', '/');
|
|
});
|
|
|
|
test('display route swaps browser favicon to player icon', async () => {
|
|
seedActiveDisplay();
|
|
render(<App />);
|
|
await waitFor(() => {
|
|
expect(document.querySelector('link[rel="icon"]')).toHaveAttribute('href', '/player-favicon.ico');
|
|
});
|
|
});
|
|
|
|
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 });
|
|
});
|
|
|
|
test('DisplayView shows Dying for dying character with Unconscious condition', async () => {
|
|
seedActiveDisplay([participant('dying')]);
|
|
render(<App />);
|
|
|
|
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(<App />);
|
|
|
|
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(<App />);
|
|
|
|
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();
|
|
});
|
|
});
|