152 lines
9.2 KiB
TypeScript
152 lines
9.2 KiB
TypeScript
import { mount, flushPromises } from '@vue/test-utils'
|
||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||
import ElementPlus, { ElFormItem, ElSelect } from 'element-plus'
|
||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||
import ContainerLastLegFeesPanel from './ContainerLastLegFeesPanel.vue'
|
||
|
||
const mocks = vi.hoisted(() => ({ upload: vi.fn(), metadata: vi.fn(), download: vi.fn(), create: vi.fn(), fees: vi.fn() }))
|
||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
|
||
vi.mock('@/modules/finance', () => ({ getFinancePaymentVoucher: mocks.metadata, downloadFinancePaymentVoucher: mocks.download }))
|
||
vi.mock('@/modules/loading-lists', () => ({ uploadContainerFeePaymentVoucher: mocks.upload, listContainerFeeSupplierOptions: async () => [{ id: '21', supplierName: '供应商甲', category: 'TERMINAL_DRAYAGE' }] }))
|
||
vi.mock('../api/overseasContainerFees', () => ({
|
||
listOverseasContainerFees: mocks.fees, listOverseasContainerCustomers: async () => [{ customerId: '11', customerName: '客户甲' }],
|
||
createOverseasContainerFee: mocks.create, adjustOverseasContainerFee: vi.fn(), reviewOverseasContainerFee: vi.fn(), submitOverseasContainerFee: vi.fn(), voidOverseasContainerFee: vi.fn(),
|
||
}))
|
||
|
||
async function setup(readonly = false, realDialog = false, containerNo: string | null = 'CONT-99') {
|
||
const wrapper = mount(ContainerLastLegFeesPanel, { props: { containerBookingId: '99', cabinetNo: 'CAB-99', containerNo, readonly }, global: { plugins: [ElementPlus], stubs: realDialog ? {} : {
|
||
AppDialog: { props: ['modelValue', 'title', 'confirmDisabled'], template: '<section v-if="modelValue"><h3>{{ title }}</h3><slot /><button :disabled="confirmDisabled" @click="$emit(\'confirm\')">保存</button></section>' },
|
||
} } })
|
||
await flushPromises()
|
||
return wrapper
|
||
}
|
||
type FormState = { form: { customerId: string; settlementMode: string; direction: string; amount: number | null; businessDate: string; supplierId: string; paidAt: string; paymentReferenceNo: string }; dialogOpen: boolean }
|
||
async function upload(wrapper: Awaited<ReturnType<typeof setup>>, name: string) {
|
||
const input = wrapper.get('input[type="file"]')
|
||
Object.defineProperty(input.element, 'files', { configurable: true, value: [new File(['image'], name, { type: 'image/png' })] })
|
||
await input.trigger('change')
|
||
await flushPromises()
|
||
}
|
||
describe('尾程付款凭证', () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks()
|
||
mocks.fees.mockResolvedValue([])
|
||
vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} })
|
||
URL.createObjectURL = vi.fn(() => 'blob:voucher')
|
||
URL.revokeObjectURL = vi.fn()
|
||
mocks.metadata.mockResolvedValue({ fileId: '91', fileName: '付款.png', contentType: 'image/png' })
|
||
mocks.download.mockResolvedValue(new Blob(['image']))
|
||
})
|
||
|
||
it('新增连续表切换付款字段和全屏后保留输入,保存仍使用真实集装箱号', async () => {
|
||
const wrapper = await setup(false, true)
|
||
await wrapper.findAll('button').find(button => button.text() === '新增费用')!.trigger('click')
|
||
await flushPromises()
|
||
const state = (wrapper.vm.$ as unknown as { setupState: FormState }).setupState
|
||
const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title').startsWith('新增尾程费用'))!
|
||
expect(dialog.props('title')).toBe('新增尾程费用 · CAB-99')
|
||
const labels = () => dialog.findAllComponents(ElFormItem).map(item => item.props('label'))
|
||
expect(labels()).toHaveLength(7)
|
||
state.form.settlementMode = 'DIRECT_PAID'
|
||
state.form.amount = 120.25
|
||
state.form.supplierId = '21'
|
||
state.form.businessDate = '2026-10-19'
|
||
state.form.paidAt = '2026-10-19T09:00:00'
|
||
state.form.paymentReferenceNo = ' PAY-001 '
|
||
await flushPromises()
|
||
expect(labels()).toHaveLength(11)
|
||
await dialog.get('[aria-label="展开全屏"]').trigger('click')
|
||
expect(dialog.find('.el-dialog.is-fullscreen').exists()).toBe(true)
|
||
expect(state.form.amount).toBe(120.25)
|
||
await dialog.get('[aria-label="退出全屏"]').trigger('click')
|
||
expect(state.form.paymentReferenceNo).toBe(' PAY-001 ')
|
||
dialog.vm.$emit('confirm')
|
||
await flushPromises()
|
||
expect(mocks.create).toHaveBeenCalledWith(expect.objectContaining({
|
||
customerId: '11', containerBookingId: '99', containerNo: 'CONT-99', businessNo: 'CONT-99',
|
||
amount: 120.25, supplierId: '21', settlementMode: 'DIRECT_PAID',
|
||
paidAt: '2026-10-19T09:00:00', paymentReferenceNo: 'PAY-001', currency: 'CNY',
|
||
}), expect.stringContaining('container-fee-create-'))
|
||
wrapper.unmount()
|
||
})
|
||
|
||
it('调整的所属客户显示只读名称,应收费用隐藏供应商和付款字段', async () => {
|
||
mocks.fees.mockResolvedValue([{ sourceFeeId: '81', sourceVersion: 7, customerId: '11', customerName: '客户甲', feeItemCode: 'PORT_FEE', amount: 10, businessDate: '2026-10-19', direction: 'RECEIVABLE', reviewStatus: 'APPROVED', sourceStatus: 'ACTIVE', paymentVoucherFileIds: [] }])
|
||
const wrapper = await setup(false, true)
|
||
await wrapper.findAll('button').find(button => button.text() === '调整')!.trigger('click')
|
||
await flushPromises()
|
||
const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title').startsWith('调整尾程费用'))!
|
||
const fields = dialog.findAllComponents(ElFormItem)
|
||
expect(fields.map(item => item.props('label'))).toEqual(['客户', '费用项目', '费用方向', '业务日期', '金额(CNY)'])
|
||
const customer = fields[0]!
|
||
expect(customer.text()).toContain('客户甲')
|
||
expect(customer.findComponent(ElSelect).exists()).toBe(false)
|
||
wrapper.unmount()
|
||
})
|
||
|
||
it('只有柜次号仍不能维护费用,保留补录真实集装箱号门禁', async () => {
|
||
const wrapper = await setup(false, true, null)
|
||
expect(wrapper.findAll('button').find(button => button.text() === '新增费用')!.attributes('disabled')).toBeDefined()
|
||
expect(wrapper.text()).toContain('尚未补录真实集装箱号')
|
||
expect(mocks.create).not.toHaveBeenCalled()
|
||
wrapper.unmount()
|
||
})
|
||
|
||
it('作废待财务确认时保留有效来源,停止调整和重复作废', async () => {
|
||
mocks.fees.mockResolvedValue([{ sourceFeeId: '81', customerId: '11', amount: 10, reviewStatus: 'APPROVED', sourceStatus: 'ACTIVE', voidStatus: 'PENDING_FINANCE', paymentVoucherFileIds: [] }])
|
||
const wrapper = await setup()
|
||
expect(wrapper.text()).toContain('待财务确认')
|
||
expect(wrapper.text()).not.toContain('已作废')
|
||
expect(wrapper.findAll('button').some(button => ['调整', '作废'].includes(button.text()))).toBe(false)
|
||
wrapper.unmount()
|
||
})
|
||
|
||
it('显示上传文件名,允许再次上传替换和移除', async () => {
|
||
const wrapper = await setup()
|
||
await wrapper.findAll('button').find(button => button.text() === '新增费用')!.trigger('click')
|
||
const state = (wrapper.vm.$ as unknown as { setupState: FormState }).setupState
|
||
state.form.settlementMode = 'DIRECT_PAID'
|
||
await flushPromises()
|
||
mocks.upload.mockResolvedValueOnce({ fileId: '91', fileName: '付款.png' }).mockResolvedValueOnce({ fileId: '92', fileName: '新凭证.png' })
|
||
await upload(wrapper, '付款.png')
|
||
expect(mocks.upload).toHaveBeenCalledWith('11', expect.any(File))
|
||
expect(wrapper.text()).toContain('付款.png')
|
||
await upload(wrapper, '新凭证.png')
|
||
expect(wrapper.text()).toContain('新凭证.png')
|
||
expect(wrapper.text()).not.toContain('付款.png')
|
||
await wrapper.findAll('button').find(b => b.text() === '移除')!.trigger('click')
|
||
expect(wrapper.text()).not.toContain('新凭证.png')
|
||
wrapper.unmount()
|
||
})
|
||
|
||
it('上传中禁止保存,客户切换后忽略旧客户上传结果', async () => {
|
||
const wrapper = await setup()
|
||
await wrapper.findAll('button').find(button => button.text() === '新增费用')!.trigger('click')
|
||
const state = (wrapper.vm.$ as unknown as { setupState: FormState }).setupState
|
||
state.form.settlementMode = 'DIRECT_PAID'
|
||
await flushPromises()
|
||
let complete!: (file: { fileId: string; fileName: string }) => void
|
||
mocks.upload.mockReturnValue(new Promise(resolve => { complete = resolve }))
|
||
await upload(wrapper, '旧客户.png')
|
||
expect(wrapper.findAll('button').find(b => b.text() === '保存')!.attributes('disabled')).toBeDefined()
|
||
state.form.customerId = '12'
|
||
complete({ fileId: '91', fileName: '旧客户.png' })
|
||
await flushPromises()
|
||
expect(wrapper.text()).not.toContain('旧客户.png')
|
||
wrapper.unmount()
|
||
})
|
||
|
||
it('只读列表能查看已保存的待审核凭证,并在关闭时释放预览资源', async () => {
|
||
mocks.fees.mockResolvedValue([{ sourceFeeId: '81', customerId: '11', amount: 10, reviewStatus: 'PENDING', paymentVoucherFileIds: ['91'] }])
|
||
const wrapper = await setup(true)
|
||
await wrapper.findAll('button').find(b => b.text() === '付款凭证 1')!.trigger('click')
|
||
await flushPromises()
|
||
expect(mocks.metadata).toHaveBeenCalledWith('11', '91')
|
||
expect(mocks.download).toHaveBeenCalledWith('11', '91')
|
||
expect(wrapper.get('img').attributes('src')).toBe('blob:voucher')
|
||
expect(wrapper.get('a[download]').attributes('download')).toBe('付款.png')
|
||
wrapper.unmount()
|
||
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:voucher')
|
||
})
|
||
})
|