mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
fix(client): compute "today" from the local calendar, not UTC
This commit is contained in:
@@ -59,3 +59,33 @@ describe('computeJourneyLifecycle', () => {
|
||||
expect(computeJourneyLifecycle('published', null, TODAY)).toBe('live')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* The midnight-window regression: "today" must be the LOCAL calendar date.
|
||||
* The old code derived it from toISOString() (the UTC date), so between local
|
||||
* midnight and the UTC rollover a journey running through yesterday still
|
||||
* showed 'live' and one starting today showed 'upcoming'. Pinned at 00:30
|
||||
* local — vacuous on a UTC machine, biting on any TZ ahead of UTC.
|
||||
*/
|
||||
describe('computeJourneyLifecycle — local today at the midnight window', () => {
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date(2026, 7, 25, 0, 30)) // local 2026-08-25 00:30
|
||||
})
|
||||
afterAll(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('a journey spanning the local today is live', () => {
|
||||
expect(computeJourneyLifecycle('active', '2026-08-25', '2026-08-26')).toBe('live')
|
||||
})
|
||||
|
||||
it('a journey that ended yesterday (local) is completed', () => {
|
||||
expect(computeJourneyLifecycle('active', '2026-08-20', '2026-08-24')).toBe('completed')
|
||||
})
|
||||
|
||||
it('single boundaries follow the local date too', () => {
|
||||
expect(computeJourneyLifecycle('active', '2026-08-25', null)).toBe('live') // starts today → live
|
||||
expect(computeJourneyLifecycle('active', null, '2026-08-24')).toBe('completed') // ended yesterday
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { localIsoDate } from './localDate'
|
||||
|
||||
export type JourneyLifecycle = 'archived' | 'live' | 'upcoming' | 'completed' | 'draft'
|
||||
|
||||
export function computeJourneyLifecycle(
|
||||
@@ -8,7 +10,7 @@ export function computeJourneyLifecycle(
|
||||
if (status === 'archived') return 'archived'
|
||||
|
||||
if (tripDateMin && tripDateMax) {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const today = localIsoDate()
|
||||
if (tripDateMin <= today && today <= tripDateMax) return 'live'
|
||||
if (tripDateMin > today) return 'upcoming'
|
||||
return 'completed'
|
||||
@@ -20,11 +22,11 @@ export function computeJourneyLifecycle(
|
||||
|
||||
// Single boundary: only start or only end
|
||||
if (tripDateMin && !tripDateMax) {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const today = localIsoDate()
|
||||
return tripDateMin > today ? 'upcoming' : 'live'
|
||||
}
|
||||
if (!tripDateMin && tripDateMax) {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const today = localIsoDate()
|
||||
return tripDateMax < today ? 'completed' : 'live'
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { localIsoDate } from './localDate'
|
||||
|
||||
/**
|
||||
* "Today" must be the user's local calendar date. `toISOString()` is the UTC
|
||||
* date — between local midnight and the UTC rollover it is YESTERDAY for any
|
||||
* TZ ahead of UTC, which misclassified trips/journeys in that window. On a UTC
|
||||
* machine local == UTC, so the distinction is vacuous there; these tests pin
|
||||
* the clock at 00:30 local, where any positive offset separates the two.
|
||||
*/
|
||||
describe('localIsoDate', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date(2026, 7, 25, 0, 30)) // local 2026-08-25 00:30
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('formats the local calendar date, zero-padded', () => {
|
||||
expect(localIsoDate()).toBe('2026-08-25')
|
||||
expect(localIsoDate(new Date(2026, 0, 3, 12))).toBe('2026-01-03')
|
||||
})
|
||||
|
||||
it('defaults to now', () => {
|
||||
expect(localIsoDate()).toBe(localIsoDate(new Date()))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* The user's LOCAL calendar date as 'YYYY-MM-DD'.
|
||||
*
|
||||
* Never derive "today" from `toISOString().split('T')[0]` — that is the UTC
|
||||
* date, which disagrees with the wall clock between local midnight and the UTC
|
||||
* rollover (for a UTC+2 user, from 00:00 to 02:00 every night). Trip and
|
||||
* journey dates are wall-clock dates, so every "is this today/past/upcoming"
|
||||
* comparison must use this instead.
|
||||
*
|
||||
* (Converting a Date you deliberately built in UTC — `new Date(s + 'T00:00:00Z')`
|
||||
* round-tripped through date arithmetic — is a different, self-consistent
|
||||
* pattern and fine as it is.)
|
||||
*/
|
||||
export function localIsoDate(d: Date = new Date()): string {
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
Reference in New Issue
Block a user