134 lines
5.2 KiB
JavaScript
134 lines
5.2 KiB
JavaScript
import dayjs from 'dayjs';
|
|
import { parseDoormileTimestamp, formatDoormileTimestamp, doormileDay } from '@/lib/doormileTimestamp';
|
|
|
|
/**
|
|
* Doormile timestamps are IST wall-clock in naive Postgres columns, and some
|
|
* responses carry a FALSE trailing `Z` (a Go+pgx marshalling footgun). Parsed
|
|
* naively, dayjs reads that Z as a real UTC instant and adds +5:30 to digits
|
|
* that were already correct.
|
|
*
|
|
* These tests protect the one property the whole app depends on: whatever zone
|
|
* marker a timestamp arrives with, the rendered and bucketed result is the raw
|
|
* wall-clock digits — asserted without depending on the machine's own timezone.
|
|
*/
|
|
describe('doormileTimestamp', () => {
|
|
describe('parseDoormileTimestamp', () => {
|
|
it('should ignore a false trailing Z rather than shifting the clock by +5:30', () => {
|
|
const parsed = parseDoormileTimestamp('2026-08-27T14:30:00Z');
|
|
expect(parsed.isValid()).toBe(true);
|
|
expect(parsed.hour()).toBe(14);
|
|
expect(parsed.minute()).toBe(30);
|
|
expect(parsed.date()).toBe(27);
|
|
});
|
|
|
|
it('should strip a +05:30 offset and keep the same wall-clock digits', () => {
|
|
const parsed = parseDoormileTimestamp('2026-08-27T14:30:00+05:30');
|
|
expect(parsed.hour()).toBe(14);
|
|
expect(parsed.minute()).toBe(30);
|
|
});
|
|
|
|
it('should strip a colon-less offset such as +0530', () => {
|
|
expect(parseDoormileTimestamp('2026-08-27T14:30:00+0530').hour()).toBe(14);
|
|
});
|
|
|
|
it('should strip a negative offset too', () => {
|
|
expect(parseDoormileTimestamp('2026-08-27T14:30:00-04:00').hour()).toBe(14);
|
|
});
|
|
|
|
it('should parse an already-naive timestamp unchanged', () => {
|
|
const parsed = parseDoormileTimestamp('2026-08-27T14:30:00');
|
|
expect(parsed.hour()).toBe(14);
|
|
expect(parsed.date()).toBe(27);
|
|
});
|
|
|
|
it('should produce the identical instant for the naive and the false-Z form', () => {
|
|
const naive = parseDoormileTimestamp('2026-08-27T23:45:00');
|
|
const falseZ = parseDoormileTimestamp('2026-08-27T23:45:00Z');
|
|
expect(falseZ.valueOf()).toBe(naive.valueOf());
|
|
});
|
|
|
|
it.each([null, undefined, '', 0, false])(
|
|
'should return an invalid dayjs (not throw) for the blank value %p',
|
|
(raw) => {
|
|
expect(parseDoormileTimestamp(raw).isValid()).toBe(false);
|
|
}
|
|
);
|
|
|
|
it('should return an invalid dayjs for unparseable text', () => {
|
|
expect(parseDoormileTimestamp('not-a-timestamp').isValid()).toBe(false);
|
|
});
|
|
|
|
it('should accept a bare date with no time component', () => {
|
|
const parsed = parseDoormileTimestamp('2026-08-27');
|
|
expect(parsed.isValid()).toBe(true);
|
|
expect(parsed.format('YYYY-MM-DD')).toBe('2026-08-27');
|
|
});
|
|
|
|
it('should accept a Date instance without throwing', () => {
|
|
expect(parseDoormileTimestamp(new Date('2026-08-27T10:00:00Z')).isValid()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('formatDoormileTimestamp', () => {
|
|
it('should render the default operator-facing format', () => {
|
|
expect(formatDoormileTimestamp('2026-08-27T14:30:00Z')).toBe('27 Aug 2026, 02:30 PM');
|
|
});
|
|
|
|
it('should honour a caller-supplied format', () => {
|
|
expect(formatDoormileTimestamp('2026-08-27T14:30:00Z', 'YYYY-MM-DD HH:mm')).toBe('2026-08-27 14:30');
|
|
});
|
|
|
|
it('should return the em-dash fallback for an unusable timestamp', () => {
|
|
expect(formatDoormileTimestamp(null)).toBe('—');
|
|
expect(formatDoormileTimestamp('garbage')).toBe('—');
|
|
});
|
|
|
|
it('should return a caller-supplied fallback instead of the em dash', () => {
|
|
expect(formatDoormileTimestamp(undefined, 'DD MMM', 'Not set')).toBe('Not set');
|
|
});
|
|
});
|
|
|
|
describe('doormileDay', () => {
|
|
it('should return the calendar day the wall-clock reading falls on', () => {
|
|
expect(doormileDay('2026-08-27T14:30:00Z')).toBe('2026-08-27');
|
|
});
|
|
|
|
it('should keep a late-evening row on its own day rather than rolling it forward', () => {
|
|
expect(doormileDay('2026-08-27T23:45:00Z')).toBe('2026-08-27');
|
|
});
|
|
|
|
it('should keep an early-morning row on its own day', () => {
|
|
expect(doormileDay('2026-08-27T00:15:00Z')).toBe('2026-08-27');
|
|
});
|
|
|
|
it('should return undefined — not a string — for an unusable timestamp', () => {
|
|
// Callers compare this against a selected date; a formatted "Invalid Date"
|
|
// would silently never match.
|
|
expect(doormileDay(null)).toBeUndefined();
|
|
expect(doormileDay('garbage')).toBeUndefined();
|
|
});
|
|
|
|
it('should agree across every zone-marker spelling of the same reading', () => {
|
|
const forms = [
|
|
'2026-08-27T23:45:00',
|
|
'2026-08-27T23:45:00Z',
|
|
'2026-08-27T23:45:00+05:30',
|
|
'2026-08-27T23:45:00+0000'
|
|
];
|
|
const days = forms.map(doormileDay);
|
|
expect(new Set(days).size).toBe(1);
|
|
expect(days[0]).toBe('2026-08-27');
|
|
});
|
|
|
|
it('should differ from a bare dayjs() parse whenever the runner is not on UTC', () => {
|
|
// Guards the regression directly: bare dayjs() on a Z-suffixed IST reading
|
|
// lands on a different day in any non-UTC zone.
|
|
const raw = '2026-08-27T23:45:00Z';
|
|
expect(doormileDay(raw)).toBe('2026-08-27');
|
|
if (dayjs(raw).utcOffset() > 15) {
|
|
expect(dayjs(raw).format('YYYY-MM-DD')).toBe('2026-08-28');
|
|
}
|
|
});
|
|
});
|
|
});
|