146 lines
8.7 KiB
TypeScript
146 lines
8.7 KiB
TypeScript
import ElementPlus, { ElMessageBox } from 'element-plus'
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { readFileSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
import OceanBookingDocumentsPanel from './OceanBookingDocumentsPanel.vue'
|
|
import type { OceanBookingDocument } from '../types'
|
|
import { invalidateOceanBookingDocument, uploadOceanBookingDocument } from '../api/platformOceanBookings'
|
|
|
|
vi.mock('../api/platformOceanBookings', () => ({
|
|
downloadOceanBookingDocument: vi.fn(),
|
|
invalidateOceanBookingDocument: vi.fn(),
|
|
uploadOceanBookingDocument: vi.fn(),
|
|
}))
|
|
|
|
const source = readFileSync(resolve(process.cwd(), 'src/modules/ocean-bookings/components/OceanBookingDocumentsPanel.vue'), 'utf8')
|
|
|
|
describe('OceanBookingDocumentsPanel contract', () => {
|
|
beforeEach(() => { vi.clearAllMocks() })
|
|
it('owns the two confirmed document types under the booking', () => {
|
|
expect(source).toContain("value: 'BOOKING_CONFIRMATION', label: '订舱确认文件'")
|
|
expect(source).toContain("value: 'BILL_OF_LADING', label: '提单文件'")
|
|
expect(source).toContain('文件统一归属当前订舱')
|
|
expect(source).not.toContain('TRUCK_APPOINTMENT_ATTACHMENT')
|
|
})
|
|
|
|
it('supports upload, replacement, logical invalidation and download with version chaining', () => {
|
|
expect(source).toContain('uploadOceanBookingDocument(props.bookingId, props.bookingVersion, type, file)')
|
|
expect(source).toContain('invalidateOceanBookingDocument(props.bookingId, item.id, version)')
|
|
expect(source).toContain('version = invalidated.bookingVersion')
|
|
expect(source).toContain('确认替换')
|
|
expect(source).toContain('确认作废')
|
|
expect(source).toContain('downloadOceanBookingDocument')
|
|
expect(source).toContain('URL.createObjectURL(blob)')
|
|
})
|
|
|
|
it('accepts every file format while rejecting a single file above 50 MiB before upload', () => {
|
|
expect(source).toContain('const maxFileBytes = 50 * 1024 * 1024')
|
|
expect(source).toContain('file.size > maxFileBytes')
|
|
expect(source).toContain('单个订舱资料文件不能超过 50MB,请重新选择')
|
|
expect(source).toContain('任意格式,单文件不超过 50MB')
|
|
expect(source).not.toContain('accept="')
|
|
})
|
|
|
|
it('rejects an oversized arbitrary-format file before calling the backend', async () => {
|
|
const wrapper = mount(OceanBookingDocumentsPanel, {
|
|
props: { bookingId: '123', bookingVersion: 2, bookingStatus: 'DRAFT', documents: [], canManage: true },
|
|
global: { plugins: [ElementPlus] },
|
|
})
|
|
const file = new File(['data'], 'archive.custom', { type: 'application/x-custom' })
|
|
Object.defineProperty(file, 'size', { value: 50 * 1024 * 1024 + 1 })
|
|
const input = wrapper.get('input[type="file"]')
|
|
Object.defineProperty(input.element, 'files', { configurable: true, value: [file] })
|
|
await input.trigger('change')
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('单个订舱资料文件不能超过 50MB')
|
|
expect(uploadOceanBookingDocument).not.toHaveBeenCalled()
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('makes released booking files permanently read-only and preserves traced errors', () => {
|
|
expect(source).toContain("props.bookingStatus === 'DRAFT' && props.canManage")
|
|
expect(source).not.toContain('已放舱 · 永久只读')
|
|
expect(source).toContain('manageReason')
|
|
expect(source).toContain('<ApiErrorMeta :details="error" />')
|
|
expect(source).toContain('订舱资料上传失败,请稍后重试')
|
|
expect(source).toContain('订舱资料下载失败,请稍后重试')
|
|
})
|
|
|
|
it('suppresses duplicate file mutations and protects list-only access', () => {
|
|
expect(source).toContain('if (busyDocumentId.value || busyType.value) return')
|
|
expect(source).toContain('if (busyDocumentId.value || busyType.value || !editable.value) return')
|
|
expect(source).toContain('!file || !editable.value || busyType.value || busyDocumentId.value')
|
|
expect(source).toContain(':disabled="Boolean(busyType || busyDocumentId)"')
|
|
})
|
|
|
|
it('keeps long Unicode active and invalidated file names readable', () => {
|
|
const fileName = '客户资料-中文-🚢-é-'.repeat(16) + '.数据包'
|
|
const document = {
|
|
id: 'doc-1', oceanBookingId: '123', fileId: 'file-1', documentType: 'BILL_OF_LADING' as const,
|
|
fileName, fileHash: null, sizeBytes: 12, contentType: null, valid: true,
|
|
uploadedBy: '7', uploadedByName: '操作员', uploadedAt: '2026-08-31',
|
|
invalidatedBy: null, invalidatedByName: null, invalidatedAt: null,
|
|
}
|
|
const wrapper = mount(OceanBookingDocumentsPanel, {
|
|
props: { bookingId: '123', bookingVersion: 2, bookingStatus: 'DRAFT', documents: [document], canManage: false, manageReason: '暂无订舱资料维护权限' },
|
|
global: { plugins: [ElementPlus] },
|
|
})
|
|
expect(wrapper.get('.booking-document-list strong').attributes('title')).toBe(fileName)
|
|
expect(wrapper.find('input[type="file"]').exists()).toBe(false)
|
|
expect(wrapper.text()).not.toContain('作废')
|
|
expect(source).toContain('.booking-document-history li span{min-width:0;overflow-wrap:anywhere}')
|
|
wrapper.unmount()
|
|
})
|
|
})
|
|
|
|
|
|
function documentFixture(id: string): OceanBookingDocument {
|
|
return { id, oceanBookingId: '123', fileId: `file-${id}`, documentType: 'BOOKING_CONFIRMATION', fileName: `${id}.pdf`, fileHash: null, sizeBytes: 128, contentType: 'application/pdf', valid: true, uploadedBy: 'internal-operator-id', uploadedByName: null, uploadedAt: '2026-10-19T10:00:00', invalidatedBy: null, invalidatedByName: null, invalidatedAt: null }
|
|
}
|
|
afterEach(() => { vi.restoreAllMocks(); document.body.innerHTML = '' })
|
|
describe('OceanBookingDocumentsPanel confirmations', () => {
|
|
beforeEach(() => { vi.clearAllMocks() })
|
|
it('uses the booking number and preserves replacement upload/invalidation version chaining', async () => {
|
|
const old = documentFixture('old')
|
|
const uploaded = documentFixture('new')
|
|
vi.mocked(uploadOceanBookingDocument).mockResolvedValue({ document: uploaded, bookingVersion: 3 })
|
|
vi.mocked(invalidateOceanBookingDocument).mockResolvedValue({ documentId: old.id, bookingVersion: 4 })
|
|
const confirm = vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
|
const wrapper = mount(OceanBookingDocumentsPanel, { props: { bookingId: '123', bookingNo: 'OB-123', bookingVersion: 2, bookingStatus: 'DRAFT', documents: [old], canManage: true }, global: { plugins: [ElementPlus] } })
|
|
expect(wrapper.text()).not.toContain('internal-operator-id')
|
|
const file = new File(['new'], '替换资料.pdf', { type: 'application/pdf' })
|
|
const input = wrapper.findAll('input[type="file"]')[0]!
|
|
Object.defineProperty(input.element, 'files', { configurable: true, value: [file] })
|
|
await input.trigger('change')
|
|
await flushPromises()
|
|
expect(confirm).toHaveBeenCalledWith(expect.stringContaining('替换资料.pdf'), '替换订舱资料 · OB-123', expect.objectContaining({ customClass: 'admin-density-confirm', confirmButtonText: '确认替换' }))
|
|
expect(uploadOceanBookingDocument).toHaveBeenCalledExactlyOnceWith('123', 2, 'BOOKING_CONFIRMATION', file)
|
|
expect(invalidateOceanBookingDocument).toHaveBeenCalledExactlyOnceWith('123', 'old', 3)
|
|
expect(wrapper.emitted('updated')).toEqual([
|
|
[[old, uploaded], 3],
|
|
[[expect.objectContaining({ id: 'old', valid: false }), uploaded], 4],
|
|
])
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('leaves documents unchanged when invalidation is cancelled and uses the current version on confirmation', async () => {
|
|
const old = documentFixture('old')
|
|
const confirm = vi.spyOn(ElMessageBox, 'confirm').mockRejectedValueOnce('cancel').mockResolvedValueOnce(undefined as never)
|
|
vi.mocked(invalidateOceanBookingDocument).mockResolvedValue({ documentId: old.id, bookingVersion: 3 })
|
|
const wrapper = mount(OceanBookingDocumentsPanel, { props: { bookingId: '123', bookingNo: 'OB-123', bookingVersion: 2, bookingStatus: 'DRAFT', documents: [old], canManage: true }, global: { plugins: [ElementPlus] } })
|
|
const button = wrapper.findAll('button').find(item => item.text() === '作废')!
|
|
await button.trigger('click')
|
|
await flushPromises()
|
|
expect(invalidateOceanBookingDocument).not.toHaveBeenCalled()
|
|
expect(wrapper.emitted('updated')).toBeUndefined()
|
|
await button.trigger('click')
|
|
await flushPromises()
|
|
expect(confirm).toHaveBeenLastCalledWith(expect.stringContaining('old.pdf'), '作废订舱资料 · OB-123', expect.objectContaining({ confirmButtonText: '确认作废' }))
|
|
expect(invalidateOceanBookingDocument).toHaveBeenCalledExactlyOnceWith('123', 'old', 2)
|
|
expect(wrapper.emitted('updated')).toEqual([[[expect.objectContaining({ id: 'old', valid: false })], 3]])
|
|
wrapper.unmount()
|
|
})
|
|
})
|