5ae1f3a3e0
Deploy / deploy (push) Successful in 1m29s
getVespersStatusLabel detected First-Vespers anticipation only via winner.vespersFrom, a tag only ever set for a sanctoral tomorrow-winner (tagVespersFrom). When Vespers anticipates a Sunday that wins its own day outright (a temporal winner, e.g. 2026-08-29 anticipating 2026-08-30 since the Decollation of St. John Baptist is below the duplex-2-classis floor that would let it keep its own evening), the tag never gets set and the status wrongly read "Second Vespers (of today)" even though the whole page was showing tomorrow's identity. Comparing the resolved day's date against the originally-requested date detects anticipation uniformly regardless of winner kind, so this drops the vespersFrom dependency entirely. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
143 lines
6.2 KiB
TypeScript
143 lines
6.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('shows First Vespers and the Sunday leading the header when Vespers anticipates a Sunday that wins outright (temporal, not sanctoral, winner)', () => {
|
|
// Regression test: getVespersStatusLabel used to detect anticipation
|
|
// only via a tag that's only ever set for a *sanctoral* tomorrow-
|
|
// winner (see the equivalent calendar-level test in
|
|
// tests/calendar/day-label.test.ts) -- 2026-08-29 (the Decollation of
|
|
// St. John Baptist, Duplex) anticipates 2026-08-30's ordinary Sunday,
|
|
// which wins outright over Ss. Felix and Adauctus (Simplex), so
|
|
// tomorrow's winner is `temporal`, not `sanctoral`.
|
|
setDate('2026-08-29');
|
|
setSelectedHour('vespers');
|
|
window.history.replaceState(null, '', '/vu/2026-08-29/vespers');
|
|
const root = document.getElementById('app')!;
|
|
mountShell(root);
|
|
|
|
expect(root.querySelector('.day-nav-label')?.textContent).toContain('13th Sunday after Trinity');
|
|
expect(root.querySelector('.day-nav-vespers-status')?.textContent).toContain('First 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);
|
|
});
|
|
});
|