Compare commits
2 Commits
5e56611944
...
5ae1f3a3e0
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ae1f3a3e0 | |||
| 95901ef56a |
@@ -500,11 +500,17 @@ const TEMPORAL_CATEGORY_RANK_LABELS: Record<TemporalCategory, Bi> = {
|
|||||||
* silent about *why* a given identity governs tonight (a reader has to
|
* silent about *why* a given identity governs tonight (a reader has to
|
||||||
* already know, e.g., that St. Zephyrinus falls on tomorrow's date to
|
* already know, e.g., that St. Zephyrinus falls on tomorrow's date to
|
||||||
* realize this evening is anticipating him). Pass the result of
|
* realize this evening is anticipating him). Pass the result of
|
||||||
* `calendar/vespers.ts`'s `resolveEveningDay`, not a plain `resolveDay` —
|
* `calendar/vespers.ts`'s `resolveEveningDay`, not a plain `resolveDay`,
|
||||||
* only that carries the `vespersFrom` tag this reads.
|
* plus the originally-requested date — comparing the two directly (rather
|
||||||
|
* than checking `winner.vespersFrom`, which only ever gets set for a
|
||||||
|
* *sanctoral* tomorrow-winner, see `vespers.ts`'s `tagVespersFrom`) is
|
||||||
|
* what makes this correct even when tomorrow's winner is `temporal` (e.g.
|
||||||
|
* an ordinary Sunday winning outright over a merely-commemorated Simplex
|
||||||
|
* saint) — a case `winner.vespersFrom` can never detect since that field
|
||||||
|
* doesn't exist on the temporal variant of `DayWinner` at all.
|
||||||
*/
|
*/
|
||||||
export function getVespersStatusLabel(day: LiturgicalDay): Bi {
|
export function getVespersStatusLabel(day: LiturgicalDay, requestedDate: string): Bi {
|
||||||
if (day.winner.kind === 'sanctoral' && day.winner.vespersFrom === 'firstVespersOfTomorrow') {
|
if (day.date !== requestedDate) {
|
||||||
return bi('First Vespers (of tomorrow)', 'Vesperæ de sequenti');
|
return bi('First Vespers (of tomorrow)', 'Vesperæ de sequenti');
|
||||||
}
|
}
|
||||||
return bi('Second Vespers (of today)', 'Vesperæ de hodierno');
|
return bi('Second Vespers (of today)', 'Vesperæ de hodierno');
|
||||||
@@ -561,12 +567,12 @@ export function getDayLabel(day: LiturgicalDay): Partial<Record<string, string>>
|
|||||||
// anchorDayName's own doc comment — checked before the octave case
|
// anchorDayName's own doc comment — checked before the octave case
|
||||||
// below since Trinity Sunday, e.g., also happens to be day 8 of
|
// below since Trinity Sunday, e.g., also happens to be day 8 of
|
||||||
// Pentecost's octave, and the anchor name is what actually governs.
|
// Pentecost's octave, and the anchor name is what actually governs.
|
||||||
// Existing convention: a commemorated saint *leads* here, before the
|
// Winner first, same as every other branch here — the anchor day is
|
||||||
// anchor name — not "winner first" like every branch above.
|
// what's actually being celebrated even when a lesser saint rides along.
|
||||||
const anchorName = anchorDayName(day);
|
const anchorName = anchorDayName(day);
|
||||||
if (anchorName) {
|
if (anchorName) {
|
||||||
const { sanctoral } = collectCommemorations(day, { showOctaves: false });
|
const { sanctoral } = collectCommemorations(day, { showOctaves: false });
|
||||||
return joinBi([...sanctoral, anchorName]);
|
return joinBi([anchorName, ...sanctoral]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// An active octave (St. Lawrence's, ...) is this day's real primary
|
// An active octave (St. Lawrence's, ...) is this day's real primary
|
||||||
@@ -609,5 +615,5 @@ export function getDayLabel(day: LiturgicalDay): Partial<Record<string, string>>
|
|||||||
const label = temporalLabel(day);
|
const label = temporalLabel(day);
|
||||||
const temporal = bi(`${label.en} (${temporalRank.en})`, `${label.la} (${temporalRank.la})`);
|
const temporal = bi(`${label.en} (${temporalRank.en})`, `${label.la} (${temporalRank.la})`);
|
||||||
const { sanctoral } = collectCommemorations(day, { showOctaves: false });
|
const { sanctoral } = collectCommemorations(day, { showOctaves: false });
|
||||||
return joinBi([...sanctoral, temporal]);
|
return joinBi([temporal, ...sanctoral]);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ export function renderDayNav(container: HTMLElement): void {
|
|||||||
const day = isEveningHour ? resolveEveningDay(date) : resolveDay(date);
|
const day = isEveningHour ? resolveEveningDay(date) : resolveDay(date);
|
||||||
const dateLabel = formatDateLong(date);
|
const dateLabel = formatDateLong(date);
|
||||||
const dayLabel = getDayLabel(day);
|
const dayLabel = getDayLabel(day);
|
||||||
const vespersStatus = selectedHour === 'vespers' ? getVespersStatusLabel(day) : undefined;
|
const vespersStatus = selectedHour === 'vespers' ? getVespersStatusLabel(day, date) : undefined;
|
||||||
const prevUrl = urlFor(addDays(date, -1), selectedHour, languages);
|
const prevUrl = urlFor(addDays(date, -1), selectedHour, languages);
|
||||||
const nextUrl = urlFor(addDays(date, 1), selectedHour, languages);
|
const nextUrl = urlFor(addDays(date, 1), selectedHour, languages);
|
||||||
const todayUrl = urlFor(todayIso(), todayTargetHour(), languages);
|
const todayUrl = urlFor(todayIso(), todayTargetHour(), languages);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { resolveDay } from '../../src/calendar';
|
import { resolveDay } from '../../src/calendar';
|
||||||
import { getDayLabel } from '../../src/calendar/day-label';
|
import { resolveEveningDay } from '../../src/calendar/vespers';
|
||||||
|
import { getDayLabel, getVespersStatusLabel } from '../../src/calendar/day-label';
|
||||||
import type { LiturgicalDay } from '../../src/calendar/types';
|
import type { LiturgicalDay } from '../../src/calendar/types';
|
||||||
|
|
||||||
describe('getDayLabel — ordinal temporal label', () => {
|
describe('getDayLabel — ordinal temporal label', () => {
|
||||||
@@ -60,9 +61,9 @@ describe('getDayLabel — ordinal temporal label', () => {
|
|||||||
// the November sanctoral pull, he's correctly commemorated alongside
|
// the November sanctoral pull, he's correctly commemorated alongside
|
||||||
// Advent I rather than simply absent (confirmed against the live
|
// Advent I rather than simply absent (confirmed against the live
|
||||||
// reference engine, which shows the same pairing), so the label
|
// reference engine, which shows the same pairing), so the label
|
||||||
// reflects both, feast name first, same pattern as Trinity Sunday's
|
// reflects both, winner (Advent I, so privileged nothing can displace
|
||||||
// own St. Felix I case above.
|
// it) first, commemorated saint after.
|
||||||
expect(getDayLabel(resolveDay('2025-11-30')).en).toBe('St. Andrew, Apostle — The 1st Sunday of Advent (Semiduplex)');
|
expect(getDayLabel(resolveDay('2025-11-30')).en).toBe('The 1st Sunday of Advent (Semiduplex) — St. Andrew, Apostle');
|
||||||
// Advent drops the redundant leading weekday word now that the header
|
// Advent drops the redundant leading weekday word now that the header
|
||||||
// always shows the weekday on its own (see temporalLabel's
|
// always shows the weekday on its own (see temporalLabel's
|
||||||
// `dropWeekdayPrefix`).
|
// `dropWeekdayPrefix`).
|
||||||
@@ -98,10 +99,10 @@ describe('getDayLabel — resumed post-Epiphany Sunday (overflow years)', () =>
|
|||||||
// Sundays) -- see calendar/temporal-id.test.ts for the id-level
|
// Sundays) -- see calendar/temporal-id.test.ts for the id-level
|
||||||
// coverage this label check builds on. Nov 10 itself also carries a
|
// coverage this label check builds on. Nov 10 itself also carries a
|
||||||
// Simplex commemoration (Ss. Tryphon, Respicius, and Nympha), shown
|
// Simplex commemoration (Ss. Tryphon, Respicius, and Nympha), shown
|
||||||
// appended to the Sunday's own label per this app's usual
|
// trailing after the Sunday's own winning label per this app's usual
|
||||||
// commemorated-Simplex display.
|
// winner-first display.
|
||||||
expect(getDayLabel(resolveDay('2024-11-10')).en).toBe(
|
expect(getDayLabel(resolveDay('2024-11-10')).en).toBe(
|
||||||
'Ss. Tryphon, Respicius, and Nympha, Martyrs — The 5th Sunday after Epiphany (Semiduplex)',
|
'The 5th Sunday after Epiphany (Semiduplex) — Ss. Tryphon, Respicius, and Nympha, Martyrs',
|
||||||
);
|
);
|
||||||
// 2024-11-11 is St. Martin of Tours (Duplex, pre-existing content) --
|
// 2024-11-11 is St. Martin of Tours (Duplex, pre-existing content) --
|
||||||
// an ordinary (non-privileged) Monday fully yields to a real winning
|
// an ordinary (non-privileged) Monday fully yields to a real winning
|
||||||
@@ -368,15 +369,47 @@ describe('getDayLabel — feast name combination', () => {
|
|||||||
expect(getDayLabel(day).en).toBe('St. Ereden (Duplex)');
|
expect(getDayLabel(day).en).toBe('St. Ereden (Duplex)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows both, feast first, when the feast is merely commemorated', () => {
|
it('shows both, winner first, when a feast is merely commemorated', () => {
|
||||||
const day: LiturgicalDay = {
|
const day: LiturgicalDay = {
|
||||||
...base,
|
...base,
|
||||||
commemorations: [{ kind: 'sanctoral', id: 'x', name: 'St. Ereden', rank: 'duplex-2-classis' }],
|
commemorations: [{ kind: 'sanctoral', id: 'x', name: 'St. Ereden', rank: 'duplex-2-classis' }],
|
||||||
};
|
};
|
||||||
expect(getDayLabel(day).en).toBe('St. Ereden — Monday in the 3rd week after Trinity (Feria)');
|
expect(getDayLabel(day).en).toBe('Monday in the 3rd week after Trinity (Feria) — St. Ereden');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows just the temporal label when nothing is commemorated at all', () => {
|
it('shows just the temporal label when nothing is commemorated at all', () => {
|
||||||
expect(getDayLabel(base).en).toBe('Monday in the 3rd week after Trinity (Feria)');
|
expect(getDayLabel(base).en).toBe('Monday in the 3rd week after Trinity (Feria)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getDayLabel/getVespersStatusLabel — Vespers evening with a temporal (Sunday) winner', () => {
|
||||||
|
it('2026-08-29 (Decollation of St. John Baptist, plain Duplex, below keepsOwnSecondVespers threshold) anticipates the following ordinary Sunday, which wins outright over Ss. Felix and Adauctus (Simplex) -- the Sunday must lead the label, with both real commemorations (Felix/Adauctus and the displaced Decollation) trailing, and the status must read First, not Second, Vespers', () => {
|
||||||
|
// Regression test: getDayLabel's temporal-fallback branch used to list
|
||||||
|
// commemorations *before* the winner, so this read "Ss. Felix and
|
||||||
|
// Adauctus, Martyrs -- The Beheading of St. John the Baptist (of
|
||||||
|
// today) -- The 13th Sunday after Trinity (Semiduplex)", making the
|
||||||
|
// actual winner (the Sunday) look like an afterthought and the
|
||||||
|
// Sunday's own rank label ("Semiduplex") look like it belonged to
|
||||||
|
// Felix and Adauctus instead. And getVespersStatusLabel used to say
|
||||||
|
// "Second Vespers (of today)" here since its old detection
|
||||||
|
// (`winner.vespersFrom`) only ever fires for a *sanctoral*
|
||||||
|
// tomorrow-winner, never a temporal one like this Sunday.
|
||||||
|
const day = resolveEveningDay('2026-08-29');
|
||||||
|
expect(day.date).toBe('2026-08-30');
|
||||||
|
expect(day.winner).toEqual({ kind: 'temporal', id: 'post-pentecost-14' });
|
||||||
|
expect(getDayLabel(day).en).toBe(
|
||||||
|
'The 13th Sunday after Trinity (Semiduplex) — Ss. Felix and Adauctus, Martyrs — The Beheading of St. John the Baptist (of today)',
|
||||||
|
);
|
||||||
|
expect(getVespersStatusLabel(day, '2026-08-29').en).toBe('First Vespers (of tomorrow)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("a plain kept evening (no anticipation) still reads Second Vespers", () => {
|
||||||
|
// 2026-08-17, St. Lawrence's own octave closing day (Duplex), keeps
|
||||||
|
// its own evening outright -- sanity check that the date-comparison
|
||||||
|
// rewrite of getVespersStatusLabel didn't flip the non-anticipating
|
||||||
|
// case too.
|
||||||
|
const day = resolveEveningDay('2026-08-17');
|
||||||
|
expect(day.date).toBe('2026-08-17');
|
||||||
|
expect(getVespersStatusLabel(day, '2026-08-17').en).toBe('Second Vespers (of today)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ describe('resolveOrdo("compline", ...)', () => {
|
|||||||
const eve = resolveOrdo('compline', '2025-11-29');
|
const eve = resolveOrdo('compline', '2025-11-29');
|
||||||
const last = eve.parts[eve.parts.length - 1];
|
const last = eve.parts[eve.parts.length - 1];
|
||||||
expect(last?.kind === 'preces' ? last.label : undefined).toBe('Alma Redemptoris Mater');
|
expect(last?.kind === 'preces' ? last.label : undefined).toBe('Alma Redemptoris Mater');
|
||||||
expect(eve.dayLabel?.en).toBe('St. Andrew, Apostle — The 1st Sunday of Advent (Semiduplex)');
|
expect(eve.dayLabel?.en).toBe('The 1st Sunday of Advent (Semiduplex) — St. Andrew, Apostle');
|
||||||
|
|
||||||
const dayBefore = resolveOrdo('compline', '2025-11-28');
|
const dayBefore = resolveOrdo('compline', '2025-11-28');
|
||||||
const lastBefore = dayBefore.parts[dayBefore.parts.length - 1];
|
const lastBefore = dayBefore.parts[dayBefore.parts.length - 1];
|
||||||
|
|||||||
@@ -69,6 +69,24 @@ describe('shell', () => {
|
|||||||
expect(root.querySelector('.day-nav-vespers-status')?.textContent).toContain('Second Vespers');
|
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', () => {
|
it('switches to Matins and shows its real content, not the pending message', () => {
|
||||||
const root = document.getElementById('app')!;
|
const root = document.getElementById('app')!;
|
||||||
mountShell(root);
|
mountShell(root);
|
||||||
|
|||||||
Reference in New Issue
Block a user