387 lines
27 KiB
TypeScript
387 lines
27 KiB
TypeScript
import ElementPlus from 'element-plus'
|
|
import { readFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
import { useAuthStore } from '@/core/auth/auth'
|
|
import { ApiResponseError } from '@/shared/api/http'
|
|
import {
|
|
adjustInboundFeeDetail, createInboundFeeDetail, deleteInboundFeeDetail,
|
|
listInboundFeeDetails, listInboundFeeItemOptions, listInboundFeeSupplierOptions,
|
|
reviewInboundFeeDetail, updateInboundFeeDetail, uploadInboundFeePaymentVoucher, voidInboundFeeDetail,
|
|
} from '@/modules/unified-orders/api/inboundFeeDetails'
|
|
import InboundFeeDetailManager from './InboundFeeDetailManager.vue'
|
|
import type { InboundFeeDetail } from '@/modules/unified-orders/api/inboundFeeDetails'
|
|
|
|
vi.mock('@/modules/unified-orders/api/inboundFeeDetails', () => ({
|
|
listInboundFeeDetails: vi.fn(), listInboundFeeItemOptions: vi.fn(), listInboundFeeSupplierOptions: vi.fn(),
|
|
createInboundFeeDetail: vi.fn(), updateInboundFeeDetail: vi.fn(), deleteInboundFeeDetail: vi.fn(),
|
|
reviewInboundFeeDetail: vi.fn(), adjustInboundFeeDetail: vi.fn(), uploadInboundFeePaymentVoucher: vi.fn(), voidInboundFeeDetail: vi.fn(),
|
|
}))
|
|
|
|
const pending: InboundFeeDetail = {
|
|
id: '11', inboundId: '88', feeItemId: '31', sourceType: 'MANUAL', sourceFeeVersion: 1,
|
|
inboundCode: 'RK-88', direction: 'RECEIVABLE', supplierId: null, supplierName: null, supplierCategory: null,
|
|
settlementMode: null, paidAt: null, paymentMethod: null, paymentReferenceNo: null, paymentVoucherFileIds: null,
|
|
businessDate: '2026-08-12', customerName: '测试客户', feeTypeNameSnapshot: '操作费',
|
|
unitPrice: 10, currency: 'CNY', amount: 100, remark: 'marker', reviewStatus: 'PENDING',
|
|
reviewReason: null, reviewerName: null, reviewedAt: null, publicationStatus: 'DRAFT',
|
|
}
|
|
const approved: InboundFeeDetail = { ...pending, id: '12', sourceFeeVersion: 3, reviewStatus: 'APPROVED', publicationStatus: 'PUBLISHED' }
|
|
const source = readFileSync(join(process.cwd(), 'src/modules/unified-orders/components/InboundFeeDetailManager.vue'), 'utf8')
|
|
|
|
enableAutoUnmount(afterEach)
|
|
|
|
function mountManager(permissions: string[]) {
|
|
const pinia = createPinia(); setActivePinia(pinia)
|
|
const auth = useAuthStore(); auth.token = 'token'; auth.permissions = permissions
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
return mount(InboundFeeDetailManager, { attachTo: document.body, props: { accountId: '100', inboundId: '88' }, global: { plugins: [pinia, ElementPlus] } })
|
|
}
|
|
|
|
describe('InboundFeeDetailManager', () => {
|
|
it.each([80, null])('refreshes the default price on reopening the editor: %s', async (defaultPrice) => {
|
|
const item = { id: '31', feeTypeName: '操作费', feeCode: 'OPERATION', defaultPrice: 60, supplierCategory: null, warehouseReceivable: true, warehousePayable: false }
|
|
vi.mocked(listInboundFeeItemOptions).mockResolvedValue([item])
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
|
|
await flushPromises()
|
|
const state = (wrapper.vm.$ as unknown as { setupState: {
|
|
openEditor(mode: string, row?: InboundFeeDetail): Promise<void>
|
|
editorOpen: boolean
|
|
form: { feeItemId: string; unitPrice: number; amount: number }
|
|
} }).setupState
|
|
await state.openEditor('create'); await flushPromises()
|
|
state.editorOpen = false; await flushPromises()
|
|
vi.mocked(listInboundFeeItemOptions).mockResolvedValue([{ ...item, defaultPrice }])
|
|
await state.openEditor('create'); await flushPromises()
|
|
const select = wrapper.findAllComponents({ name: 'ElFormItem' }).find((field) => field.props('prop') === 'feeItemId')!.findComponent({ name: 'ElSelect' })
|
|
select.vm.$emit('update:modelValue', '31'); select.vm.$emit('change', '31')
|
|
await flushPromises()
|
|
expect(state.form).toMatchObject({ unitPrice: defaultPrice ?? 0, amount: defaultPrice ?? 0 })
|
|
expect(listInboundFeeItemOptions).toHaveBeenCalledTimes(2)
|
|
state.editorOpen = false; await flushPromises()
|
|
await state.openEditor('edit', pending); await flushPromises()
|
|
expect(state.form).toMatchObject({ unitPrice: 10, amount: 100 })
|
|
})
|
|
|
|
it('clears cached choices and blocks saving while refresh is pending or has failed', async () => {
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
|
|
await flushPromises()
|
|
const state = (wrapper.vm.$ as unknown as { setupState: {
|
|
openEditor(mode: string, row?: InboundFeeDetail): Promise<void>
|
|
save(): Promise<void>
|
|
editorOpen: boolean
|
|
feeItems: unknown[]
|
|
feeItemsLoading: boolean
|
|
feeItemsError: { message: string } | null
|
|
actionError: { message: string } | null
|
|
form: { unitPrice: number; amount: number }
|
|
} }).setupState
|
|
await state.openEditor('edit', pending); await flushPromises()
|
|
expect(state.feeItems).toHaveLength(1)
|
|
state.editorOpen = false; await flushPromises()
|
|
let rejectRefresh!: (reason: Error) => void
|
|
vi.mocked(listInboundFeeItemOptions).mockImplementationOnce(() => new Promise((_, reject) => { rejectRefresh = reject }))
|
|
const opening = state.openEditor('edit', pending)
|
|
await flushPromises()
|
|
expect(state.feeItems).toEqual([])
|
|
expect(state.feeItemsLoading).toBe(true)
|
|
const select = wrapper.findAllComponents({ name: 'ElFormItem' }).find((field) => field.props('prop') === 'feeItemId')!.findComponent({ name: 'ElSelect' })
|
|
expect(select.props('disabled')).toBe(true)
|
|
await state.save()
|
|
expect(state.actionError?.message).toContain('正在加载')
|
|
rejectRefresh(new Error('加载失败'))
|
|
await opening; await flushPromises()
|
|
expect(state.feeItems).toEqual([])
|
|
expect(state.feeItemsError).not.toBeNull()
|
|
expect(state.form).toMatchObject({ unitPrice: 10, amount: 100 })
|
|
await state.save()
|
|
expect(updateInboundFeeDetail).not.toHaveBeenCalled()
|
|
state.feeItemsError = null
|
|
await state.save()
|
|
expect(updateInboundFeeDetail).not.toHaveBeenCalled()
|
|
expect(state.actionError?.message).toContain('不可用')
|
|
})
|
|
|
|
it('prefills only on user selection and saves a manually overridden price', async () => {
|
|
vi.mocked(listInboundFeeItemOptions).mockResolvedValue([
|
|
{ id: '31', feeTypeName: '操作费', feeCode: 'OPERATION', defaultPrice: 60, supplierCategory: null, warehouseReceivable: true, warehousePayable: false },
|
|
{ id: '32', feeTypeName: '手工费', feeCode: 'MANUAL', defaultPrice: null, supplierCategory: null, warehouseReceivable: true, warehousePayable: false },
|
|
{ id: '33', feeTypeName: '零元费', feeCode: 'ZERO', defaultPrice: 0, supplierCategory: null, warehouseReceivable: true, warehousePayable: false },
|
|
])
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
|
|
await flushPromises()
|
|
const state = (wrapper.vm.$ as unknown as { setupState: {
|
|
openEditor(mode: string, row?: InboundFeeDetail): Promise<void>
|
|
save(): Promise<void>
|
|
form: { feeItemId: string; unitPrice: number; amount: number }
|
|
} }).setupState
|
|
await state.openEditor('edit', pending)
|
|
await flushPromises()
|
|
expect(state.form).toMatchObject({ unitPrice: 10, amount: 100 })
|
|
await state.openEditor('create')
|
|
await flushPromises()
|
|
const select = wrapper.findAllComponents({ name: 'ElFormItem' }).find((item) => item.props('prop') === 'feeItemId')!.findComponent({ name: 'ElSelect' })
|
|
for (const [id, price] of [['31', 60], ['32', 0], ['33', 0]] as const) {
|
|
select.vm.$emit('update:modelValue', id)
|
|
select.vm.$emit('change', id)
|
|
await flushPromises()
|
|
expect(state.form).toMatchObject({ unitPrice: price, amount: price })
|
|
}
|
|
select.vm.$emit('update:modelValue', '31')
|
|
select.vm.$emit('change', '31')
|
|
await flushPromises()
|
|
state.form.unitPrice = 65; state.form.amount = 75
|
|
await state.save(); await flushPromises()
|
|
expect(createInboundFeeDetail).toHaveBeenCalledWith('100', '88', expect.objectContaining({ feeItemId: '31', unitPrice: 65, amount: 75 }))
|
|
})
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks(); document.body.innerHTML = ''
|
|
vi.mocked(listInboundFeeDetails).mockResolvedValue([pending, approved])
|
|
vi.mocked(listInboundFeeItemOptions).mockResolvedValue([{ id: '31', feeTypeName: '操作费', feeCode: 'OPERATION', defaultPrice: null, supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true }])
|
|
vi.mocked(listInboundFeeSupplierOptions).mockResolvedValue([{ id: '41', supplierName: '测试供应商', category: 'TRUCKING' }])
|
|
vi.mocked(createInboundFeeDetail).mockResolvedValue(pending)
|
|
vi.mocked(updateInboundFeeDetail).mockResolvedValue(pending)
|
|
vi.mocked(adjustInboundFeeDetail).mockResolvedValue({ ...approved, reviewStatus: 'PENDING' })
|
|
vi.mocked(uploadInboundFeePaymentVoucher).mockResolvedValue({ fileId: '9001', fileName: '付款凭证.pdf' })
|
|
vi.mocked(reviewInboundFeeDetail).mockResolvedValue(null)
|
|
vi.mocked(deleteInboundFeeDetail).mockResolvedValue(null)
|
|
vi.mocked(voidInboundFeeDetail).mockResolvedValue(null)
|
|
})
|
|
|
|
it('renders actions from backend status and exact permissions', async () => {
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:inbound:fee-detail:delete', 'firstleg:inbound:fee-detail:review'])
|
|
await flushPromises()
|
|
expect(listInboundFeeDetails).toHaveBeenCalledWith('100', '88')
|
|
const first = wrapper.findAll('.el-table__row')[0]!.text()
|
|
const second = wrapper.findAll('.el-table__row')[1]!.text()
|
|
const secondButtons = wrapper.findAll('.el-table__row')[1]!.findAll('button').map((button) => button.text())
|
|
expect(first).toContain('编辑'); expect(first).toContain('审核'); expect(first).toContain('驳回')
|
|
expect(first).toContain('草稿')
|
|
expect(second).toContain('已发布')
|
|
expect(second).toContain('调整'); expect(second).toContain('作废'); expect(secondButtons).not.toContain('审核')
|
|
expect(wrapper.find('th').text()).toContain('操作')
|
|
expect(source).not.toMatch(/<el-button[^>]+link[^>]+(?:编辑|审核|驳回|调整|作废)/)
|
|
expect(wrapper.findAll('.el-table__row')[0]!.findAll('.density-actions button').map(button => button.attributes('data-tone'))).toEqual(['neutral', 'warning', 'warning', 'neutral'])
|
|
expect(wrapper.findAll('.el-table__row')[1]!.findAll('.density-actions button').map(button => button.attributes('data-tone'))).toEqual(['neutral', 'danger'])
|
|
})
|
|
|
|
it.each([
|
|
[[], [], false],
|
|
[['delete'], ['删除'], false],
|
|
[['edit', 'delete'], ['编辑', '删除'], false],
|
|
[['review', 'delete'], ['审核', '驳回', '删除'], false],
|
|
[['edit', 'review'], ['编辑', '审核', '驳回'], false],
|
|
[['edit', 'review', 'delete'], ['编辑', '审核', '驳回', '更多'], true],
|
|
])('folds pending fee actions only when more than three are available: %j', async (actions, labels, overflow) => {
|
|
vi.mocked(listInboundFeeDetails).mockResolvedValue([pending])
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', ...actions.map(action => 'firstleg:inbound:fee-detail:' + action)])
|
|
await flushPromises()
|
|
const row = wrapper.get('.el-table__row .density-actions')
|
|
expect(row.findAll('button').map(button => button.text().trim())).toEqual(labels)
|
|
expect(row.find('.el-dropdown').exists()).toBe(overflow)
|
|
if (overflow) expect(row.get('.el-dropdown button').attributes('aria-label')).toContain('操作费')
|
|
})
|
|
|
|
it.each(['编辑', '驳回'])('styles the native %s fee dialog at its own root', async action => {
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:inbound:fee-detail:review', 'firstleg:fee-item:view'])
|
|
await flushPromises()
|
|
await wrapper.get('.el-table__row .density-actions').findAll('button').find(button => button.text().trim() === action)!.trigger('click')
|
|
await flushPromises()
|
|
const dialog = document.querySelector<HTMLElement>('.el-dialog.admin-density-dialog')!
|
|
expect(dialog).not.toBeNull()
|
|
expect(dialog.querySelector('.density-form')).not.toBeNull()
|
|
expect(dialog.style.getPropertyValue('--el-dialog-width')).toBe(action === '编辑' ? 'min(1000px, calc(100vw - 28px))' : 'min(880px, calc(100vw - 28px))')
|
|
if (action === '驳回') {
|
|
const confirm = [...dialog.querySelectorAll<HTMLButtonElement>('button')].find(button => button.textContent?.trim() === '确认驳回')!
|
|
confirm.click()
|
|
await flushPromises()
|
|
expect(reviewInboundFeeDetail).not.toHaveBeenCalled()
|
|
expect(dialog.querySelector('textarea')?.value).toBe('')
|
|
} else expect(dialog.textContent).toContain('业务发生日')
|
|
})
|
|
|
|
it('keeps FBA_PRICING rows derived and read-only regardless of write permissions', async () => {
|
|
vi.mocked(listInboundFeeDetails).mockResolvedValue([{ ...approved, sourceType: 'FBA_PRICING' }])
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:inbound:fee-detail:delete', 'firstleg:inbound:fee-detail:review'])
|
|
await flushPromises()
|
|
const row = wrapper.find('.el-table__row')
|
|
expect(row.text()).toContain('头程派生')
|
|
const buttons = row.findAll('button').map((button) => button.text())
|
|
for (const action of ['编辑', '审核', '驳回', '调整', '作废', '删除']) expect(buttons).not.toContain(action)
|
|
})
|
|
|
|
it('shows payable settlement meaning and a DIRECT_PAID summary without changing fee direction', async () => {
|
|
vi.mocked(listInboundFeeDetails).mockResolvedValue([{ ...pending, direction: 'PAYABLE', supplierId: '41', supplierName: '测试供应商', settlementMode: 'DIRECT_PAID', paidAt: '2026-08-12T10:00:00', paymentMethod: 'BANK_TRANSFER', paymentReferenceNo: 'BANK-11', paymentVoucherFileIds: ['9001'] }])
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view'])
|
|
await flushPromises()
|
|
expect(wrapper.text()).toContain('应付'); expect(wrapper.text()).toContain('已付成本'); expect(wrapper.text()).toContain('银行转账'); expect(wrapper.text()).toContain('BANK-11')
|
|
})
|
|
|
|
it('does not request or expose fee rows without view permission', async () => {
|
|
mountManager([]); await flushPromises()
|
|
expect(listInboundFeeDetails).not.toHaveBeenCalled()
|
|
expect(document.body.textContent).toContain('没有查看费用明细的权限')
|
|
})
|
|
|
|
it.each(['', ' ', 'null', 'undefined'])('does not request fee rows for invalid inboundId %j', async (inboundId) => {
|
|
const pinia = createPinia(); setActivePinia(pinia)
|
|
const auth = useAuthStore(); auth.token = 'token'; auth.permissions = ['firstleg:inbound:fee-detail:view']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
mount(InboundFeeDetailManager, {
|
|
props: { accountId: '100', inboundId },
|
|
global: { plugins: [pinia, ElementPlus] },
|
|
})
|
|
await flushPromises()
|
|
expect(listInboundFeeDetails).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('approves a pending fee then refreshes rows and emits changed', async () => {
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:review'])
|
|
await flushPromises()
|
|
wrapper.findAll('button').find((button) => button.text().includes('审核'))?.trigger('click')
|
|
await flushPromises()
|
|
expect(reviewInboundFeeDetail).toHaveBeenCalledWith('100', '88', '11', 'approve')
|
|
expect(listInboundFeeDetails).toHaveBeenCalledTimes(2)
|
|
expect(wrapper.emitted('changed')).toHaveLength(1)
|
|
})
|
|
|
|
it('preserves request and trace IDs when a write fails', async () => {
|
|
vi.mocked(reviewInboundFeeDetail).mockRejectedValue(new ApiResponseError('状态冲突', 'A0409', 409, null, 'request-fee', 'trace-fee'))
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:review'])
|
|
await flushPromises()
|
|
wrapper.findAll('button').find((button) => button.text().includes('审核'))?.trigger('click')
|
|
await flushPromises()
|
|
expect(document.body.textContent).toContain('request-fee')
|
|
expect(document.body.textContent).toContain('trace-fee')
|
|
})
|
|
|
|
it('keeps currency fixed to CNY in the form and write payload', async () => {
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
|
|
await flushPromises()
|
|
await wrapper.findAll('button').find((button) => button.text().includes('新增费用'))?.trigger('click')
|
|
await flushPromises()
|
|
const currency = document.querySelector<HTMLInputElement>('input[aria-label="货币 CNY"]')
|
|
expect(currency?.readOnly).toBe(true)
|
|
expect(currency?.value).toBe('CNY')
|
|
})
|
|
|
|
it('uses only the Element Plus table scroller without an outer horizontal scrollbar', async () => {
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view'])
|
|
await flushPromises()
|
|
expect(wrapper.find('.fee-table-shell').exists()).toBe(true)
|
|
expect(wrapper.find('.detail-table-wrap').exists()).toBe(false)
|
|
expect(source).toContain('<el-table :data="rows" border stripe')
|
|
})
|
|
|
|
it('spins only the refresh icon while fee rows are loading', () => {
|
|
expect(source).toContain('class="fee-refresh-button" circle aria-label="刷新费用明细"')
|
|
expect(source).toContain(':aria-busy="loading" :disabled="loading"')
|
|
expect(source).toContain(`<RefreshCw :size="16" :class="{ 'is-spinning': loading }" />`)
|
|
expect(source).not.toMatch(/aria-label="刷新费用明细"[^>]*:loading=/)
|
|
expect(source).toContain('@keyframes fee-refresh-spin')
|
|
})
|
|
|
|
it('loads RECEIVABLE fee items without requesting suppliers', async () => {
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
|
|
await flushPromises()
|
|
await wrapper.findAll('button').find((button) => button.text().includes('新增费用'))?.trigger('click')
|
|
await flushPromises()
|
|
expect(listInboundFeeItemOptions).toHaveBeenCalledTimes(1)
|
|
expect(listInboundFeeSupplierOptions).not.toHaveBeenCalled()
|
|
expect(document.body.textContent).toContain('缺少供应商查看权限')
|
|
expect(document.body.textContent).toContain('应收费用不受影响')
|
|
})
|
|
|
|
it('does not expose the editor without fee-item view permission', async () => {
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit'])
|
|
await flushPromises()
|
|
expect(wrapper.findAll('button').some((button) => button.text().includes('新增费用'))).toBe(false)
|
|
expect(listInboundFeeItemOptions).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('keeps fee items usable but blocks payable save when the supplier request fails', async () => {
|
|
vi.mocked(listInboundFeeDetails).mockResolvedValue([{ ...pending, direction: 'PAYABLE', supplierId: '41', supplierName: '测试供应商', supplierCategory: 'TRUCKING', settlementMode: 'ON_ACCOUNT' }])
|
|
vi.mocked(listInboundFeeSupplierOptions).mockRejectedValue(new ApiResponseError('供应商不可用', 'A0403', 403, null, 'request-supplier', 'trace-supplier'))
|
|
const wrapper = mountManager([
|
|
'firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit',
|
|
'firstleg:fee-item:view', 'platform:supplier:list',
|
|
])
|
|
await flushPromises()
|
|
await wrapper.findAll('button').find((button) => button.text().includes('编辑'))?.trigger('click')
|
|
await flushPromises()
|
|
expect(listInboundFeeItemOptions).toHaveBeenCalledTimes(1)
|
|
expect(listInboundFeeSupplierOptions).toHaveBeenCalledWith('TRUCKING')
|
|
expect(document.body.textContent).toContain('当前账号无权访问该数据')
|
|
expect(document.body.textContent).toContain('A0403')
|
|
expect(document.body.textContent).toContain('request-supplier')
|
|
expect(document.body.textContent).toContain('trace-supplier')
|
|
expect(document.querySelector('.fee-form .el-select')?.classList.contains('is-disabled')).toBe(false)
|
|
})
|
|
|
|
it('allows any loaded ACTIVE supplier when a payable fee item has no supplier category', async () => {
|
|
vi.mocked(listInboundFeeItemOptions).mockResolvedValue([{ id: '31', feeTypeName: '未映射费用', feeCode: 'UNMAPPED', defaultPrice: null, supplierCategory: null, warehouseReceivable: false, warehousePayable: true }])
|
|
vi.mocked(listInboundFeeSupplierOptions).mockResolvedValue([{ id: '41', supplierName: '测试供应商', category: 'CUSTOMS' }])
|
|
const wrapper = mountManager([
|
|
'firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit',
|
|
'firstleg:fee-item:view', 'platform:supplier:list',
|
|
])
|
|
await flushPromises()
|
|
await wrapper.findAll('button').find((button) => button.text().includes('新增费用'))?.trigger('click')
|
|
await flushPromises()
|
|
const state = (wrapper.vm.$ as unknown as { setupState: { save(): Promise<void>; form: { direction: string; feeItemId: string; supplierId: string | null; settlementMode: string | null; amount: number } } }).setupState
|
|
state.form.direction = 'PAYABLE'; state.form.feeItemId = '31'; await flushPromises(); state.form.supplierId = '41'; state.form.settlementMode = 'ON_ACCOUNT'; state.form.amount = 100
|
|
await state.save(); await flushPromises()
|
|
expect(listInboundFeeSupplierOptions).toHaveBeenCalledWith(null)
|
|
expect(createInboundFeeDetail).toHaveBeenCalledWith('100', '88', expect.objectContaining({ direction: 'PAYABLE', supplierId: '41', settlementMode: 'ON_ACCOUNT', paidAt: null, paymentMethod: null, paymentReferenceNo: null, paymentVoucherFileIds: null }))
|
|
})
|
|
|
|
it('clears settlement and paid fields when switching to RECEIVABLE or ON_ACCOUNT', async () => {
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view', 'platform:supplier:list'])
|
|
await flushPromises(); await wrapper.findAll('button').find((button) => button.text().includes('新增费用'))?.trigger('click'); await flushPromises()
|
|
const state = (wrapper.vm.$ as unknown as { setupState: { form: { direction: string; settlementMode: string | null; paidAt: string | null; paymentMethod: string | null; paymentReferenceNo: string | null; paymentVoucherFileIds: string[] | null } } }).setupState
|
|
Object.assign(state.form, { direction: 'PAYABLE', settlementMode: 'DIRECT_PAID', paidAt: '2026-08-12T10:00:00', paymentMethod: 'CASH', paymentReferenceNo: 'CASH-1', paymentVoucherFileIds: ['9001'] }); await flushPromises()
|
|
state.form.settlementMode = 'ON_ACCOUNT'; await flushPromises()
|
|
expect(state.form).toMatchObject({ settlementMode: 'ON_ACCOUNT', paidAt: null, paymentMethod: null, paymentReferenceNo: null, paymentVoucherFileIds: null })
|
|
state.form.direction = 'RECEIVABLE'; await flushPromises()
|
|
expect(state.form).toMatchObject({ direction: 'RECEIVABLE', settlementMode: null, paidAt: null, paymentMethod: null, paymentReferenceNo: null, paymentVoucherFileIds: null })
|
|
})
|
|
|
|
it('requires complete DIRECT_PAID evidence and submits uploaded voucher IDs only after scoped upload succeeds', async () => {
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view', 'platform:supplier:list'])
|
|
await flushPromises(); await wrapper.findAll('button').find((button) => button.text().includes('新增费用'))?.trigger('click'); await flushPromises()
|
|
const state = (wrapper.vm.$ as unknown as { setupState: { 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
|
|
Object.assign(state.form, { direction: 'PAYABLE', feeItemId: '31' }); await flushPromises()
|
|
Object.assign(state.form, { supplierId: '41', settlementMode: 'DIRECT_PAID', paidAt: '2026-08-12T10:00:00', paymentMethod: 'BANK_TRANSFER', paymentReferenceNo: null, amount: 100 }); await flushPromises()
|
|
await state.save(); expect(createInboundFeeDetail).not.toHaveBeenCalled()
|
|
const input = document.createElement('input'); Object.defineProperty(input, 'files', { value: [new File(['x'], '付款凭证.pdf')] })
|
|
await state.selectVoucher({ target: input } as unknown as Event); await state.save()
|
|
expect(uploadInboundFeePaymentVoucher).toHaveBeenCalledWith('100', expect.objectContaining({ name: '付款凭证.pdf' }))
|
|
expect(createInboundFeeDetail).toHaveBeenCalledWith('100', '88', expect.objectContaining({ settlementMode: 'DIRECT_PAID', paymentMethod: 'BANK_TRANSFER', paymentVoucherFileIds: ['9001'] }))
|
|
})
|
|
|
|
it('keeps DIRECT_PAID save closed and exposes feedback when voucher upload fails', async () => {
|
|
vi.mocked(uploadInboundFeePaymentVoucher).mockRejectedValue(new ApiResponseError('File服务不可用', 'A0503', 503, null, 'request-file', 'trace-file'))
|
|
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view', 'platform:supplier:list'])
|
|
await flushPromises(); await wrapper.findAll('button').find((button) => button.text().includes('新增费用'))?.trigger('click'); await flushPromises()
|
|
const state = (wrapper.vm.$ as unknown as { setupState: { 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
|
|
Object.assign(state.form, { direction: 'PAYABLE', feeItemId: '31' }); await flushPromises()
|
|
Object.assign(state.form, { supplierId: '41', settlementMode: 'DIRECT_PAID', paidAt: '2026-08-12T10:00:00', paymentMethod: 'ONLINE', paymentReferenceNo: 'ONLINE-1', amount: 100 }); await flushPromises()
|
|
const input = document.createElement('input'); Object.defineProperty(input, 'files', { value: [new File(['x'], '失败凭证.png')] })
|
|
await state.selectVoucher({ target: input } as unknown as Event); await state.save(); await flushPromises()
|
|
expect(createInboundFeeDetail).not.toHaveBeenCalled(); expect(document.body.textContent).toContain('request-file'); expect(document.body.textContent).toContain('trace-file')
|
|
})
|
|
|
|
it('refreshes fee rows and the order detail after every successful write', () => {
|
|
expect(source).toMatch(/async function finishAction\(message: string\) \{\r?\n await load\(\); emit\('changed'\)/)
|
|
expect(source).toMatch(/createInboundFeeDetail[\s\S]*updateInboundFeeDetail[\s\S]*adjustInboundFeeDetail[\s\S]*await finishAction\(/)
|
|
expect(source).toMatch(/deleteInboundFeeDetail[\s\S]*await finishAction\('费用明细已删除'\)/)
|
|
expect(source).toMatch(/reviewInboundFeeDetail[\s\S]*await finishAction\('费用审核通过'\)/)
|
|
expect(source).toMatch(/reviewInboundFeeDetail[\s\S]*rejectReason[\s\S]*await finishAction\('费用已驳回'\)/)
|
|
expect(source).toMatch(/voidInboundFeeDetail[\s\S]*await finishAction\('费用明细已作废'\)/)
|
|
})
|
|
})
|