import { mount } from '@vue/test-utils' import { describe, expect, it } from 'vitest' import AppointmentCalendarPanel from './AppointmentCalendarPanel.vue' import { scheduleTypes, type ScheduleEvent } from './schedule' import { projectTripCalendar, type TripCalendarEntry } from './tripCalendar' 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' }] }) const render = (events: ScheduleEvent[] = [], initialDate = '2026-10-19') => mount(AppointmentCalendarPanel, { props: { initialDate, events }, global: { stubs: { ElButton: { template: '' } } } }) function reportedDispatch() { const row: TripCalendarEntry = { sourceType: 'DISPATCH', dispatchNo: 'PC-CALENDAR-001', containerNumbers: ['TSTU1234567', 'TSTU7654321'], 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: [] } return projectTripCalendar({ records: [row, row], unresolvedRecords: [] }, { from: '2026-10-01', to: '2026-11-01' }).events } describe('four-type appointment calendar', () => { it.each(['TSTU1234567', 'tstu1234567', 'TSTU7654321', ' tstu7654321 '])('finds the one shared dispatch by associated container %s on its origin-local date, not the Beijing date', async keyword => { 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') 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() }) 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 项安排') 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) expect(wrapper.text()).not.toMatch(/新增预约|全局待办|需复核|中转|直送/) }) it('filters locally without requesting another range', async () => { const wrapper = render([event('arrival'), event('dispatch')]) await wrapper.get('[aria-label="安排类型"] input').setValue(false) expect(wrapper.findAll('.detail-event')).toHaveLength(1) await wrapper.get('[aria-label="搜索当前日历"]').setValue('missing') expect(wrapper.text()).toContain('当天暂无安排') expect(wrapper.emitted('range')).toBeUndefined() }) 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() }) it('changes the selected date locally and requests one range per mode or period change', async () => { const wrapper = render() await wrapper.get('[data-date="2026-10-20"]').trigger('click') expect(wrapper.emitted('range')).toBeUndefined() expect(wrapper.get('[aria-label="当天安排"]').text()).toContain('2026-10-20') await wrapper.get('[aria-label="预约视图"]').findAll('button')[1]!.trigger('click') expect(wrapper.emitted('range')).toEqual([[{ from: '2026-10-19', to: '2026-10-26' }]]) await wrapper.get('[aria-label="下一周"]').trigger('click') expect(wrapper.emitted('range')?.at(-1)).toEqual([{ from: '2026-10-26', to: '2026-11-02' }]) }) 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) }) 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('日期未定') }) 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 () => { const wrapper = render() await wrapper.setProps({ loading: true }) expect(wrapper.text()).toContain('正在读取安排') expect(wrapper.text()).not.toContain('当天暂无安排') await wrapper.setProps({ loading: false, incomplete: true }) expect(wrapper.get('[aria-label="当天安排"] header').text()).toContain('当前已加载 0 项安排') expect(wrapper.text()).not.toContain('部分数据不可用') expect(wrapper.text()).not.toContain('当天暂无安排') }) })