Files
admin-web/src/modules/appointment-calendar/tripCalendar.test.ts
T

59 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it, vi } from 'vitest'
import { listTripCalendar, projectTripCalendar, tripQueryWindow, type TripCalendarEntry } from './tripCalendar'
import { http } from '@/shared/api/http'
vi.mock('@/shared/api/http', () => ({ http: { get: vi.fn() }, unwrapApiResponse: (value: {data: unknown}) => value.data }))
const range = { from: '2026-10-01', to: '2026-11-01' }
function trip(tripNo: string, plannedDepartureAt: string, originTimezone = 'America/Los_Angeles'): TripCalendarEntry {
return { sourceType: 'TRIP_PLAN', tripNo, status: 'PLANNED', originWarehouseId: '1', originWarehouseCode: 'US-W', originWarehouseName: 'US warehouse', originCountryCode: 'US', originTimezone, originWarehouseStatus: 'ACTIVE', plannedDepartureAt, expectedDriverEndAt: null, departedAt: null, completedAt: null, stops: [{name:'US warehouse'}, {name:'FBA'}], incompleteReasons: [] }
}
describe('location-based trip calendar', () => {
it('counts a shared trip once and excludes cancelled arrangements', () => {
const shared = trip('TR-SHARED', '2026-10-10T12:00:00Z')
const cancelled = { ...trip('TR-CANCELLED', '2026-10-10T12:00:00Z'), status: 'CANCELLED' }
const result = projectTripCalendar({ records: [shared, shared, cancelled], unresolvedRecords: [] }, range)
expect(result.events).toHaveLength(1)
expect(result.events[0]).toMatchObject({ type: 'dispatch', title: 'TR-SHARED 排车' })
})
it('queries one padded UTC window for 42 location dates without frontend page scanning', async () => {
const localRange = {from:'2026-09-28',to:'2026-11-09'}
expect(tripQueryWindow(localRange)).toEqual({from:'2026-09-27T10:00:00.000Z',to:'2026-11-09T14:00:00.000Z'})
vi.mocked(http.get).mockResolvedValue({data:{records:[],unresolvedRecords:[]}})
await listTripCalendar(localRange)
expect(http.get).toHaveBeenCalledExactlyOnceWith('/platform/v1/last-leg/fleet/trips/calendar',{params:tripQueryWindow(localRange),signal:undefined})
expect(() => tripQueryWindow({from:'2026-10-01',to:'2026-11-20'})).toThrow()
})
it('includes the first and last local day and removes padded rows outside each origin date range', () => {
const records=[trip('LA-before','2026-10-01T06:30:00Z'),trip('NY-start','2026-10-01T06:30:00Z','America/New_York'),trip('LA-end','2026-11-01T06:30:00Z'),trip('NY-after','2026-11-01T06:30:00Z','America/New_York')]
const projected=projectTripCalendar({records,unresolvedRecords:[]},range)
expect(projected.events.map(event=>[event.key,event.date,event.time])).toEqual([['trip-NY-start','2026-10-01','02:30'],['trip-LA-end','2026-10-31','23:30']])
})
it('does not guess a slot for absent, fixed-offset, invalid or browser-unsupported time zones', () => {
const unknown={...trip('unknown','2026-10-10T12:00:00Z'),originTimezone:null,incompleteReasons:['ORIGIN_TIMEZONE_UNKNOWN']}
const result=projectTripCalendar({records:[trip('offset','2026-10-10T12:00:00Z','UTC-5'),trip('bad','2026-10-10T12:00:00Z','Mars/Base'),trip('naive','2026-10-10T12:00:00')],unresolvedRecords:[unknown]},range)
expect(result.events).toEqual([]);expect(result.unresolved.map(row=>row.tripNo)).toEqual(['unknown','offset','bad','naive'])
})
it('uses actual local departure through DST changes while retaining the original plan for comparison', () => {
const row={...trip('TR-DST','2026-11-01T05:30:00Z','America/New_York'),departedAt:'2026-11-02T05:00:00Z',stops:Array.from({length:20},(_,i)=>({name:`Stop ${i}`}))}
const events=projectTripCalendar({records:[row,trip('TR-DST-2','2026-11-01T06:30:00Z','America/New_York')],unresolvedRecords:[]},{from:'2026-11-01',to:'2026-11-08'}).events
expect(events.map(event=>event.time)).toEqual(['00:00','01:30'])
expect(events[0]).toMatchObject({date:'2026-11-02',title:'TR-DST 实际发车'})
expect(events[0]!.fields).toContainEqual({label:'计划出发(UTC)',value:'2026-11-01T05:30:00Z'})
expect(events[0]!.fields).toContainEqual({label:'路线',value:'Stop 0 → Stop 1 → Stop 2 等20站'})
})
it('keeps executed trips in the actual month after replanning and excludes their old plan month', () => {
const executed = {...trip('TR-ACTUAL', '2026-10-31T12:00:00Z'), departedAt:'2026-11-01T12:00:00Z'}
expect(projectTripCalendar({records:[executed],unresolvedRecords:[]}, range).events).toEqual([])
const november = {from:'2026-11-01',to:'2026-12-01'}
const event = projectTripCalendar({records:[executed],unresolvedRecords:[]}, november).events[0]
expect(event).toMatchObject({date:'2026-11-01',time:'04:00'})
const changed = {...executed, plannedDepartureAt:'2026-12-10T12:00:00Z'}
expect(projectTripCalendar({records:[changed],unresolvedRecords:[]},november).events[0]).toMatchObject({date:event!.date,time:event!.time})
})
it('does not fall back to the plan when an actual departure cannot be resolved', () => {
const invalid = {...trip('TR-INVALID', '2026-10-15T12:00:00Z'), departedAt:'2026-10-16T12:00:00'}
const result = projectTripCalendar({records:[invalid],unresolvedRecords:[]},range)
expect(result.events).toEqual([])
expect(result.unresolved.map(row=>row.tripNo)).toEqual(['TR-INVALID'])
})
})