This commit is contained in:
2026-10-19 17:56:31 +08:00
parent 5a46198bf0
commit 5c28cd7c79
84 changed files with 3356 additions and 3630 deletions
@@ -39,6 +39,90 @@ beforeEach(() => {
})
describe('ContainerFeesPanel contract', () => {
it.each(['success', 'failure'])('ignores a closed editor request that finishes late: %s', async (outcome) => {
const item = { id: '7', name: '操作费', feeCode: 'OPERATION', defaultPrice: 60, supplierCategory: null, warehouseReceivable: true, warehousePayable: false, pickupReceivable: false, pickupPayable: false }
let resolveOld!: (items: typeof item[]) => void
let rejectOld!: (reason: Error) => void
vi.mocked(listApplicableFeeItems).mockImplementationOnce(() => new Promise((resolve, reject) => { resolveOld = resolve; rejectOld = reject }))
vi.mocked(listApplicableFeeItems).mockResolvedValueOnce([{ ...item, defaultPrice: 80 }])
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
const state = (wrapper.vm.$ as unknown as { setupState: {
openCreate(): void
editorOpen: boolean
form: { amount: number }
feeItems: typeof item[]
feeItemsLoading: boolean
feeItemsError: unknown
} }).setupState
state.openCreate(); await flushPromises()
state.editorOpen = false; await flushPromises()
state.openCreate(); await flushPromises()
expect(state.feeItems[0]?.defaultPrice).toBe(80)
if (outcome === 'success') resolveOld([item])
else rejectOld(new Error('旧窗口请求失败'))
await flushPromises()
expect(state.feeItemsError).toBeNull()
expect(state.feeItemsLoading).toBe(false)
const select = wrapper.findAllComponents({ name: 'ElFormItem' }).find((field) => field.props('prop') === 'feeItemId')!.findComponent({ name: 'ElSelect' })
select.vm.$emit('update:modelValue', '7'); select.vm.$emit('change', '7')
await flushPromises()
expect(state.form.amount).toBe(80)
wrapper.unmount()
})
it('does not let an old completion clear the current loading state or refill a closed editor', async () => {
const item = { id: '7', name: '操作费', feeCode: 'OPERATION', defaultPrice: 60, supplierCategory: null, warehouseReceivable: true, warehousePayable: false, pickupReceivable: false, pickupPayable: false }
let resolveOld!: (items: typeof item[]) => void
let resolveCurrent!: (items: typeof item[]) => void
vi.mocked(listApplicableFeeItems)
.mockImplementationOnce(() => new Promise((resolve) => { resolveOld = resolve }))
.mockImplementationOnce(() => new Promise((resolve) => { resolveCurrent = resolve }))
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
const state = (wrapper.vm.$ as unknown as { setupState: {
openCreate(): void; editorOpen: boolean; feeItems: unknown[]; feeItemsLoading: boolean; feeItemsError: unknown
} }).setupState
state.openCreate(); await flushPromises()
state.editorOpen = false
state.openCreate(); await flushPromises()
resolveOld([item]); await flushPromises()
expect(state.feeItems).toEqual([])
expect(state.feeItemsLoading).toBe(true)
state.editorOpen = false; await flushPromises()
resolveCurrent([{ ...item, defaultPrice: 80 }]); await flushPromises()
expect(state.feeItems).toEqual([])
expect(state.feeItemsLoading).toBe(false)
expect(state.feeItemsError).toBeNull()
wrapper.unmount()
})
it('prefills the selected default, clears stale prices and retains manual overrides', async () => {
const base = { name: '操作费', feeCode: 'OPERATION', supplierCategory: null, warehouseReceivable: true, warehousePayable: false, pickupReceivable: false, pickupPayable: false }
vi.mocked(listApplicableFeeItems).mockResolvedValue([
{ ...base, id: '7', defaultPrice: 60 }, { ...base, id: '8', defaultPrice: null }, { ...base, id: '9', defaultPrice: 0 },
])
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
const state = (wrapper.vm.$ as unknown as { setupState: {
openCreate(): void; save(): Promise<void>
resetForm(row: Record<string, unknown>): void
form: { feeItemId: string; amount: number }
} }).setupState
state.resetForm({ feeItemId: '7', direction: 'RECEIVABLE', businessDate: '2026-08-12', amount: 100 })
await flushPromises()
expect(state.form.amount).toBe(100)
state.openCreate(); await flushPromises()
const select = wrapper.findAllComponents({ name: 'ElFormItem' }).find((item) => item.props('prop') === 'feeItemId')!.findComponent({ name: 'ElSelect' })
for (const [id, price] of [['7', 60], ['8', 0], ['9', 0]] as const) {
select.vm.$emit('update:modelValue', id); select.vm.$emit('change', id)
await flushPromises()
expect(state.form.amount).toBe(price)
}
select.vm.$emit('update:modelValue', '7'); select.vm.$emit('change', '7')
await flushPromises()
state.form.amount = 75
await state.save(); await flushPromises()
expect(createContainerFee).toHaveBeenCalledWith('9', '11', expect.objectContaining({ feeItemId: '7', amount: 75 }))
})
it('keeps fee controls and table utilities in compact aligned toolbars', () => {
expect(panel).toContain('class="fees-header__actions"')
expect(panel).toContain('class="customer-context"')
@@ -179,7 +263,7 @@ describe('ContainerFeesPanel contract', () => {
})
it('keeps receivable usable when supplier permission is missing', async () => {
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'OPERATION', supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'OPERATION', defaultPrice: null, supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
const state = (wrapper.vm.$ as unknown as { setupState: { openCreate(): void; form: { direction: string; supplierId: string | null } } }).setupState
state.openCreate(); await flushPromises()
@@ -190,7 +274,7 @@ describe('ContainerFeesPanel contract', () => {
})
it('isolates a supplier load failure and clears supplier fields for receivable', async () => {
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'TRUCKING', supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'TRUCKING', defaultPrice: null, supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
vi.mocked(listContainerFeeSupplierOptions).mockRejectedValue(new Error('suppliers unavailable'))
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view', 'platform:supplier:list'])
const state = (wrapper.vm.$ as unknown as { setupState: { openCreate(): void; form: { direction: string; feeItemId: string; supplierId: string | null }; feeItems: unknown[] } }).setupState
@@ -203,7 +287,7 @@ describe('ContainerFeesPanel contract', () => {
})
it('submits only the selected supplier ID for payable fees', async () => {
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'TRUCKING', supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'TRUCKING', defaultPrice: null, supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
vi.mocked(listContainerFeeSupplierOptions).mockResolvedValue([{ id: '41', supplierName: '承运商A', category: 'TRUCKING' }])
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view', 'platform:supplier:list'])
const state = (wrapper.vm.$ as unknown as { setupState: { openCreate(): void; save(): Promise<void>; form: { direction: string; feeItemId: string; supplierId: string | null; settlementMode: string | null; amount: number } } }).setupState
@@ -215,7 +299,7 @@ describe('ContainerFeesPanel contract', () => {
})
it('allows any loaded ACTIVE supplier when the payable fee item has no supplier category', async () => {
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '未映射费用', feeCode: 'UNMAPPED', supplierCategory: null, warehouseReceivable: false, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '未映射费用', feeCode: 'UNMAPPED', defaultPrice: null, supplierCategory: null, warehouseReceivable: false, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
vi.mocked(listContainerFeeSupplierOptions).mockResolvedValue([{ id: '41', supplierName: '报关行', category: 'CUSTOMS' }])
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view', 'platform:supplier:list'])
const state = (wrapper.vm.$ as unknown as { setupState: { openCreate(): void; save(): Promise<void>; form: { direction: string; feeItemId: string; supplierId: string | null; settlementMode: string | null; amount: number } } }).setupState
@@ -227,7 +311,7 @@ describe('ContainerFeesPanel contract', () => {
})
it('fails closed when a supplier candidate does not match the fee-item category', async () => {
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '拖车费', feeCode: 'TRUCKING', supplierCategory: 'TRUCKING', warehouseReceivable: false, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '拖车费', feeCode: 'TRUCKING', defaultPrice: null, supplierCategory: 'TRUCKING', warehouseReceivable: false, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
vi.mocked(listContainerFeeSupplierOptions).mockResolvedValue([{ id: '41', supplierName: '报关行', category: 'CUSTOMS' }])
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view', 'platform:supplier:list'])
const state = (wrapper.vm.$ as unknown as { setupState: { openCreate(): void; save(): Promise<void>; form: { direction: string; feeItemId: string; supplierId: string | null; amount: number } } }).setupState
@@ -249,7 +333,7 @@ describe('ContainerFeesPanel contract', () => {
it('clears paid inputs for RECEIVABLE and ON_ACCOUNT and shows backend settlement semantics', async () => {
vi.mocked(listContainerFees).mockResolvedValue([{ id: '2', loadingPlanId: '9', containerBookingId: '99', containerNo: null, feeItemId: '7', feeItemName: '已付拖车费', direction: 'PAYABLE', supplierId: '41', supplierName: '承运商A', supplierCategory: 'TRUCKING', settlementMode: 'DIRECT_PAID', paidAt: '2026-08-12T10:00:00', paymentMethod: 'BANK_TRANSFER', paymentReferenceNo: 'BANK-1', paymentVoucherFileIds: ['9001'], businessDate: '2026-08-12', amount: 100, currency: 'CNY', reviewStatus: 'PENDING', sourceStatus: 'ACTIVE', sourceVersion: 1, remark: null, reviewerName: null, reviewedAt: null, reviewReason: null }])
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'TRUCKING', supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'TRUCKING', defaultPrice: null, supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view', 'platform:supplier:list'])
expect(wrapper.text()).toContain('已付成本'); expect(wrapper.text()).toContain('银行转账'); expect(wrapper.text()).toContain('BANK-1')
const state = (wrapper.vm.$ as unknown as { setupState: { openCreate(): void; form: { direction: string; settlementMode: string | null; paidAt: string | null; paymentMethod: string | null; paymentReferenceNo: string | null; paymentVoucherFileIds: string[] | null } } }).setupState
@@ -259,7 +343,7 @@ describe('ContainerFeesPanel contract', () => {
})
it('fails closed without DIRECT_PAID evidence and submits a scoped uploaded voucher', async () => {
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'TRUCKING', supplierCategory: 'TRUCKING', warehouseReceivable: false, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'TRUCKING', defaultPrice: null, supplierCategory: 'TRUCKING', warehouseReceivable: false, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
vi.mocked(listContainerFeeSupplierOptions).mockResolvedValue([{ id: '41', supplierName: '承运商A', category: 'TRUCKING' }])
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view', 'platform:supplier:list'])
const state = (wrapper.vm.$ as unknown as { setupState: { openCreate(): void; save(): Promise<void>; selectVoucher(event: Event): Promise<void>; form: { direction: string; feeItemId: string; supplierId: string | null; settlementMode: string | null; paidAt: string | null; paymentMethod: string | null; paymentReferenceNo: string | null; amount: number } } }).setupState