Files
ComfyUI_frontend/src/utils/dateTimeUtil.test.ts
Dante bab2870131 test: harden strict parser coverage for long ISO fractions (#11529)
## Summary

- run every `parseIsoDateSafe` case with more than 3 fractional-second
digits through `withStrictMillisecondParser`
- assert the normalized 3-digit timestamp string passed into `Date` for
each long-fraction variant
- keep the follow-up scoped to test coverage only

## Root cause

V8 already accepts and truncates ISO timestamps with more than 3
fractional-second digits, so the existing tests could stay green even if
`parseIsoDateSafe` failed to normalize those values before constructing
`Date`. Wrapping the long-fraction cases in the strict parser shim makes
CI exercise the Safari/WebView-sensitive path the feature is meant to
protect.

## Testing

- `pnpm exec vitest run src/utils/dateTimeUtil.test.ts`
- `pnpm exec eslint src/utils/dateTimeUtil.test.ts`

Fixes #11528

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11529-codex-test-harden-strict-parser-coverage-for-long-ISO-fractions-34a6d73d365081119577eb5fb6d4992c)
by [Unito](https://www.unito.io)
2026-04-23 03:05:03 +00:00

220 lines
6.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
dateKey,
formatClockTime,
formatShortMonthDay,
isToday,
isYesterday,
parseIsoDateSafe
} from './dateTimeUtil'
const isoFractionalSecondsPattern = /\.(\d+)(?=Z|[+-]\d{2}:?\d{2}|$)/
function withStrictMillisecondParser<T>(
run: (normalizedValues: string[]) => T
): T {
const RealDate = Date
const normalizedValues: string[] = []
class StrictDate extends RealDate {
constructor(value?: string | number | Date) {
if (arguments.length === 0) {
super()
return
}
if (typeof value === 'string') {
normalizedValues.push(value)
const fractionalSeconds = value.match(isoFractionalSecondsPattern)?.[1]
if (fractionalSeconds && fractionalSeconds.length !== 3) {
super(Number.NaN)
return
}
}
super(value as string | number)
}
}
vi.stubGlobal('Date', StrictDate as DateConstructor)
try {
return run(normalizedValues)
} finally {
vi.unstubAllGlobals()
}
}
describe('parseIsoDateSafe', () => {
it('parses standard ISO 8601 with millisecond precision', () => {
const date = parseIsoDateSafe('2026-04-18T10:04:55.651Z')
expect(date?.toISOString()).toBe('2026-04-18T10:04:55.651Z')
})
it('normalizes fractional seconds longer than 3 digits', () => {
withStrictMillisecondParser((normalizedValues) => {
const date = parseIsoDateSafe('2026-04-18T10:04:55.6513Z')
expect(date?.toISOString()).toBe('2026-04-18T10:04:55.651Z')
expect(normalizedValues).toEqual(['2026-04-18T10:04:55.651Z'])
})
})
it('handles fractional seconds without timezone suffix', () => {
withStrictMillisecondParser((normalizedValues) => {
const date = parseIsoDateSafe('2026-04-18T10:04:55.6513')
expect(date).not.toBeNull()
expect(Number.isNaN(date!.getTime())).toBe(false)
expect(normalizedValues).toEqual(['2026-04-18T10:04:55.651'])
})
})
it('handles offset timezones with long fractional seconds', () => {
withStrictMillisecondParser((normalizedValues) => {
const date = parseIsoDateSafe('2026-04-18T10:04:55.6513+09:00')
expect(date?.toISOString()).toBe('2026-04-18T01:04:55.651Z')
expect(normalizedValues).toEqual(['2026-04-18T10:04:55.651+09:00'])
})
})
it('handles negative-offset timezones with long fractional seconds', () => {
withStrictMillisecondParser((normalizedValues) => {
const date = parseIsoDateSafe('2026-04-18T10:04:55.987654-05:00')
expect(date?.toISOString()).toBe('2026-04-18T15:04:55.987Z')
expect(normalizedValues).toEqual(['2026-04-18T10:04:55.987-05:00'])
})
})
it('handles the full 9-digit nanosecond precision Go can emit', () => {
withStrictMillisecondParser((normalizedValues) => {
const date = parseIsoDateSafe('2026-04-18T10:04:55.123456789Z')
expect(date?.toISOString()).toBe('2026-04-18T10:04:55.123Z')
expect(normalizedValues).toEqual(['2026-04-18T10:04:55.123Z'])
})
})
it('passes through timestamps without any fractional seconds', () => {
const date = parseIsoDateSafe('2026-04-18T10:04:55Z')
expect(date?.toISOString()).toBe('2026-04-18T10:04:55.000Z')
})
it('preserves an all-zero 3-digit fractional', () => {
const date = parseIsoDateSafe('2026-04-18T10:04:55.000Z')
expect(date?.toISOString()).toBe('2026-04-18T10:04:55.000Z')
})
it('normalizes 1- and 2-digit fractionals for strict parsers', () => {
withStrictMillisecondParser((normalizedValues) => {
expect(parseIsoDateSafe('2026-04-18T10:04:55.6Z')?.toISOString()).toBe(
'2026-04-18T10:04:55.600Z'
)
expect(parseIsoDateSafe('2026-04-18T10:04:55.65Z')?.toISOString()).toBe(
'2026-04-18T10:04:55.650Z'
)
expect(normalizedValues).toEqual([
'2026-04-18T10:04:55.600Z',
'2026-04-18T10:04:55.650Z'
])
})
})
it('returns null for empty string', () => {
expect(parseIsoDateSafe('')).toBeNull()
})
it('returns null for null', () => {
expect(parseIsoDateSafe(null)).toBeNull()
})
it('returns null for undefined', () => {
expect(parseIsoDateSafe(undefined)).toBeNull()
})
it('returns null for unparseable input', () => {
expect(parseIsoDateSafe('not-a-date')).toBeNull()
})
})
describe('dateKey', () => {
it('returns YYYY-MM-DD for a given timestamp', () => {
// 2024-03-15 in UTC
const ts = new Date(2024, 2, 15, 10, 30).getTime()
expect(dateKey(ts)).toBe('2024-03-15')
})
it('zero-pads single-digit months and days', () => {
const ts = new Date(2024, 0, 5).getTime()
expect(dateKey(ts)).toBe('2024-01-05')
})
})
describe('isToday', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date(2024, 5, 15, 14, 0, 0))
})
afterEach(() => {
vi.useRealTimers()
})
it('returns true for a timestamp on the same day', () => {
const ts = new Date(2024, 5, 15, 8, 0, 0).getTime()
expect(isToday(ts)).toBe(true)
})
it('returns false for yesterday', () => {
const ts = new Date(2024, 5, 14, 23, 59, 59).getTime()
expect(isToday(ts)).toBe(false)
})
it('returns false for tomorrow', () => {
const ts = new Date(2024, 5, 16, 0, 0, 0).getTime()
expect(isToday(ts)).toBe(false)
})
})
describe('isYesterday', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date(2024, 5, 15, 14, 0, 0))
})
afterEach(() => {
vi.useRealTimers()
})
it('returns true for a timestamp yesterday', () => {
const ts = new Date(2024, 5, 14, 10, 0, 0).getTime()
expect(isYesterday(ts)).toBe(true)
})
it('returns false for today', () => {
const ts = new Date(2024, 5, 15, 10, 0, 0).getTime()
expect(isYesterday(ts)).toBe(false)
})
it('returns false for two days ago', () => {
const ts = new Date(2024, 5, 13, 10, 0, 0).getTime()
expect(isYesterday(ts)).toBe(false)
})
})
describe('formatShortMonthDay', () => {
it('formats a date as short month + day', () => {
const ts = new Date(2024, 0, 2, 12, 0, 0).getTime()
const result = formatShortMonthDay(ts, 'en-US')
expect(result).toBe('Jan 2')
})
})
describe('formatClockTime', () => {
it('formats time with hours, minutes, and seconds', () => {
const ts = new Date(2024, 5, 15, 14, 5, 6).getTime()
const result = formatClockTime(ts, 'en-GB')
// en-GB uses 24-hour format
expect(result).toBe('14:05:06')
})
})