100 lines
6.0 KiB
TypeScript
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)
|
|
})
|
|
})
|