/** * Tests for pages/subscriptions.js — formatNextUpdate. * * Pure date-formatting helper used by the subscriptions page to label * "Next refresh in …". The 5-minute past threshold drives a timezone * correction branch that has caused user-visible bugs before; lock the * branch boundaries down here. */ let formatNextUpdate; beforeAll(async () => { // subscriptions.js has a DOMContentLoaded listener that calls fetch(). // The event has typically already fired in happy-dom by the time we // import, so the listener never runs — but stub fetch defensively in // case the test ordering changes. globalThis.fetch = vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) ); await import('@js/pages/subscriptions.js'); formatNextUpdate = window.formatNextUpdate; }); describe('formatNextUpdate', () => { it('returns "Invalid date" for non-parsable input', () => { expect(formatNextUpdate('not-a-date')).toBe('Invalid date'); expect(formatNextUpdate('')).toBe('Invalid date'); expect(formatNextUpdate(undefined)).toBe('Invalid date'); }); it('returns a locale string for a clearly-future date (no past-branch fixup)', () => { const tenYearsLater = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000); const result = formatNextUpdate(tenYearsLater.toISOString()); // Should be a non-empty formatted string, not the invalid sentinel. expect(result).not.toBe('Invalid date'); expect(typeof result).toBe('string'); expect(result.length).toBeGreaterThan(0); }); it('returns a locale string for "near future" (within 5 min, no fixup)', () => { const twoMinutesFromNow = new Date(Date.now() + 2 * 60 * 1000); const result = formatNextUpdate(twoMinutesFromNow.toISOString()); expect(result).not.toBe('Invalid date'); expect(typeof result).toBe('string'); }); it('applies the timezone-offset correction when the date is >5 min in the past', () => { // The past-branch subtracts getTimezoneOffset()*60000 ms from the // parsed date, then formats. For timezones with non-zero offsets // the formatted output will differ from the un-corrected string; // for UTC environments both branches produce the same output, so // we just assert the value is not "Invalid date" and the function // executed the past branch without throwing. const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); const result = formatNextUpdate(oneHourAgo.toISOString()); expect(result).not.toBe('Invalid date'); expect(typeof result).toBe('string'); expect(result.length).toBeGreaterThan(0); }); it('treats a date exactly at the 5-minute past boundary as past-correctable', () => { // sixMinutesAgo is unambiguously past the threshold (>5min ago). const sixMinutesAgo = new Date(Date.now() - 6 * 60 * 1000); const result = formatNextUpdate(sixMinutesAgo.toISOString()); expect(result).not.toBe('Invalid date'); }); });