133 lines
6.2 KiB
TypeScript
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);
|
|
});
|
|
});
|