158 lines
9.5 KiB
TypeScript
158 lines
9.5 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, listInboundFeePartnerOptions,
|
|
reviewInboundFeeDetail, updateInboundFeeDetail, 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(), listInboundFeePartnerOptions: vi.fn(),
|
|
createInboundFeeDetail: vi.fn(), updateInboundFeeDetail: vi.fn(), deleteInboundFeeDetail: vi.fn(),
|
|
reviewInboundFeeDetail: vi.fn(), adjustInboundFeeDetail: vi.fn(), voidInboundFeeDetail: vi.fn(),
|
|
}))
|
|
|
|
const pending: InboundFeeDetail = {
|
|
id: '11', inboundId: '88', feeItemId: '31', sourceType: 'MANUAL', sourceFeeVersion: 1,
|
|
inboundCode: 'RK-88', direction: 'RECEIVABLE', partnerId: null, partnerName: 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', settlementIncluded: true,
|
|
}
|
|
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', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks(); document.body.innerHTML = ''
|
|
vi.mocked(listInboundFeeDetails).mockResolvedValue([pending, approved])
|
|
vi.mocked(listInboundFeeItemOptions).mockResolvedValue([{ id: '31', feeTypeName: '操作费', warehouseReceivable: true, warehousePayable: true }])
|
|
vi.mocked(listInboundFeePartnerOptions).mockResolvedValue([{ id: '41', partnerName: '测试往来单位' }])
|
|
vi.mocked(createInboundFeeDetail).mockResolvedValue(pending)
|
|
vi.mocked(updateInboundFeeDetail).mockResolvedValue(pending)
|
|
vi.mocked(adjustInboundFeeDetail).mockResolvedValue({ ...approved, reviewStatus: 'PENDING' })
|
|
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(second).toContain('调整'); expect(second).toContain('作废'); expect(secondButtons).not.toContain('审核')
|
|
expect(wrapper.find('th').text()).toContain('操作')
|
|
})
|
|
|
|
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('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)
|
|
})
|
|
|
|
it('loads RECEIVABLE fee items without requesting counterparties', 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(listInboundFeePartnerOptions).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 when the independent counterparty request fails', async () => {
|
|
vi.mocked(listInboundFeeDetails).mockResolvedValue([{ ...pending, direction: 'PAYABLE', partnerId: '41', partnerName: '测试往来单位' }])
|
|
vi.mocked(listInboundFeePartnerOptions).mockRejectedValue(new ApiResponseError('往来单位不可用', 'A0403', 403, null, 'request-partner', 'trace-partner'))
|
|
const wrapper = mountManager([
|
|
'firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit',
|
|
'firstleg:fee-item:view', 'platform:counterparty:list',
|
|
])
|
|
await flushPromises()
|
|
await wrapper.findAll('button').find((button) => button.text().includes('编辑'))?.trigger('click')
|
|
await flushPromises()
|
|
expect(listInboundFeeItemOptions).toHaveBeenCalledTimes(1)
|
|
expect(listInboundFeePartnerOptions).toHaveBeenCalledTimes(1)
|
|
expect(document.body.textContent).toContain('当前账号无权访问该数据')
|
|
expect(document.body.textContent).toContain('A0403')
|
|
expect(document.body.textContent).toContain('request-partner')
|
|
expect(document.body.textContent).toContain('trace-partner')
|
|
expect(document.querySelector('.fee-form .el-select')?.classList.contains('is-disabled')).toBe(false)
|
|
})
|
|
|
|
it('refreshes fee rows and the order detail after every successful write', () => {
|
|
expect(source).toContain("async function finishAction(message: string) {\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\('费用明细已作废'\)/)
|
|
})
|
|
})
|