Files
electric-cherry/tests/unit/time-zone.test.ts
T

133 lines
6.2 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { get } from 'svelte/store';
import { availableTimeZones, createTimeFormatter, DEFAULT_TIME_ZONE, validTimeZone } from '../../src/lib/time-zone.js';
import { startTimeZonePreference, timeFormat, timeZone, timeZonePersistenceError, TIME_ZONE_STORAGE_KEY } from '../../src/lib/ui/stores/time-zone.js';
let stop: (() => void) | undefined;
afterEach(() => {
stop?.(); stop = undefined;
timeZone.set(DEFAULT_TIME_ZONE);
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
describe('timezone formatting', () => {
const ny = createTimeFormatter(DEFAULT_TIME_ZONE);
it('uses New York by default, including when a saved zone is invalid', () => {
expect(get(timeZone)).toBe('America/New_York');
expect(createTimeFormatter('invalid').dateTime('2026-01-15T12:00:00Z')).toBe('Jan 15, 2026, 7:00:00 AM EST');
expect(ny.dateTime('2026-07-15T12:00:00Z')).toBe('Jul 15, 2026, 8:00:00 AM EDT');
});
it('jumps over the missing spring hour and distinguishes repeated fall hours', () => {
expect(ny.time('2026-03-08T06:59:59Z')).toBe('1:59:59 AM EST');
expect(ny.time('2026-03-08T07:00:00Z')).toBe('3:00:00 AM EDT');
expect(ny.period('2026-11-01T05:30:00Z')).toBe('Nov 1, 2026, 1:30 AM EDT');
expect(ny.period('2026-11-01T06:30:00Z')).toBe('Nov 1, 2026, 1:30 AM EST');
});
it('uses the selected calendar day for date-only displays and history groups', () => {
const at = '2026-01-01T02:00:00Z';
expect(ny.date(at)).toBe('Dec 31, 2025');
expect(createTimeFormatter('Asia/Tokyo').date(at)).toBe('Jan 1, 2026');
expect(ny.dateTime(at)).toContain('Dec 31, 2025');
});
it('handles half-hour offsets, epoch values, and Date objects', () => {
const format = createTimeFormatter('Asia/Kolkata');
const at = Date.parse('2026-01-15T12:00:00Z');
expect(format.minuteTime(at)).toBe('5:30 PM GMT+5:30');
expect(format.dateTime(new Date(at))).toBe(format.dateTime(at));
expect(createTimeFormatter('UTC').time(0)).toBe('12:00:00 AM UTC');
});
it('interprets SQLite and unzoned ISO timestamps as UTC while preserving explicit offsets', () => {
const expected = ny.dateTime('2026-01-15T12:00:00Z');
for (const at of ['2026-01-15 12:00:00', '2026-01-15T12:00:00', '2026-01-15 12:00:00.123', '2026-01-15T07:00:00-05:00']) {
expect(ny.dateTime(at)).toBe(expected);
}
});
it('renders missing or invalid timestamps without crashing the page', () => {
for (const value of [null, undefined, '', 'bad timestamp', NaN, new Date(NaN)]) {
expect(ny.dateTime(value)).toBe('—');
}
});
it('offers valid zones including New York and UTC, with an older-browser fallback', () => {
expect(availableTimeZones()).toContain(DEFAULT_TIME_ZONE);
expect(availableTimeZones()).toContain('UTC');
expect(validTimeZone('America/Los_Angeles')).toBe(true);
for (const value of [null, '', 'invalid', 123]) expect(validTimeZone(value)).toBe(false);
vi.spyOn(Intl, 'supportedValuesOf').mockImplementation(() => ['Europe/London']);
expect(availableTimeZones()).toEqual(['America/New_York', 'Europe/London', 'UTC']);
vi.stubGlobal('Intl', new Proxy(Intl, {
get: (target, key) => key === 'supportedValuesOf' ? undefined : Reflect.get(target, key)
}));
expect(availableTimeZones()).toContain('Asia/Kolkata');
expect(availableTimeZones().every(validTimeZone)).toBe(true);
});
});
function browser(saved?: string) {
const values = new Map(saved ? [[TIME_ZONE_STORAGE_KEY, saved]] : []);
const storage = {
getItem: vi.fn((key: string) => values.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => { values.set(key, value); })
};
const window = new EventTarget();
vi.stubGlobal('localStorage', storage);
vi.stubGlobal('window', window);
function change(newValue: string | null, key: string | null = TIME_ZONE_STORAGE_KEY) {
window.dispatchEvent(Object.assign(new Event('storage'), { storageArea: storage, key, newValue }));
}
return { values, storage, change };
}
describe('timezone preference', () => {
it('restores the saved zone and updates subscribed formatters and persistence immediately', () => {
const { values } = browser('UTC');
stop = startTimeZonePreference();
let shown = '';
const unsubscribe = timeFormat.subscribe(format => { shown = format.time('2026-01-15T12:00:00Z'); });
expect(shown).toBe('12:00:00 PM UTC');
timeZone.set('America/New_York');
expect(shown).toBe('7:00:00 AM EST');
expect(values.get(TIME_ZONE_STORAGE_KEY)).toBe('America/New_York');
stop(); stop = startTimeZonePreference();
expect(get(timeZone)).toBe('America/New_York');
unsubscribe();
});
it.each([undefined, 'invalid'])('defaults safely with a missing or corrupt preference (%s)', saved => {
browser(saved);
stop = startTimeZonePreference();
expect(get(timeZone)).toBe(DEFAULT_TIME_ZONE);
});
it('syncs changes from other tabs and removes listeners on cleanup', () => {
const { change } = browser();
stop = startTimeZonePreference();
change('Asia/Tokyo');
expect(get(timeZone)).toBe('Asia/Tokyo');
change('UTC', 'unrelated-key');
expect(get(timeZone)).toBe('Asia/Tokyo');
change(null, null);
expect(get(timeZone)).toBe(DEFAULT_TIME_ZONE);
stop(); stop = undefined;
change('UTC');
expect(get(timeZone)).toBe(DEFAULT_TIME_ZONE);
});
it('keeps the preference usable when browser storage is blocked', () => {
const { storage } = browser();
storage.getItem.mockImplementation(() => { throw new Error('Storage blocked'); });
storage.setItem.mockImplementation(() => { throw new Error('Storage blocked'); });
expect(() => { stop = startTimeZonePreference(); }).not.toThrow();
timeZone.set('UTC');
expect(get(timeFormat).time('2026-01-15T12:00:00Z')).toBe('12:00:00 PM UTC');
expect(get(timeZonePersistenceError)).toBe(true);
});
});