3da72f1fd1
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
152 lines
5.7 KiB
TypeScript
152 lines
5.7 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
calendarBucket,
|
|
DAY,
|
|
fmtMonth,
|
|
fmtMonthYear,
|
|
formatAgo,
|
|
HOUR,
|
|
MINUTE,
|
|
nominalDayStart,
|
|
SECOND,
|
|
sessionBucketLabel
|
|
} from './time'
|
|
|
|
const labels = {
|
|
ageNow: 'now',
|
|
ageSeconds: (s: number) => `${s}s ago`,
|
|
ageMinutes: (m: number) => `${m}m ago`,
|
|
ageHours: (h: number) => `${h}h ago`,
|
|
ageDays: (d: number) => `${d}d ago`
|
|
}
|
|
|
|
const now = 1_000 * DAY
|
|
const ago = (delta: number) => formatAgo(now - delta, labels, now)
|
|
|
|
describe('formatAgo', () => {
|
|
it('reads "now" under two seconds, then seconds', () => {
|
|
expect(ago(0)).toBe('now')
|
|
expect(ago(1.5 * SECOND)).toBe('now')
|
|
expect(ago(5 * SECOND)).toBe('5s ago')
|
|
})
|
|
|
|
it('buckets to the coarsest unit, floored', () => {
|
|
expect(ago(3 * MINUTE)).toBe('3m ago')
|
|
expect(ago(2 * HOUR + 59 * MINUTE)).toBe('2h ago')
|
|
expect(ago(5 * DAY)).toBe('5d ago')
|
|
})
|
|
|
|
it('clamps future timestamps to "now"', () => {
|
|
expect(ago(-HOUR)).toBe('now')
|
|
})
|
|
})
|
|
|
|
// Thursday 18 Jun 2026, local noon (15 Jun 2026 is a Monday).
|
|
const THU_NOON = new Date(2026, 5, 18, 12, 0, 0).getTime()
|
|
|
|
const secondsAt = (year: number, month: number, day: number, hour = 10) =>
|
|
Math.floor(new Date(year, month, day, hour, 0, 0).getTime() / 1000)
|
|
|
|
describe('nominalDayStart', () => {
|
|
it('rolls the day boundary at 4 AM, not midnight', () => {
|
|
// 1 AM Saturday still belongs to Friday's run.
|
|
expect(nominalDayStart(new Date(2026, 5, 20, 1, 30).getTime())).toBe(new Date(2026, 5, 19).getTime())
|
|
expect(nominalDayStart(new Date(2026, 5, 20, 4, 30).getTime())).toBe(new Date(2026, 5, 20).getTime())
|
|
})
|
|
})
|
|
|
|
describe('calendarBucket', () => {
|
|
// Monday week start: the current week began Mon 15 Jun, last week is Jun 8-14.
|
|
const MONDAY = 1
|
|
|
|
const kindAt = (year: number, month: number, day: number, hour = 10) =>
|
|
calendarBucket(secondsAt(year, month, day, hour), THU_NOON, MONDAY).kind
|
|
|
|
it('buckets the current day (and, defensively, the future) as today', () => {
|
|
// The head run normally absorbs these; "Earlier today" covers the rest.
|
|
expect(kindAt(2026, 5, 18, 5)).toBe('today')
|
|
expect(kindAt(2026, 5, 18, 23)).toBe('today')
|
|
expect(kindAt(2026, 5, 19)).toBe('today')
|
|
})
|
|
|
|
it('assigns the small hours to the previous evening', () => {
|
|
// 1 AM today (before the 4 AM rollover) is part of yesterday's run.
|
|
expect(kindAt(2026, 5, 18, 1)).toBe('yesterday')
|
|
|
|
// And viewed at 00:58, last evening's sessions are still the current day.
|
|
const smallHours = new Date(2026, 5, 19, 0, 58).getTime()
|
|
|
|
expect(calendarBucket(secondsAt(2026, 5, 18, 23), smallHours, MONDAY).kind).toBe('today')
|
|
expect(calendarBucket(secondsAt(2026, 5, 18, 10), smallHours, MONDAY).kind).toBe('today')
|
|
expect(calendarBucket(secondsAt(2026, 5, 17, 15), smallHours, MONDAY).kind).toBe('yesterday')
|
|
})
|
|
|
|
it('uses coarse, non-overlapping ranges that coarsen with age', () => {
|
|
expect(kindAt(2026, 5, 17)).toBe('yesterday')
|
|
expect(kindAt(2026, 5, 16)).toBe('thisWeek') // Tue this week
|
|
expect(kindAt(2026, 5, 15)).toBe('thisWeek') // Mon this week
|
|
expect(kindAt(2026, 5, 14)).toBe('lastWeek') // Sun last week
|
|
expect(kindAt(2026, 5, 8)).toBe('lastWeek') // Mon last week
|
|
expect(kindAt(2026, 5, 7)).toBe('thisMonth') // earlier in June
|
|
expect(kindAt(2026, 5, 1)).toBe('thisMonth')
|
|
expect(kindAt(2026, 4, 28)).toBe('month') // May, same year
|
|
expect(kindAt(2025, 11, 3)).toBe('monthYear') // December, prior year
|
|
})
|
|
|
|
it('respects a Sunday week start', () => {
|
|
// With the week starting Sun 14 Jun, that Sunday is this week, not last.
|
|
expect(calendarBucket(secondsAt(2026, 5, 14), THU_NOON, 0).kind).toBe('thisWeek')
|
|
expect(calendarBucket(secondsAt(2026, 5, 13), THU_NOON, 0).kind).toBe('lastWeek')
|
|
})
|
|
|
|
it('keys same-month sessions together and disambiguates across years', () => {
|
|
expect(calendarBucket(secondsAt(2026, 2, 3), THU_NOON, MONDAY).key).toBe('m-2026-2')
|
|
expect(calendarBucket(secondsAt(2026, 2, 20), THU_NOON, MONDAY).key).toBe('m-2026-2')
|
|
expect(calendarBucket(secondsAt(2025, 2, 3), THU_NOON, MONDAY).key).toBe('my-2025-2')
|
|
})
|
|
})
|
|
|
|
describe('sessionBucketLabel', () => {
|
|
const labels = {
|
|
lastWeek: 'Last week',
|
|
thisMonth: 'Earlier this month',
|
|
thisWeek: 'Earlier this week',
|
|
today: 'Earlier today',
|
|
yesterday: 'Yesterday'
|
|
}
|
|
|
|
const labelAt = (year: number, month: number, day: number) =>
|
|
sessionBucketLabel(calendarBucket(secondsAt(year, month, day), THU_NOON, 1), labels)
|
|
|
|
it('uses fixed labels for the relative buckets', () => {
|
|
expect(labelAt(2026, 5, 18)).toBe('Earlier today')
|
|
expect(labelAt(2026, 5, 17)).toBe('Yesterday')
|
|
expect(labelAt(2026, 5, 16)).toBe('Earlier this week')
|
|
expect(labelAt(2026, 5, 10)).toBe('Last week')
|
|
expect(labelAt(2026, 5, 2)).toBe('Earlier this month')
|
|
})
|
|
|
|
it('formats month (same year) and month + year (prior year) via Intl', () => {
|
|
// Locale-agnostic contract: same-year month buckets render via fmtMonth,
|
|
// prior-year buckets via fmtMonthYear. Assert against the shared
|
|
// formatters instead of frozen en-US strings so the test passes under
|
|
// any host locale (the formatters intentionally use the runtime locale).
|
|
const monthBucket = calendarBucket(secondsAt(2026, 2, 3), THU_NOON, 1)
|
|
|
|
if (monthBucket.kind !== 'month') {
|
|
throw new Error(`expected month bucket, got ${monthBucket.kind}`)
|
|
}
|
|
|
|
expect(sessionBucketLabel(monthBucket, labels)).toBe(fmtMonth.format(monthBucket.at))
|
|
|
|
const monthYearBucket = calendarBucket(secondsAt(2025, 11, 3), THU_NOON, 1)
|
|
|
|
if (monthYearBucket.kind !== 'monthYear') {
|
|
throw new Error(`expected monthYear bucket, got ${monthYearBucket.kind}`)
|
|
}
|
|
|
|
expect(sessionBucketLabel(monthYearBucket, labels)).toBe(fmtMonthYear.format(monthYearBucket.at))
|
|
})
|
|
})
|