59 lines
5.1 KiB
TypeScript
59 lines
5.1 KiB
TypeScript
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'])
|
||
})
|
||
})
|