Files
admin-web/src/modules/sailing-calendar/pages/SailingCalendarView.test.ts
T
2026-10-19 17:56:31 +08:00

100 lines
6.0 KiB
TypeScript

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)
})
})