Files
simstudioai--sim/apps/sim/lib/table/dates.test.ts
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

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')
})
})