Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -1,11 +1,24 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { get } from 'svelte/store';
|
||||
import { availableTimeZones, createTimeFormatter, DEFAULT_TIME_ZONE, validTimeZone, timestampISO } from '../../src/lib/time-zone.js';
|
||||
import { startTimeZonePreference, timeFormat, timeZone, timeZonePersistenceError, TIME_ZONE_STORAGE_KEY } from '../../src/lib/ui/stores/time-zone.js';
|
||||
import {
|
||||
availableTimeZones,
|
||||
createTimeFormatter,
|
||||
DEFAULT_TIME_ZONE,
|
||||
validTimeZone,
|
||||
timestampISO
|
||||
} 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;
|
||||
stop?.();
|
||||
stop = undefined;
|
||||
timeZone.set(DEFAULT_TIME_ZONE);
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
@@ -16,7 +29,9 @@ describe('timezone formatting', () => {
|
||||
|
||||
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(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');
|
||||
});
|
||||
|
||||
@@ -44,7 +59,12 @@ describe('timezone formatting', () => {
|
||||
|
||||
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']) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -70,9 +90,18 @@ describe('timezone formatting', () => {
|
||||
});
|
||||
|
||||
it('uses timezone calendar dates for yesterday across DST and year boundaries', () => {
|
||||
expect(ny.relative('2026-11-01T04:30:00Z', Date.parse('2026-11-02T05:15:00Z'))).toBe('Yesterday, 12:30 AM');
|
||||
expect(ny.relative('2025-12-31T05:00:00Z', Date.parse('2026-01-01T06:00:00Z'))).toBe('Yesterday, 12:00 AM');
|
||||
expect(createTimeFormatter('invalid').relative('2026-10-05T17:00:00Z', Date.parse('2026-10-07T18:00:00Z'))).toBe('Oct 5, 2026, 1:00 PM EDT');
|
||||
expect(ny.relative('2026-11-01T04:30:00Z', Date.parse('2026-11-02T05:15:00Z'))).toBe(
|
||||
'Yesterday, 12:30 AM'
|
||||
);
|
||||
expect(ny.relative('2025-12-31T05:00:00Z', Date.parse('2026-01-01T06:00:00Z'))).toBe(
|
||||
'Yesterday, 12:00 AM'
|
||||
);
|
||||
expect(
|
||||
createTimeFormatter('invalid').relative(
|
||||
'2026-10-05T17:00:00Z',
|
||||
Date.parse('2026-10-07T18:00:00Z')
|
||||
)
|
||||
).toBe('Oct 5, 2026, 1:00 PM EDT');
|
||||
});
|
||||
|
||||
it('offers valid zones including New York and UTC, with an older-browser fallback', () => {
|
||||
@@ -82,9 +111,13 @@ describe('timezone formatting', () => {
|
||||
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)
|
||||
}));
|
||||
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);
|
||||
});
|
||||
@@ -94,13 +127,17 @@ 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); })
|
||||
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 }));
|
||||
window.dispatchEvent(
|
||||
Object.assign(new Event('storage'), { storageArea: storage, key, newValue })
|
||||
);
|
||||
}
|
||||
return { values, storage, change };
|
||||
}
|
||||
@@ -110,21 +147,27 @@ describe('timezone preference', () => {
|
||||
const { values } = browser('UTC');
|
||||
stop = startTimeZonePreference();
|
||||
let shown = '';
|
||||
const unsubscribe = timeFormat.subscribe(format => { shown = format.time('2026-01-15T12:00:00Z'); });
|
||||
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();
|
||||
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.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();
|
||||
@@ -135,16 +178,23 @@ describe('timezone preference', () => {
|
||||
expect(get(timeZone)).toBe('Asia/Tokyo');
|
||||
change(null, null);
|
||||
expect(get(timeZone)).toBe(DEFAULT_TIME_ZONE);
|
||||
stop(); stop = undefined;
|
||||
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();
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user