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(dispatchNo: string, plannedDepartureAt: string, originTimezone = 'America/Los_Angeles'): TripCalendarEntry { return { sourceType: 'DISPATCH', dispatchNo, 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('routes the canonical dispatch number to dispatch detail and does not invent a link without it', () => { const row = trip('DS/REAL', '2026-10-10T12:00:00Z') const result = projectTripCalendar({ records: [row], unresolvedRecords: [] }, range) expect(result.events[0]).toMatchObject({ key: 'dispatch-DS/REAL', title: 'DS/REAL 排车', sourcePath: '/overseas-warehouse/dispatches/DS%2FREAL' }) const missing = projectTripCalendar({ records: [{ ...row, dispatchNo: '' }], unresolvedRecords: [] }, range) expect(missing.events).toEqual([]) expect(missing.unresolved[0]!.incompleteReasons).toContain('DISPATCH_REFERENCE_UNKNOWN') }) it('counts a shared trip once and excludes cancelled arrangements', () => { const shared = trip('DS-SHARED', '2026-10-10T12:00:00Z') const cancelled = { ...trip('DS-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: 'DS-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([['dispatch-NY-start','2026-10-01','02:30'],['dispatch-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.dispatchNo)).toEqual(['unknown','offset','bad','naive']) }) it('uses actual local departure through DST changes while retaining the original plan for comparison', () => { const row={...trip('DS-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('DS-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:'DS-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('DS-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('DS-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.dispatchNo)).toEqual(['DS-INVALID']) }) })