import { test } from 'node:test'; import assert from 'node:assert/strict'; import { getActiveSeason, resolveSeasonTheme, SEASON_SCHEDULE, SEASON_DATE_RANGES, } from './seasonSchedule'; import { SeasonTheme } from './types'; // Date(year, monthIndex0, day) const on = (monthIndex0: number, day: number): Date => new Date(2026, monthIndex0, day); test('each theme activates on a representative day in its window', () => { const cases: Array<[Date, SeasonTheme]> = [ [on(11, 31), 'newyear'], // Dec 31 [on(0, 1), 'newyear'], // Jan 1 [on(0, 25), 'lunar'], // Jan 25 [on(1, 3), 'lunar'], // Feb 3 [on(1, 12), 'valentines'], // Feb 12 [on(2, 16), 'stpatricks'], // Mar 16 [on(3, 1), 'aprilfools'], // Apr 1 [on(3, 21), 'earthday'], // Apr 21 [on(8, 12), 'arcade'], // Sep 12 [on(8, 25), 'autumn'], // Sep 25 [on(9, 20), 'halloween'], // Oct 20 [on(10, 1), 'halloween'], // Nov 1 [on(11, 15), 'christmas'], // Dec 15 ]; for (const [date, expected] of cases) { assert.equal(getActiveSeason(date), expected, `${date.toDateString()} -> ${expected}`); } }); test('priority order resolves overlapping windows (Deep Space outranks Autumn)', () => { // Oct 4-10 is inside Autumn's Oct<=14 window too; Deep Space comes first. assert.equal(getActiveSeason(on(9, 5)), 'deepspace'); // Oct 5 // Oct 12 is past Deep Space -> falls through to Autumn. assert.equal(getActiveSeason(on(9, 12)), 'autumn'); }); test('New Year outranks Lunar New Year on Jan 1-2', () => { assert.equal(getActiveSeason(on(0, 1)), 'newyear'); // Jan 22+ is past New Year -> Lunar. assert.equal(getActiveSeason(on(0, 22)), 'lunar'); }); test('returns null on an off-season day', () => { assert.equal(getActiveSeason(on(5, 15)), null); // Jun 15 assert.equal(getActiveSeason(on(6, 4)), null); // Jul 4 }); test('window boundaries are inclusive at both ends', () => { assert.equal(getActiveSeason(on(1, 10)), 'valentines'); // Feb 10 start assert.equal(getActiveSeason(on(1, 15)), 'valentines'); // Feb 15 end assert.equal(getActiveSeason(on(1, 16)), null); // Feb 16 just after }); test('resolveSeasonTheme: off → null, pinned → that theme, auto → active season', () => { const halloweenTs = on(9, 20).getTime(); // Oct 20 → halloween season const offSeasonTs = on(5, 15).getTime(); // Jun 15 → no season // 'off' never renders, regardless of date. assert.equal(resolveSeasonTheme('off', halloweenTs), null); // A pinned theme renders regardless of date (even off-season). assert.equal(resolveSeasonTheme('christmas', offSeasonTs), 'christmas'); // 'auto' tracks the active season for the given instant. assert.equal(resolveSeasonTheme('auto', halloweenTs), 'halloween'); assert.equal(resolveSeasonTheme('auto', offSeasonTs), null); }); test('resolveSeasonTheme: auto re-evaluates as `now` advances across a boundary', () => { // The same 'auto' override yields different themes at different instants — this // is what the SeasonalEffect ticker relies on (incl. the switch-into-auto case // where `now` must be current, not a stale mount value). assert.equal(resolveSeasonTheme('auto', on(9, 20).getTime()), 'halloween'); // Oct 20 assert.equal(resolveSeasonTheme('auto', on(11, 15).getTime()), 'christmas'); // Dec 15 assert.equal(resolveSeasonTheme('auto', on(6, 4).getTime()), null); // Jul 4 }); test('SEASON_DATE_RANGES has a label for every scheduled theme', () => { assert.equal(SEASON_SCHEDULE.length, 11); const themes = SEASON_SCHEDULE.map((e) => e.theme); assert.equal(new Set(themes).size, 11); // unique for (const t of themes) { assert.ok( typeof SEASON_DATE_RANGES[t] === 'string' && SEASON_DATE_RANGES[t].length > 0, `missing date range for ${t}`, ); } });