Files
vu/tests/ui/shell.test.ts
T
will 63fd319509
Deploy / deploy (push) Successful in 1m19s
Fix Vespers/Compline header to use evening day, not daytime day
day-nav.ts's header always resolved via resolveDay(date) regardless of
selected hour, so viewing Vespers/Compline on a day whose evening
anticipates tomorrow's First Vespers (e.g. Aug 25, 2026: St. Louis's day,
but St. Zephyrinus claims First Vespers per calendar/vespers.ts) still
showed today's saint in the header while the hour's actual content had
already switched to tomorrow's. Now uses resolveEveningDay for those two
hours specifically.

Also adds an explicit "First Vespers (of tomorrow)" / "Second Vespers (of
today)" status line to the Vespers header, via new
getVespersStatusLabel(), so the anticipation/commemoration result is
visible rather than only inferable from which saint's name shows up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 07:02:21 -04:00

125 lines
5.2 KiB
TypeScript

import { beforeEach, describe, expect, it } from 'vitest';
import { mountShell } from '../../src/ui/shell';
import { setDate, setSelectedHour, setLanguages } from '../../src/app-state';
describe('shell', () => {
// app-state is a module-level singleton, so each test must reset every
// field it might have mutated — not just the one it cares about — or
// state leaks across tests in this file.
beforeEach(() => {
document.body.innerHTML = '<div id="app"></div>';
setDate('2026-08-09');
setSelectedHour('prime');
setLanguages(['en']);
window.history.replaceState(null, '', '/vu/2026-08-09/prime');
});
it('renders the day nav, all 8 hours, and Prime content by default', () => {
const root = document.getElementById('app')!;
mountShell(root);
expect(root.querySelector('.day-nav-date')?.textContent).toContain('August 9, 2026');
// The "day being celebrated" text now lives in the merged day-nav
// header, not repeated inside each hour's own view.
expect(root.querySelector('.day-nav-label')?.textContent).toBeTruthy();
expect(root.querySelector('.hour-view .day-label')).toBeNull();
const hourNames = Array.from(root.querySelectorAll('.hour-name')).map((el) => el.textContent);
expect(hourNames).toEqual([
'Matins',
'Lauds',
'Prime',
'Terce',
'Sext',
'None',
'Vespers',
'Compline',
]);
expect(root.querySelector('.hour-item.is-selected .hour-name')?.textContent).toBe('Prime');
expect(root.querySelector('.hour-view h2')?.textContent).toBe('Prime');
expect(root.querySelectorAll('.ordo-part-psalm').length).toBeGreaterThan(0);
});
it('shows tomorrow\'s saint (not today\'s) in the header when Vespers/Compline anticipate First Vespers', () => {
// Live-verified case (calendar/vespers.ts): Aug 25, 2026 is St. Louis
// (Simplex), but St. Zephyrinus (Aug 26, also Simplex) claims First
// Vespers over him -- the evening's real identity is tomorrow's, not
// today's. The header previously always used the daytime `resolveDay`
// regardless of selected hour, so it kept showing St. Louis even while
// the hour content itself had already switched to St. Zephyrinus.
setDate('2026-08-25');
setSelectedHour('vespers');
window.history.replaceState(null, '', '/vu/2026-08-25/vespers');
const root = document.getElementById('app')!;
mountShell(root);
expect(root.querySelector('.day-nav-label')?.textContent).toContain('Zephyrinus');
expect(root.querySelector('.day-nav-label')?.textContent).not.toContain('Louis');
expect(root.querySelector('.day-nav-vespers-status')?.textContent).toContain('First Vespers');
});
it('shows a plain Second Vespers status when Vespers is not anticipating tomorrow', () => {
setDate('2026-08-09');
setSelectedHour('vespers');
window.history.replaceState(null, '', '/vu/2026-08-09/vespers');
const root = document.getElementById('app')!;
mountShell(root);
expect(root.querySelector('.day-nav-vespers-status')?.textContent).toContain('Second Vespers');
});
it('switches to Matins and shows its real content, not the pending message', () => {
const root = document.getElementById('app')!;
mountShell(root);
const matinsBtn = Array.from(root.querySelectorAll<HTMLButtonElement>('[data-hour]')).find(
(btn) => btn.dataset.hour === 'matins',
);
matinsBtn?.click();
expect(root.querySelector('.hour-view h2')?.textContent).toBe('Matins');
expect(root.querySelector('.hour-view-pending')).toBeNull();
// Ferial psalmody always resolves regardless of date (unauthored
// reading content is the only genuinely date-gated part) — see
// hours/matins.ts.
expect(root.querySelectorAll('.ordo-part-psalm').length).toBeGreaterThan(0);
});
it('switches to Compline and shows its real content, not the pending message', () => {
const root = document.getElementById('app')!;
mountShell(root);
const complineBtn = Array.from(root.querySelectorAll<HTMLButtonElement>('[data-hour]')).find(
(btn) => btn.dataset.hour === 'compline',
);
complineBtn?.click();
expect(root.querySelector('.hour-view h2')?.textContent).toBe('Compline');
expect(root.querySelector('.hour-view-pending')).toBeNull();
expect(root.querySelectorAll('.ordo-part-psalm').length).toBe(3);
});
it('moving to the next day updates the displayed date', () => {
const root = document.getElementById('app')!;
mountShell(root);
root.querySelector<HTMLButtonElement>('[data-action="next"]')?.click();
expect(root.querySelector('.day-nav-date')?.textContent).toContain('August 10, 2026');
});
it('switching to two-language mode renders two columns per verse', () => {
const root = document.getElementById('app')!;
mountShell(root);
const bilingualBtn = Array.from(root.querySelectorAll<HTMLButtonElement>('[data-langs]')).find(
(btn) => btn.dataset.langs === 'en,la',
);
bilingualBtn?.click();
const firstVerseColumns = root.querySelector('.psalm-verse .lang-columns');
expect(firstVerseColumns?.classList.contains('lang-columns-2')).toBe(true);
expect(firstVerseColumns?.querySelectorAll('.lang-column').length).toBe(2);
});
});