d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
159 lines
6.3 KiB
TypeScript
159 lines
6.3 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*/
|
|
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
formatDateCellDisplay,
|
|
isCalendarDateString,
|
|
normalizeDateCellValue,
|
|
storedDateToEditable,
|
|
} from '@/lib/table/dates'
|
|
|
|
/** The runtime zone's offset suffix at a given local wall time, e.g. `-07:00`. */
|
|
function localOffsetSuffix(local: Date): string {
|
|
const minutes = -local.getTimezoneOffset()
|
|
if (minutes === 0) return 'Z'
|
|
const sign = minutes > 0 ? '+' : '-'
|
|
const abs = Math.abs(minutes)
|
|
const pad = (n: number) => String(n).padStart(2, '0')
|
|
return `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`
|
|
}
|
|
|
|
describe('isCalendarDateString', () => {
|
|
it('accepts YYYY-MM-DD and rejects everything else', () => {
|
|
expect(isCalendarDateString('2026-07-06')).toBe(true)
|
|
expect(isCalendarDateString('2026-13-45')).toBe(false)
|
|
expect(isCalendarDateString('2026-07-06T00:00:00Z')).toBe(false)
|
|
expect(isCalendarDateString('07/06/2026')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('normalizeDateCellValue', () => {
|
|
it('keeps calendar dates timezone-free', () => {
|
|
expect(normalizeDateCellValue('2026-07-06')).toBe('2026-07-06')
|
|
expect(normalizeDateCellValue(' 2026-07-06 ')).toBe('2026-07-06')
|
|
})
|
|
|
|
it('normalizes date-only inputs in other formats to calendar dates', () => {
|
|
expect(normalizeDateCellValue('07/06/2026')).toBe('2026-07-06')
|
|
expect(normalizeDateCellValue('7/6/2026')).toBe('2026-07-06')
|
|
expect(normalizeDateCellValue('July 6, 2026')).toBe('2026-07-06')
|
|
})
|
|
|
|
it('normalizes reduced-precision ISO forms via their UTC day', () => {
|
|
expect(normalizeDateCellValue('2026-07')).toBe('2026-07-01')
|
|
expect(normalizeDateCellValue('2026')).toBe('2026-01-01')
|
|
})
|
|
|
|
it('preserves the wall time and offset of explicit-offset inputs', () => {
|
|
expect(normalizeDateCellValue('2026-07-06T16:04:55-07:00')).toBe('2026-07-06T16:04:55-07:00')
|
|
expect(normalizeDateCellValue('2026-07-06 16:04:55 PDT')).toBe('2026-07-06T16:04:55-07:00')
|
|
expect(normalizeDateCellValue('2026-07-06T23:04:55.000Z')).toBe('2026-07-06T23:04:55Z')
|
|
expect(normalizeDateCellValue('2026-07-06 16:04:55+00')).toBe('2026-07-06T16:04:55Z')
|
|
expect(normalizeDateCellValue('2026-07-06 16:04:55 EST')).toBe('2026-07-06T16:04:55-05:00')
|
|
})
|
|
|
|
it('is idempotent on canonical instants', () => {
|
|
const canonical = '2026-07-06T16:04:55-07:00'
|
|
expect(normalizeDateCellValue(canonical)).toBe(canonical)
|
|
expect(normalizeDateCellValue(canonical, { timezone: 'Asia/Tokyo' })).toBe(canonical)
|
|
})
|
|
|
|
it('stamps naive datetimes with the runtime zone offset by default', () => {
|
|
const local = new Date(2026, 6, 6, 16, 4, 55)
|
|
expect(normalizeDateCellValue('2026-07-06 16:04:55')).toBe(
|
|
`2026-07-06T16:04:55${localOffsetSuffix(local)}`
|
|
)
|
|
})
|
|
|
|
it('stamps naive datetimes with the provided IANA zone offset', () => {
|
|
// July → America/New_York is EDT (UTC-4)
|
|
expect(normalizeDateCellValue('2026-07-06 16:04:55', { timezone: 'America/New_York' })).toBe(
|
|
'2026-07-06T16:04:55-04:00'
|
|
)
|
|
// January → EST (UTC-5); DST resolved per wall date, not per import date
|
|
expect(normalizeDateCellValue('2026-01-15 12:00', { timezone: 'America/New_York' })).toBe(
|
|
'2026-01-15T12:00:00-05:00'
|
|
)
|
|
expect(normalizeDateCellValue('7/6/2026 4:04 PM', { timezone: 'America/Los_Angeles' })).toBe(
|
|
'2026-07-06T16:04:00-07:00'
|
|
)
|
|
})
|
|
|
|
it('ignores the zone option when the input carries an explicit offset', () => {
|
|
expect(
|
|
normalizeDateCellValue('2026-07-06T23:04:55.000Z', { timezone: 'America/New_York' })
|
|
).toBe('2026-07-06T23:04:55Z')
|
|
expect(
|
|
normalizeDateCellValue('2026-07-06 16:04:55 PDT', { timezone: 'America/New_York' })
|
|
).toBe('2026-07-06T16:04:55-07:00')
|
|
})
|
|
|
|
it('leaves calendar dates untouched by the zone option', () => {
|
|
expect(normalizeDateCellValue('2026-07-06', { timezone: 'America/New_York' })).toBe(
|
|
'2026-07-06'
|
|
)
|
|
})
|
|
|
|
it('throws on an invalid IANA zone', () => {
|
|
expect(() => normalizeDateCellValue('2026-07-06 12:00', { timezone: 'Not/AZone' })).toThrow(
|
|
RangeError
|
|
)
|
|
})
|
|
|
|
it('returns null for unparseable input', () => {
|
|
expect(normalizeDateCellValue('not-a-date')).toBeNull()
|
|
expect(normalizeDateCellValue('')).toBeNull()
|
|
expect(normalizeDateCellValue('2026-13-45')).toBeNull()
|
|
expect(normalizeDateCellValue('13/06/2026')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('formatDateCellDisplay', () => {
|
|
it('renders calendar dates as MM/DD/YYYY', () => {
|
|
expect(formatDateCellDisplay('2026-07-06')).toBe('07/06/2026')
|
|
})
|
|
|
|
it('renders legacy UTC-midnight instants as their UTC calendar day', () => {
|
|
expect(formatDateCellDisplay('2026-07-06T00:00:00.000Z')).toBe('07/06/2026')
|
|
expect(formatDateCellDisplay('2026-07-06T00:00:00Z')).toBe('07/06/2026')
|
|
})
|
|
|
|
it('renders the literal wall time — identical for every viewer', () => {
|
|
expect(formatDateCellDisplay('2026-07-06T16:04:55-07:00')).toBe('07/06/2026 4:04 PM')
|
|
expect(formatDateCellDisplay('2026-07-06T16:04:55-07:00', { seconds: true })).toBe(
|
|
'07/06/2026 4:04:55 PM'
|
|
)
|
|
// The offset never shifts the displayed wall time
|
|
expect(formatDateCellDisplay('2026-07-06T16:04:55+09:00')).toBe('07/06/2026 4:04 PM')
|
|
expect(formatDateCellDisplay('2026-07-06T23:04:55Z')).toBe('07/06/2026 11:04 PM')
|
|
expect(formatDateCellDisplay('2026-07-06T00:30:00-07:00')).toBe('07/06/2026 12:30 AM')
|
|
})
|
|
|
|
it('omits the seconds suffix when seconds are zero', () => {
|
|
expect(formatDateCellDisplay('2026-07-06T23:04:00Z', { seconds: true })).toBe(
|
|
'07/06/2026 11:04 PM'
|
|
)
|
|
})
|
|
|
|
it('returns unparseable legacy strings as-is', () => {
|
|
expect(formatDateCellDisplay('garbage')).toBe('garbage')
|
|
})
|
|
})
|
|
|
|
describe('storedDateToEditable', () => {
|
|
it('surfaces legacy UTC-midnight instants as their UTC calendar day', () => {
|
|
expect(storedDateToEditable('2026-07-06T00:00:00.000Z')).toBe('2026-07-06')
|
|
})
|
|
|
|
it('keeps calendar dates and canonicalizes instants', () => {
|
|
expect(storedDateToEditable('2026-07-06')).toBe('2026-07-06')
|
|
expect(storedDateToEditable('2026-07-06T16:04:55-07:00')).toBe('2026-07-06T16:04:55-07:00')
|
|
expect(storedDateToEditable('2026-07-06T23:04:55.000Z')).toBe('2026-07-06T23:04:55Z')
|
|
})
|
|
|
|
it('passes unparseable legacy strings through', () => {
|
|
expect(storedDateToEditable('garbage')).toBe('garbage')
|
|
})
|
|
})
|