when-am-i / files / src / data / index.test.ts
index.test.ts
a167bf48 2026-06-17 Pavel Safronov | 1 | import { describe, it, expect } from 'vitest'; |
| 2 | import { regions } from './index'; | |
c7d226e1 2026-06-20 Pavel Safronov | 3 | import { convert } from '../utils/calendars'; |
| 4 | import { parseGregorianDate } from '../utils/julianDay'; | |
a167bf48 2026-06-17 Pavel Safronov | 5 | |
| 6 | // Smoke test: the data loader must validate and expose the calendar files. | |
| 7 | // As more regions are added this also acts as a basic data sanity check. | |
| 8 | describe('calendar data loader', () => { | |
| 9 | it('loads at least one region', () => { | |
| 10 | expect(regions.length).toBeGreaterThan(0); | |
| 11 | }); | |
| 12 | ||
26a02b71 2026-06-19 Pavel Safronov | 13 | it('loads the Massachusett region with named eras and Polygon geometry', () => { |
| 14 | const massachusett = regions.find((r) => r.region.name === 'Massachusett homeland'); | |
| 15 | expect(massachusett).toBeDefined(); | |
| 16 | expect(massachusett!.region.geo.type).toBe('Polygon'); | |
| 17 | expect(massachusett!.sourceFile).toBe('src/data/calendars/massachusett.json'); | |
| 18 | expect(massachusett!.eras.length).toBeGreaterThan(0); | |
| 19 | for (const era of massachusett!.eras) { | |
a167bf48 2026-06-17 Pavel Safronov | 20 | expect(era.name).toBeTruthy(); |
| 21 | expect(era.calendar).toBeTruthy(); | |
| 22 | } | |
| 23 | }); | |
| 24 | ||
| 25 | it('every era has string|null start/end and a calendar', () => { | |
| 26 | for (const { eras } of regions) { | |
| 27 | for (const era of eras) { | |
| 28 | expect(['string', 'object']).toContain(typeof era.start); // string or null | |
| 29 | expect(['string', 'object']).toContain(typeof era.end); | |
| 30 | expect(typeof era.calendar).toBe('string'); | |
| 31 | } | |
| 32 | } | |
| 33 | }); | |
c7d226e1 2026-06-20 Pavel Safronov | 34 | |
| 35 | // Regression guard: validate:data does not whitelist calendar names, so a | |
| 36 | // typo (e.g. "Persain") would pass validation but silently make a region | |
| 37 | // descriptive-only. These regions are meant to convert to a numeric year, so | |
| 38 | // pin that here. The date is the midpoint of each era's covered range. | |
| 39 | it.each([ | |
| 40 | ['Iran (historic Persia)', 'Lunar Hijri era', 'Islamic (Umm al-Qura)'], | |
| 41 | ['Iran (historic Persia)', 'Solar Hijri era', 'Persian (Solar Hijri)'], | |
| 42 | ['Russian America (Alaska)', 'Russian America (Old Style)', 'Julian'], | |
| 43 | ['Russian America (Alaska)', 'Alaska (United States)', 'Gregorian'], | |
| 44 | ['Oregon Country', 'Joint occupation (Hudson\'s Bay Company)', 'Gregorian'], | |
| 45 | ])('%s / %s converts to a numeric year (%s)', (regionName, eraName, system) => { | |
| 46 | const region = regions.find((r) => r.region.name === regionName); | |
| 47 | expect(region, `region "${regionName}" not loaded`).toBeDefined(); | |
| 48 | const era = region!.eras.find((e) => e.name === eraName); | |
| 49 | expect(era, `era "${eraName}" not found`).toBeDefined(); | |
| 50 | ||
| 51 | // Pick a date inside the era's range (midpoint, or near a null bound). | |
| 52 | const startJd = era!.start ? parseGregorianDate(era!.start) : null; | |
| 53 | const endJd = era!.end ? parseGregorianDate(era!.end) : null; | |
| 54 | const year = startJd && endJd | |
| 55 | ? Math.floor((startJd.year + endJd.year) / 2) | |
| 56 | : startJd ? startJd.year + 10 | |
| 57 | : endJd ? endJd.year - 10 | |
| 58 | : 1850; | |
| 59 | ||
| 60 | const conversion = convert({ year, month: 7, day: 1 }, era!.calendar); | |
| 61 | expect(conversion.convertible, `${regionName}/${eraName} should be convertible`).toBe(true); | |
| 62 | expect(conversion.system).toBe(system); | |
| 63 | }); | |
a167bf48 2026-06-17 Pavel Safronov | 64 | }); |