import { mount, flushPromises } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import ElementPlus from 'element-plus'
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 () => [] }))
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) {
const wrapper = mount(ContainerLastLegFeesPanel, { props: { containerBookingId: '99', containerNo: 'CONT-99', readonly }, global: { plugins: [ElementPlus], stubs: {
AppDialog: { props: ['modelValue', 'title', 'confirmDisabled'], template: '' },
} } })
await flushPromises()
return wrapper
}
type FormState = { form: { customerId: string; settlementMode: string }; dialogOpen: boolean }
async function upload(wrapper: Awaited>, 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 () => {
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.get('button').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.get('button').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')
})
})