2026-10-09 11:01:41 +08:00
import { mount } from '@vue/test-utils'
import { describe , expect , it } from 'vitest'
import AppointmentCalendarPanel from './AppointmentCalendarPanel.vue'
2026-10-19 12:20:13 +08:00
import { scheduleTypes , type ScheduleEvent } from './schedule'
2026-10-21 11:38:19 +08:00
import { projectTripCalendar , type TripCalendarEntry } from './tripCalendar'
2026-10-09 11:01:41 +08:00
2026-10-19 12:20:13 +08:00
const event = ( type : ScheduleEvent [ 'type' ], index = 0 ) : ScheduleEvent => ({ key : ` ${ type } - ${ index } ` , type , date : '2026-10-19' , time : '' , title : `TEST-CONT ${ type } ` , fields : [{ label : '仓库' , value : 'ONT8' }] })
2026-10-21 11:38:19 +08:00
const render = ( events : ScheduleEvent [] = [], initialDate = '2026-10-19' ) => mount ( AppointmentCalendarPanel , { props : { initialDate , events },
2026-10-19 12:20:13 +08:00
global : { stubs : { ElButton : { template : '<button><slot /></button>' } } } })
2026-10-21 11:38:19 +08:00
function reportedDispatch() {
2026-10-22 18:03:45 +08:00
const row : TripCalendarEntry = { sourceType : 'DISPATCH' , dispatchNo : 'PC-CALENDAR-001' , containerNumbers : [ 'TSTU1234567' , 'TSTU7654321' ], cabinetNumbers : [ 'D1821' , 'D1822' ], status : 'PLANNED' , originWarehouseId : '1' , originWarehouseCode : 'LAX' , originWarehouseName : '洛杉矶仓' , originCountryCode : 'US' , originTimezone : 'America/Los_Angeles' , originWarehouseStatus : 'ACTIVE' , plannedDepartureAt : '2026-10-22T16:00:00Z' , expectedDriverEndAt : null , departedAt : null , completedAt : null , stops : [{ name : '洛杉矶仓' }, { name : 'ONT8' }], incompleteReasons : [] }
2026-10-21 11:38:19 +08:00
return projectTripCalendar ({ records : [ row , row ], unresolvedRecords : [] }, { from : '2026-10-01' , to : '2026-11-01' }). events
}
2026-10-19 12:20:13 +08:00
describe ( 'four-type appointment calendar' , () => {
2026-10-22 18:03:45 +08:00
it . each ([ 'TSTU1234567' , 'tstu1234567' , 'TSTU7654321' , ' tstu7654321 ' , 'D1821' , ' d1821 ' , 'D1822' , '1821' ])( 'finds the one shared dispatch by associated container %s on its origin-local date, not the Beijing date' , async keyword => {
2026-10-21 11:38:19 +08:00
const wrapper = render ( reportedDispatch (), '2026-10-23' )
const search = wrapper . get ( '[aria-label="搜索当前日历"]' )
expect ( search . attributes ( 'placeholder' )). toBe ( '柜号、排车单号、仓库' )
await search . setValue ( keyword )
expect ( wrapper . findAll ( '.detail-event' )). toHaveLength ( 0 )
expect ( wrapper . get ( '[aria-label="当天安排"]' ). text ()). toContain ( '当天暂无安排' )
expect ( wrapper . get ( '[data-date="2026-10-23"]' ). findAll ( '.event-chip' )). toHaveLength ( 0 )
expect ( wrapper . get ( '[data-date="2026-10-22"]' ). findAll ( '.event-chip' )). toHaveLength ( 1 )
await wrapper . get ( '[data-date="2026-10-22"]' ). trigger ( 'click' )
expect ( wrapper . findAll ( '.detail-event' )). toHaveLength ( 1 )
expect ( wrapper . get ( '.detail-event' ). text ()). toContain ( 'PC-CALENDAR-001' )
expect ( wrapper . get ( '.detail-event' ). text ()). toContain ( '09:00' )
expect ( wrapper . get ( '.detail-event' ). text ()). toContain ( 'TSTU1234567、TSTU7654321' )
2026-10-22 18:03:45 +08:00
expect ( wrapper . get ( '.detail-event' ). text ()). toContain ( 'D1821、D1822' )
2026-10-21 11:38:19 +08:00
expect ( wrapper . get ( '.detail-event' ). text ()). toContain ( '2026-10-22 09:00' )
expect ( wrapper . emitted ( 'range' )). toBeUndefined ()
wrapper . unmount ()
})
it ( 'does not match an unrelated container and can find the same event by the dispatch number' , async () => {
const wrapper = render ( reportedDispatch (), '2026-10-22' )
await wrapper . get ( '[aria-label="搜索当前日历"]' ). setValue ( 'UNRELATED9999' )
expect ( wrapper . findAll ( '.detail-event' )). toHaveLength ( 0 )
expect ( wrapper . findAll ( '.event-chip' )). toHaveLength ( 0 )
await wrapper . get ( '[aria-label="搜索当前日历"]' ). setValue ( 'PC-CALENDAR-001' )
expect ( wrapper . findAll ( '.detail-event' )). toHaveLength ( 1 )
expect ( wrapper . findAll ( '.event-chip' )). toHaveLength ( 1 )
expect ( wrapper . emitted ( 'range' )). toBeUndefined ()
wrapper . unmount ()
})
2026-10-19 12:20:13 +08:00
it ( 'shows all four types by default with a calendar and persistent daily detail' , () => {
const wrapper = render ( scheduleTypes . map ( type => event ( type )))
expect ( wrapper . findAll ( '[aria-label="安排类型"] input' )). toHaveLength ( 4 )
expect ( wrapper . get ( '[aria-label="安排类型"]' ). text ()). toBe ( '到港排车还柜计划到仓' )
expect ( wrapper . findAll ( '.detail-event' )). toHaveLength ( 4 )
expect ( wrapper . get ( '[aria-label="当天安排"] header' ). text ()). toContain ( '4 项安排' )
2026-10-19 17:02:27 +08:00
expect ( wrapper . findAll ( '.calendar-day' )). toHaveLength ( 35 )
expect ( wrapper . find ( '[data-date="2026-11-01"]' ). exists ()). toBe ( true )
expect ( wrapper . find ( '[data-date="2026-11-02"]' ). exists ()). toBe ( false )
2026-10-19 12:20:13 +08:00
expect ( wrapper . text ()). not . toMatch ( /新增预约|全局待办|需复核|中转|直送/ )
2026-10-12 11:45:00 +08:00
})
2026-10-19 12:20:13 +08:00
it ( 'filters locally without requesting another range' , async () => {
const wrapper = render ([ event ( 'arrival' ), event ( 'dispatch' )])
await wrapper . get ( '[aria-label="安排类型"] input' ). setValue ( false )
2026-10-10 15:33:10 +08:00
expect ( wrapper . findAll ( '.detail-event' )). toHaveLength ( 1 )
2026-10-19 12:20:13 +08:00
await wrapper . get ( '[aria-label="搜索当前日历"]' ). setValue ( 'missing' )
expect ( wrapper . text ()). toContain ( '当天暂无安排' )
2026-10-10 15:33:10 +08:00
expect ( wrapper . emitted ( 'range' )). toBeUndefined ()
})
2026-10-19 17:02:27 +08:00
it ( 'offers creation in the header only when authorized' , async () => {
const wrapper = render ()
expect ( wrapper . findAll ( 'button' ). some ( button => button . text () === '新增预约' )). toBe ( false )
await wrapper . setProps ({ canCreate : true })
await wrapper . findAll ( '.calendar-controls button' ). find ( button => button . text () === '新增预约' ) ! . trigger ( 'click' )
expect ( wrapper . emitted ( 'create' )). toEqual ([[]])
expect ( wrapper . emitted ( 'range' )). toBeUndefined ()
wrapper . unmount ()
})
2026-10-19 12:20:13 +08:00
it ( 'changes the selected date locally and requests one range per mode or period change' , async () => {
2026-10-09 11:01:41 +08:00
const wrapper = render ()
2026-10-19 12:20:13 +08:00
await wrapper . get ( '[data-date="2026-10-20"]' ). trigger ( 'click' )
2026-10-09 11:01:41 +08:00
expect ( wrapper . emitted ( 'range' )). toBeUndefined ()
2026-10-19 12:20:13 +08:00
expect ( wrapper . get ( '[aria-label="当天安排"]' ). text ()). toContain ( '2026-10-20' )
2026-10-09 11:01:41 +08:00
await wrapper . get ( '[aria-label="预约视图"]' ). findAll ( 'button' )[ 1 ] ! . trigger ( 'click' )
2026-10-19 12:20:13 +08:00
expect ( wrapper . emitted ( 'range' )). toEqual ([[{ from : '2026-10-19' , to : '2026-10-26' }]])
2026-10-09 11:01:41 +08:00
await wrapper . get ( '[aria-label="下一周"]' ). trigger ( 'click' )
2026-10-19 12:20:13 +08:00
expect ( wrapper . emitted ( 'range' ) ? . at ( - 1 )). toEqual ([{ from : '2026-10-26' , to : '2026-11-02' }])
2026-10-09 11:01:41 +08:00
})
2026-10-19 12:20:13 +08:00
it ( 'limits previews to three while retaining every event in the daily details' , () => {
const wrapper = render ( Array . from ({ length : 23 }, ( _ , index ) => event ( 'arrival' , index )))
expect ( wrapper . get ( '[data-date="2026-10-19"]' ). findAll ( '.event-chip' )). toHaveLength ( 3 )
expect ( wrapper . get ( '.event-more' ). text ()). toBe ( '还有 20 条' )
expect ( wrapper . findAll ( '.detail-event' )). toHaveLength ( 23 )
2026-10-09 12:03:25 +08:00
})
2026-10-19 12:20:13 +08:00
it ( 'keeps undated records outside date cells and exposes them explicitly' , async () => {
const wrapper = render ([{ ... event ( 'warehouse' ), date : null }])
expect ( wrapper . findAll ( '.event-chip' )). toHaveLength ( 0 )
await wrapper . get ( '.undated-button' ). trigger ( 'click' )
expect ( wrapper . findAll ( '.detail-event' )). toHaveLength ( 1 )
expect ( wrapper . get ( '[aria-label="当天安排"]' ). text ()). toContain ( '日期未定' )
2026-10-09 11:01:41 +08:00
})
2026-10-19 12:20:13 +08:00
it ( 'retains business links and exposes editing only with authoritative eligibility' , async () => {
const row = { ... event ( 'warehouse' ), sourcePath : '/container-loading/overseas/1' , canEdit : true }
const wrapper = render ([ row ])
await wrapper . get ( '.detail-event footer' ). findAll ( 'button' )[ 0 ] ! . trigger ( 'click' )
await wrapper . get ( '.detail-event footer' ). findAll ( 'button' )[ 1 ] ! . trigger ( 'click' )
expect ( wrapper . emitted ( 'open' )). toEqual ([[ row ]])
expect ( wrapper . emitted ( 'edit' )). toEqual ([[ row ]])
await wrapper . setProps ({ events : [{ ... row , canEdit : false , sourcePath : undefined }] })
expect ( wrapper . find ( '.detail-event footer' ). exists ()). toBe ( false )
})
it ( 'distinguishes loading and partial failure from a complete empty day' , async () => {
2026-10-09 22:05:51 +08:00
const wrapper = render ()
2026-10-19 12:20:13 +08:00
await wrapper . setProps ({ loading : true })
expect ( wrapper . text ()). toContain ( '正在读取安排' )
expect ( wrapper . text ()). not . toContain ( '当天暂无安排' )
await wrapper . setProps ({ loading : false , incomplete : true })
2026-10-21 17:22:25 +08:00
expect ( wrapper . get ( '[aria-label="当天安排"] header' ). text ()). toContain ( '当前已加载 0 项安排' )
expect ( wrapper . text ()). not . toContain ( '部分数据不可用' )
2026-10-19 12:20:13 +08:00
expect ( wrapper . text ()). not . toContain ( '当天暂无安排' )
2026-10-09 22:05:51 +08:00
})
2026-10-09 11:01:41 +08:00
})