import { describe, expect, it, vi } from 'vitest'; import { parseHolidays, withRetries, type ApiHoliday } from './openholidays'; const FR: ApiHoliday[] = [ { id: 'a', startDate: '2026-09-10', endDate: '2026-09-10', nationwide: false, name: [{ language: 'FR', text: 'Jeûne genevois' }], }, { id: 'b', startDate: '2026-12-25', endDate: '2026-12-25', nationwide: true, name: [{ language: 'FR', text: 'Noël' }], }, ]; const EN: ApiHoliday[] = [ { id: 'a', startDate: '2026-09-10', endDate: '2026-09-10', name: [{ language: 'EN', text: 'Geneva Fast' }] }, { id: 'b', startDate: '2026-12-25', endDate: '2026-12-25', name: [{ language: 'EN', text: 'Christmas Day' }] }, ]; describe('parseHolidays', () => { it('joins the two language calls on the entry id', () => { // Holiday names are proper nouns with established English forms; joining // beats sending them through a translation model. expect(parseHolidays(FR, EN)).toEqual([ { date: '2026-09-10', nameFr: 'Jeûne genevois', nameEn: 'Geneva Fast', nationwide: false }, { date: '2026-12-25', nameFr: 'Noël', nameEn: 'Christmas Day', nationwide: true }, ]); }); it('falls back to the French name when the English call comes back short', () => { expect(parseHolidays(FR, [])[0]).toMatchObject({ nameFr: 'Jeûne genevois', nameEn: 'Jeûne genevois' }); }); it('expands an entry that spans several days', () => { const spanning: ApiHoliday[] = [ { id: 'c', startDate: '2026-12-24', endDate: '2026-12-26', name: [{ language: 'FR', text: 'Fêtes' }], }, ]; expect(parseHolidays(spanning, []).map((holiday) => holiday.date)).toEqual([ '2026-12-24', '2026-12-25', '2026-12-26', ]); }); it('treats a missing end date as a single day', () => { const single: ApiHoliday[] = [ { id: 'd', startDate: '2026-08-01', endDate: '', name: [{ language: 'FR', text: 'Fête nationale' }] }, ]; expect(parseHolidays(single, [])).toHaveLength(1); }); it('ignores a reversed range rather than spinning', () => { const reversed: ApiHoliday[] = [ { id: 'e', startDate: '2026-08-05', endDate: '2026-08-01', name: [{ language: 'FR', text: 'X' }] }, ]; expect(parseHolidays(reversed, [])).toHaveLength(1); }); it('drops an entry with no usable name', () => { expect(parseHolidays([{ id: 'f', startDate: '2026-08-01', endDate: '2026-08-01' }], [])).toEqual([]); }); it('defaults an unstated scope to nationwide', () => { const vague: ApiHoliday[] = [ { id: 'g', startDate: '2026-08-01', endDate: '2026-08-01', name: [{ language: 'FR', text: 'X' }] }, ]; expect(parseHolidays(vague, [])[0]?.nationwide).toBe(true); }); it('accepts an empty calendar', () => { expect(parseHolidays([], [])).toEqual([]); }); }); describe('withRetries', () => { it('returns the first success without waiting', async () => { const sleep = vi.fn(async (_ms: number) => undefined); expect(await withRetries(async () => 'ok', 3, 100, sleep)).toBe('ok'); expect(sleep).not.toHaveBeenCalled(); }); it('retries and widens the gap', async () => { const sleep = vi.fn(async (_ms: number) => undefined); let attempts = 0; const result = await withRetries( async () => { attempts += 1; if (attempts < 3) { throw new Error('transient'); } return 'ok'; }, 3, 100, sleep, ); expect(result).toBe('ok'); expect(sleep.mock.calls.map((call) => call[0])).toEqual([100, 200]); }); it('gives up after the last attempt and reports the final error', async () => { const sleep = vi.fn(async (_ms: number) => undefined); await expect( withRetries( async () => { throw new Error('toujours en panne'); }, 3, 10, sleep, ), ).rejects.toThrow('toujours en panne'); expect(sleep).toHaveBeenCalledTimes(2); }); });