import { flushPromises, mount, type VueWrapper } from '@vue/test-utils' import ElementPlus from 'element-plus' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { listSailingCalendar } from '../api/platformSailingCalendar' import type { SailingCalendarResponse } from '../types' import SailingCalendarView from './SailingCalendarView.vue' const permissions = vi.hoisted(() => ({ write: true })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => permissions.write }), })) vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) })) vi.mock('../api/platformSailingCalendar', () => ({ listSailingCalendar: vi.fn(), cancelSailing: vi.fn(), copySailing: vi.fn(), createSailingTemplate: vi.fn(), createSailing: vi.fn(), generateSailingTemplate: vi.fn(), getSailing: vi.fn(), getSailingBookingAdvice: vi.fn(), getSailingVolumeDetail: vi.fn(), listSailingChannels: vi.fn().mockResolvedValue([]), listSailingTemplates: vi.fn().mockResolvedValue([]), previewSailingTemplateGeneration: vi.fn(), updateSailingTemplate: vi.fn(), updateSailing: vi.fn(), })) const volume = { actualReceivedCbm: 12, catchableForecastCbm: 0, riskUnmatchedCbm: 0, bookedContainerCount: 0, recommendedContainerCount: 1, pendingContainerCount: 1, lateBatchCount: 0, overCapacity: false } const calendar: SailingCalendarResponse = { month: '2026-09', timezone: 'Asia/Shanghai', summary: { ...volume, sailingCount: 1 }, filterOptions: { warehouses: [], channels: [], departurePorts: [], destinationPorts: [] }, records: [{ id: 'sailing-1', sailingNo: 'SAIL-TEST', warehouseId: 'warehouse-1', warehouseName: '测试仓', channelCode: 'CH-TEST', channelName: '测试渠道', vesselName: 'TEST VESSEL', voyageNo: 'TEST-01', departurePortCode: 'CNSHA', destinationPortCode: 'USLAX', etdAt: '2026-09-30T10:00:00', cutoffAt: '2026-09-29T18:00:00', status: 'OPEN', volumeSummary: volume, version: 1 }], } let wrapper: VueWrapper | undefined async function mountView() { wrapper = mount(SailingCalendarView, { attachTo: document.body, global: { plugins: [ElementPlus] } }) await flushPromises() return wrapper } beforeEach(() => { vi.useFakeTimers({ toFake: ['Date'] }) vi.setSystemTime(new Date(2026, 8, 30, 12)) vi.clearAllMocks() permissions.write = true vi.mocked(listSailingCalendar).mockResolvedValue(calendar) }) afterEach(() => { wrapper?.unmount(); wrapper = undefined; document.body.innerHTML = ''; vi.useRealTimers() }) describe('SailingCalendarView integrated workspace', () => { it('keeps one create entry and defaults the sailing to the selected calendar date', async () => { const view = await mountView() expect(view.find('.detail-header button').exists()).toBe(false) const createButtons = view.findAll('button').filter(button => button.text() === '新增单个船次') expect(createButtons).toHaveLength(1) const otherDate = view.findAll('.calendar-cell').find(cell => cell.get('.day-number').text() === '29')! await otherDate.trigger('click') await createButtons[0].trigger('click') await flushPromises() expect(document.querySelector('.el-dialog__title')?.textContent).toBe('新增船期') const departurePicker = view.findAllComponents({ name: 'ElDatePicker' }).find(picker => picker.props('type') === 'datetime')! expect(departurePicker.props('modelValue')).toBe('2026-09-29T10:00:00') }) it('preserves keyword search, date selection and month navigation with the shared header', async () => { const view = await mountView() expect(view.get('h1').text()).toBe('船期日历') expect(view.get('.data-table__business-actions').text()).toContain('新增单个船次') expect(view.find('button[aria-label="列设置"]').exists()).toBe(false) expect(view.find('.data-table__pagination').exists()).toBe(false) expect(view.get('.detail-panel').text()).toContain('SAIL-TEST') await view.get('.sailing-card').trigger('click') expect(view.get('.detail-actions').text()).toContain('货量明细') const otherDate = view.findAll('.calendar-cell').find(cell => cell.get('.day-number').text() === '29')! await otherDate.trigger('click') expect(view.get('.detail-header h2').text()).toBe('2026-09-29') expect(view.get('.detail-panel').text()).toContain('当天暂无船期') const keyword = view.get('input[placeholder="渠道 / 船名 / 航次 / 船期编号"]') await keyword.setValue('TEST VESSEL') await keyword.trigger('keyup', { key: 'Enter' }) await flushPromises() expect(listSailingCalendar).toHaveBeenLastCalledWith(expect.objectContaining({ month: '2026-09', keyword: 'TEST VESSEL' })) await view.get('.toolbar-filters button').trigger('click') await flushPromises() expect(listSailingCalendar).toHaveBeenLastCalledWith(expect.objectContaining({ keyword: undefined })) await view.get('button[aria-label="下个月"]').trigger('click') await flushPromises() expect(listSailingCalendar).toHaveBeenLastCalledWith(expect.objectContaining({ month: '2026-10' })) }) it('keeps filters and retry available on failure and preserves read-only permissions', async () => { permissions.write = false vi.mocked(listSailingCalendar).mockRejectedValueOnce(new Error('日历加载失败')) const view = await mountView() expect(view.get('h1').text()).toBe('船期日历') expect(view.find('.toolbar-filters').exists()).toBe(true) expect(view.find('.calendar-layout').exists()).toBe(false) expect(view.find('.data-table__business-actions button').exists()).toBe(false) await view.get('.load-error-state__actions button').trigger('click') await flushPromises() expect(view.get('.calendar-layout').attributes('aria-busy')).toBe('false') expect(view.get('.detail-panel').text()).toContain('SAIL-TEST') await view.get('.sailing-card').trigger('click') const operations = view.get('.detail-actions').text() expect(operations).toContain('货量明细') for (const action of ['编辑', '复制下一班', '取消']) expect(operations).not.toContain(action) }) })