226fff2ce6
Deploy / deploy (push) Successful in 38s
Adds a real label under each hour's heading — "Monday in the 10th week after Trinity", "The 1st Sunday of Advent" — computed from whichever LiturgicalDay the hour actually resolved against, which can differ from the nav date for Compline's evening anticipation. calendar/day-label.ts combines two things: an ordinal week-within-season label (pure date arithmetic on the season anchors from calendar/temporal.ts and calendar/easter.ts — Trinity-counted, not Divinum Officium's own Pentecost-counted convention; meant to become configurable later via the same day->id indirection already used for the sanctoral calendar, not hardcoded forever), and a feast name from calendar/commemorations.ts's occurrence decision — shown alone if the feast displaces the day outright, prefixed onto the temporal label if merely commemorated, or omitted entirely if nothing's occurring.
139 lines
7.2 KiB
TypeScript
139 lines
7.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { resolveOrdo } from '../../src/hours';
|
|
|
|
const MONDAY = '2026-06-15';
|
|
const SUNDAY = '2026-06-14';
|
|
|
|
describe('resolveOrdo("compline", ...)', () => {
|
|
it('is implemented, with fixed psalms 4, 90, 133 regardless of weekday', () => {
|
|
const monday = resolveOrdo('compline', MONDAY);
|
|
const sunday = resolveOrdo('compline', SUNDAY);
|
|
expect(monday.notImplemented).toBeUndefined();
|
|
|
|
const psalmNumbers = (ordo: typeof monday) =>
|
|
ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
|
expect(psalmNumbers(monday)).toEqual([4, 90, 133]);
|
|
expect(psalmNumbers(sunday)).toEqual([4, 90, 133]);
|
|
});
|
|
|
|
it('gives the psalms no antiphon (real ferial Compline has none)', () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const psalms = ordo.parts.filter((p) => p.kind === 'psalm');
|
|
for (const psalm of psalms) {
|
|
expect(psalm.kind === 'psalm' ? psalm.antiphon : 'should be undefined').toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('resolves the opening versicle and the pre-psalm Confiteor (before the psalms, unlike Prime)', () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const firstPsalmIndex = ordo.parts.findIndex((p) => p.kind === 'psalm');
|
|
const confiteorIndex = ordo.parts.findIndex(
|
|
(p) => p.kind === 'preces' && p.text.text.la?.includes('Confíteor Deo'),
|
|
);
|
|
const openingIndex = ordo.parts.findIndex((p) => p.kind === 'versicle');
|
|
expect(confiteorIndex).toBeGreaterThanOrEqual(0);
|
|
expect(openingIndex).toBeGreaterThan(confiteorIndex);
|
|
expect(firstPsalmIndex).toBeGreaterThan(openingIndex);
|
|
});
|
|
|
|
it("appends the hymn's own (per-annum) doxology, distinct from Prime's", () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const hymn = ordo.parts.find((p) => p.kind === 'hymn');
|
|
expect(hymn && hymn.kind === 'hymn' ? hymn.text.text.en : undefined).toContain(
|
|
'O Father, that we ask be done',
|
|
);
|
|
});
|
|
|
|
it('gives the capitulum its scripture citation (fixed, not Sunday/feria-split)', () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const chapter = ordo.parts.find((p) => p.kind === 'chapter');
|
|
expect(chapter && chapter.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Jer. 14:9');
|
|
});
|
|
|
|
it('resolves the Nunc Dimittis as a canticle with an opening antiphon, plus a full antiphon reprise', () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const canticle = ordo.parts.find((p) => p.kind === 'canticle');
|
|
expect(canticle && canticle.kind === 'canticle' ? canticle.canticleId : undefined).toBe('nunc-dimittis');
|
|
expect(canticle && canticle.kind === 'canticle' ? canticle.text.text.en : undefined).toContain(
|
|
'Now ✠ thou dost dismiss thy servant',
|
|
);
|
|
expect(canticle && canticle.kind === 'canticle' ? canticle.antiphon?.text.en : undefined).toBe(
|
|
'Ant. Protect us.',
|
|
);
|
|
|
|
const canticleIndex = ordo.parts.indexOf(canticle!);
|
|
const closingAntiphon = ordo.parts[canticleIndex + 1];
|
|
expect(closingAntiphon?.kind).toBe('antiphon');
|
|
// The "*" chant mark is preserved in the full (closing) form.
|
|
expect(closingAntiphon?.kind === 'antiphon' ? closingAntiphon.text.text.en : undefined).toBe(
|
|
'Ant. Protect us, * Lord, while we are awake and safeguard us while we sleep; that we may keep watch with Christ, and rest in peace.',
|
|
);
|
|
});
|
|
|
|
it('includes the fuller Tridentine 1906 Preces (the triple "Blessed art thou" exchange Monastic drops)', () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const preces = ordo.parts.find((p) => p.kind === 'preces' && p.text.text.la?.includes('Kýrie, eléison'));
|
|
expect(preces && preces.kind === 'preces' ? preces.text.text.la : undefined).toContain('Benedíctus es, Dómine');
|
|
expect(preces && preces.kind === 'preces' ? preces.text.text.la : undefined).toContain('Miserére nostri');
|
|
});
|
|
|
|
it('closes with the Salve Regina (the per-annum default Marian antiphon), labeled by name', () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const last = ordo.parts[ordo.parts.length - 1];
|
|
expect(last?.kind).toBe('preces');
|
|
expect(last?.kind === 'preces' ? last.label : undefined).toBe('Salve Regina');
|
|
expect(last?.kind === 'preces' ? last.text.text.en : undefined).toContain('Hail holy Queen');
|
|
});
|
|
|
|
it('anticipates Advent I: the Saturday evening before switches to the Alma Redemptoris Mater and shows the anticipated day label', () => {
|
|
// Nov 30, 2025 is Advent I Sunday.
|
|
const eve = resolveOrdo('compline', '2025-11-29');
|
|
const last = eve.parts[eve.parts.length - 1];
|
|
expect(last?.kind === 'preces' ? last.label : undefined).toBe('Alma Redemptoris Mater');
|
|
expect(eve.dayLabel).toBe('The 1st Sunday of Advent');
|
|
|
|
const dayBefore = resolveOrdo('compline', '2025-11-28');
|
|
const lastBefore = dayBefore.parts[dayBefore.parts.length - 1];
|
|
expect(lastBefore?.kind === 'preces' ? lastBefore.label : undefined).toBe('Salve Regina');
|
|
});
|
|
|
|
it('switches to the Ave Regina Caelorum from Candlemas through the day before Maundy Thursday', () => {
|
|
const marianLabel = (date: string) => {
|
|
const ordo = resolveOrdo('compline', date);
|
|
const last = ordo.parts[ordo.parts.length - 1];
|
|
return last?.kind === 'preces' ? last.label : undefined;
|
|
};
|
|
// Easter 2026 is Apr 5, so Maundy Thursday is Apr 2.
|
|
expect(marianLabel('2026-01-31')).toBe('Salve Regina'); // well before Candlemas
|
|
// Candlemas (Feb 2) has First Vespers — Compline on Feb 1 evening
|
|
// already anticipates it, same as Advent I and Easter do (see
|
|
// calendar/vespers.ts).
|
|
expect(marianLabel('2026-02-01')).toBe('Ave Regina Caelorum'); // eve of Candlemas, anticipated
|
|
expect(marianLabel('2026-02-02')).toBe('Ave Regina Caelorum'); // Candlemas itself
|
|
expect(marianLabel('2026-03-10')).toBe('Ave Regina Caelorum'); // mid-Lent
|
|
expect(marianLabel('2026-04-01')).toBe('Ave Regina Caelorum'); // eve of Maundy Thursday
|
|
expect(marianLabel('2026-04-02')).not.toBe('Ave Regina Caelorum'); // Maundy Thursday itself
|
|
});
|
|
|
|
it('closes the Short Reading with "Deo gratias", via the shared lesson-close proper', () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const shortReadingIndex = ordo.parts.findIndex((p) => p.kind === 'lesson' && p.label === 'Short Reading');
|
|
const closing = ordo.parts[shortReadingIndex + 1];
|
|
expect(closing?.kind).toBe('preces');
|
|
expect(closing?.kind === 'preces' ? closing.text.text.en : undefined).toContain('Thanks be to God');
|
|
});
|
|
|
|
it('includes the "In manus tuas" responsory between the capitulum and the versicle', () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const chapterIndex = ordo.parts.findIndex((p) => p.kind === 'chapter');
|
|
const responsoryIndex = ordo.parts.findIndex((p) => p.kind === 'responsory');
|
|
const versicleIndex = ordo.parts.findIndex((p) => p.kind === 'versicle' && p.text.text.la?.includes('Custódi nos'));
|
|
expect(responsoryIndex).toBeGreaterThan(chapterIndex);
|
|
expect(versicleIndex).toBeGreaterThan(responsoryIndex);
|
|
const responsory = ordo.parts[responsoryIndex];
|
|
expect(responsory?.kind === 'responsory' ? responsory.text.text.en : undefined).toContain(
|
|
'Into thy hands, O Lord',
|
|
);
|
|
});
|
|
});
|