Files
admin-web/src/modules/inbounds/review/MeasurementReviewPanel.test.ts
T
2026-10-20 16:45:09 +08:00

312 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import ElementPlus, { ElMessage, ElMessageBox } from 'element-plus'
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 { approveMeasurement, getMeasurementReviewDetail, listMeasurementReviewHistory } from './api'
import MeasurementReviewPanel from './MeasurementReviewPanel.vue'
import type { MeasurementReviewDetail } from './types'
vi.mock('./api', () => ({
approveMeasurement: vi.fn(),
getMeasurementReviewDetail: vi.fn(),
listMeasurementReviewHistory: vi.fn(),
}))
vi.mock('../weight/components/WeightOcrPanel.vue', () => ({
default: {
name: 'WeightOcrPanel',
props: { customerId: String, inboundId: String, reviewMode: Boolean, correctionMode: Boolean },
emits: ['changed'],
template: '<button class="mock-weight-change" @click="$emit(\'changed\')">重量证据</button>',
},
}))
vi.mock('../measurement/components/MeasurementEvidenceThumbnail.vue', () => ({
default: {
name: 'MeasurementEvidenceThumbnail',
props: { customerId: String, inboundId: String, fileId: String, alt: String, emptyText: String },
emits: ['open'],
template: '<button class="mock-measurement-image" @click="$emit(\'open\')">测量图片</button>',
},
}))
const detail: MeasurementReviewDetail = {
inboundId: '90001', inboundCode: 'RK90001', companyName: '柏悦客户', channelCode: 'SEA', channelName: '海运',
processStatus: 'MEASURE_PENDING', measureReviewStatus: 'PENDING', recommendedPalletType: 'STANDARD', reviewReason: null, reviewerId: null, reviewerName: null,
measurementId: '70001', sourceType: 'DEVICE', measurementStatus: 'SUBMITTED', inboundVersion: 7, version: 4,
originalDocumentNo: 'ORD-001', originalGrossWeightKg: 100, originalVolumeCbm: 2, originalPackageCount: 10,
actualPackageCount: 10, actualGrossWeightKg: 120, actualNetWeightKg: 110, actualVolumeCbm: 2.2, volumeWeightKg: 367.4, actualGoodsName: '测试货物',
items: [],
weightResults: [],
}
enableAutoUnmount(afterEach)
describe('MeasurementReviewPanel', () => {
beforeEach(() => {
vi.clearAllMocks()
setActivePinia(createPinia())
const auth = useAuthStore()
auth.token = 'token'
auth.permissions = [
'firstleg:inbound:measurement-review',
'firstleg:inbound:measurement-review-history',
'platform:firstleg:inbound:measure:view',
'platform:firstleg:inbound:weight:edit',
'platform:firstleg:inbound:weight:confirm',
]
useAccessPreviewStore().initializeAfterLogin()
vi.mocked(getMeasurementReviewDetail).mockResolvedValue(detail)
vi.mocked(listMeasurementReviewHistory).mockResolvedValue({ records: [], page: 1, size: 20, total: 0, pages: 0 })
})
it('keeps customer context and pending actions in one sticky toolbar while reviewing long content', async () => {
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
const toolbar = wrapper.get('.review-toolbar')
expect(toolbar.text()).toContain('待审核')
expect(toolbar.text()).toContain('修改重量')
expect(toolbar.findAll('button').map(button => button.text())).toContain('通过')
expect(toolbar.text()).toContain('刷新')
expect(toolbar.text()).toContain('客户:柏悦客户')
expect(toolbar.text()).toContain('渠道:海运')
expect(wrapper.find('.review-toolbar__context').exists()).toBe(true)
expect(wrapper.get('.review-toolbar__status').find('.review-toolbar__context').exists()).toBe(true)
expect(wrapper.find('.review-context').exists()).toBe(false)
expect(wrapper.text()).not.toContain('RK90001')
expect(wrapper.text()).not.toContain('入仓码')
expect(wrapper.text()).not.toContain('驳回')
expect(wrapper.text()).not.toContain('异常放行')
expect(wrapper.text()).not.toContain('审核历史')
expect(listMeasurementReviewHistory).not.toHaveBeenCalled()
const source = await import('./MeasurementReviewPanel.vue?raw').then((module) => module.default)
expect(source).toContain('position: sticky')
expect(source).toContain('review-toolbar__context')
expect(source.indexOf('class="review-toolbar"')).toBeLessThan(source.indexOf('<WeightOcrPanel'))
expect(source).not.toContain('reasonDialog')
expect(source).not.toContain('saveInboundMeasurementDraft')
expect(source).not.toContain('submitInboundMeasurement')
expect(source).not.toContain('listMeasurementReviewHistory')
})
it.each([true, false])('describes dropship palletizing and uses the server result when pricing succeeds=%s', async (initialPricingSucceeded) => {
vi.mocked(approveMeasurement).mockResolvedValue({ inboundId: '90001', measurementId: '70001', processStatus: 'WAIT_PALLETIZE', feeStatus: initialPricingSucceeded ? 'CALCULATED' : 'UNPRICED', initialPricingSucceeded, initialPricingMessage: null, initialFee: null })
const confirmation = vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
const success = vi.spyOn(ElMessage, 'success')
const warning = vi.spyOn(ElMessage, 'warning')
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001', fulfillmentType: 'ONE_CLICK_DROPSHIP' }, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).not.toContain('通过并进入')
await wrapper.findAll('button').find((button) => button.text() === '通过')!.trigger('click')
await flushPromises()
expect(confirmation).toHaveBeenCalledWith(expect.stringContaining('进入待一件代发组托'), '确认测量审核通过', expect.objectContaining({ confirmButtonText: '通过' }))
expect(initialPricingSucceeded ? success : warning).toHaveBeenCalledWith(expect.stringContaining('入库单已进入待一件代发组托'))
expect(approveMeasurement).toHaveBeenCalledTimes(1)
expect(wrapper.emitted('changed')).toHaveLength(1)
})
it('uses the list customer context when the review detail has no customer name', async () => {
vi.mocked(getMeasurementReviewDetail).mockResolvedValue({ ...detail, companyName: null })
const wrapper = mount(MeasurementReviewPanel, {
props: { customerId: '10001', inboundId: '90001', customerName: '欧阳的公司', channelName: '纽约海铁' },
global: { plugins: [ElementPlus] },
})
await flushPromises()
expect(wrapper.get('.review-toolbar__context').text()).toBe('客户:欧阳的公司 · 渠道:海运')
})
it('shows declared facts, authoritative measurement totals, and box-detail evidence for manual review', async () => {
vi.mocked(getMeasurementReviewDetail).mockResolvedValue({
...detail,
originalPackageCount: 9,
originalGrossWeightKg: 118,
originalVolumeCbm: 2,
actualPackageCount: 10,
actualGrossWeightKg: 120,
actualNetWeightKg: 110,
actualVolumeCbm: 2.2,
volumeWeightKg: 367.4,
items: [{
id: 'item-1', boxCode: 'BOX-001', itemNo: '1', boxSpecification: null, packageCount: 10,
lengthCm: 60, widthCm: 40, heightCm: 30, girthCm: 200, unitWeightKg: 12,
unitVolumeCbm: 0.072, totalWeightKg: 120, totalVolumeCbm: 0.72, volumeWeightKg: 120.2,
intelligentImageFileId: '88001', machineNo: 'DEVICE-01', deviceMeasuredAt: '2026-08-18T10:30:00',
overLimitWarning: null, outerLengthCm: null, outerWidthCm: null, outerHeightCm: null, remark: null,
}],
})
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
const summary = wrapper.get('.review-data').text()
expect(summary).toContain('审核数据总览')
const comparisonRows = wrapper.findAll('.review-comparison tbody tr')
expect(comparisonRows.map(row => row.findAll('th, td').map(cell => cell.text()))).toEqual([
['箱数', '9', '10', '差异 +1'],
['毛重(kg)', '118', '120', '差异 +2 kg'],
['体积(m³)', '2', '2.2', '差异 +0.2 m³'],
])
expect(wrapper.get('.review-comparison').text()).not.toContain('实际品名')
expect(wrapper.get('.review-comparison').text()).not.toContain('原单据号码')
const extraFacts = wrapper.get('.review-extra-facts').text().replace(/\s/g, '')
expect(extraFacts).toContain('货物净重110kg')
expect(extraFacts).toContain('体积重367.4kg')
const items = wrapper.get('.review-items').text()
expect(items).toContain('箱规测量明细')
expect(items).toContain('BOX-001')
expect(items).toContain('60 × 40 × 30')
expect(items).toContain('120.2')
expect(items).toContain('2026-08-18 10:30:00')
expect(items).not.toContain('每箱毛重(kg)')
expect(items).not.toContain('总重量(kg)')
const image = wrapper.getComponent({ name: 'MeasurementEvidenceThumbnail' })
expect(image.props('customerId')).toBe('10001')
expect(image.props('inboundId')).toBe('90001')
expect(image.props('fileId')).toBe('88001')
const source = await import('./MeasurementReviewPanel.vue?raw').then((module) => module.default)
expect(source).toContain(':file-id="row.intelligentImageFileId"')
expect(source).toContain('openImagePreview(row.intelligentImageFileId')
expect(source).toContain('downloadInboundMeasurementImage')
expect(source).not.toContain('.review-table :deep(.el-table__header .cell) { white-space: nowrap; }')
expect(source).toContain('.review-table :deep(.review-time-cell .cell) { white-space: nowrap; }')
})
it('keeps difference labels absent when declared values are unavailable', async () => {
vi.mocked(getMeasurementReviewDetail).mockResolvedValue({ ...detail, originalGrossWeightKg: null, originalVolumeCbm: null })
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
const rows = wrapper.findAll('.review-comparison tbody tr')
expect(rows[0].findAll('td').at(-1)?.text()).toBe('一致')
expect(rows[1].findAll('td').map(cell => cell.text())).toEqual(['—', '120', '—'])
expect(rows[2].findAll('td').map(cell => cell.text())).toEqual(['—', '2.2', '—'])
})
it('reuses WeightOcrPanel and enables only its review correction mode', async () => {
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
const weightPanel = wrapper.getComponent({ name: 'WeightOcrPanel' })
expect(weightPanel.props('reviewMode')).toBe(true)
expect(weightPanel.props('correctionMode')).toBe(false)
await wrapper.findAll('button').find((button) => button.text().includes('修改重量'))?.trigger('click')
expect(weightPanel.props('correctionMode')).toBe(true)
})
it('refreshes inboundVersion after a weight correction before approval', async () => {
vi.mocked(getMeasurementReviewDetail)
.mockResolvedValueOnce(detail)
.mockResolvedValueOnce({ ...detail, inboundVersion: 8, actualGrossWeightKg: 125, actualNetWeightKg: 115 })
vi.mocked(approveMeasurement).mockResolvedValue({ inboundId: '90001', measurementId: '70001', processStatus: 'WAIT_SPLIT', feeStatus: 'CALCULATED', initialPricingSucceeded: true, initialPricingMessage: null, initialFee: null })
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
await wrapper.get('.mock-weight-change').trigger('click')
await flushPromises()
await wrapper.findAll('button').find((button) => button.text() === '通过')?.trigger('click')
await flushPromises()
expect(getMeasurementReviewDetail).toHaveBeenCalledTimes(2)
expect(approveMeasurement).toHaveBeenCalledWith('10001', '90001', { measurementId: '70001', expectedVersion: 8 })
expect(wrapper.emitted('changed')).toHaveLength(1)
})
it('exposes the same guarded actions when a dialog owns the fixed footer', async () => {
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
vi.mocked(approveMeasurement).mockResolvedValue({ inboundId: '90001', measurementId: '70001', processStatus: 'WAIT_SPLIT', feeStatus: 'CALCULATED', initialPricingSucceeded: true, initialPricingMessage: null, initialFee: null })
const wrapper = mount(MeasurementReviewPanel, {
props: { customerId: '10001', inboundId: '90001', showActions: false },
global: { plugins: [ElementPlus] },
})
await flushPromises()
expect(wrapper.find('.review-toolbar__actions').exists()).toBe(false)
expect(wrapper.get('.review-toolbar__context').text()).toContain('客户:柏悦客户')
expect(wrapper.vm.approvalButtonLabel).toBe('通过')
wrapper.vm.enableWeightCorrection()
await flushPromises()
expect(wrapper.getComponent({ name: 'WeightOcrPanel' }).props('correctionMode')).toBe(true)
await wrapper.vm.confirmApprove()
expect(approveMeasurement).toHaveBeenCalledWith('10001', '90001', { measurementId: '70001', expectedVersion: 7 })
expect(wrapper.emitted('changed')).toHaveLength(1)
})
it('emits only after approval succeeds and blocks duplicate submissions', async () => {
let finishApproval: (() => void) | undefined
vi.mocked(approveMeasurement).mockImplementation(() => new Promise((resolve) => {
finishApproval = () => resolve({ inboundId: '90001', measurementId: '70001', processStatus: 'WAIT_SPLIT', feeStatus: 'CALCULATED', initialPricingSucceeded: true, initialPricingMessage: null, initialFee: null })
}))
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
const approveButton = wrapper.findAll('button').find((button) => button.text() === '通过')!
await approveButton.trigger('click')
await approveButton.trigger('click')
expect(approveMeasurement).toHaveBeenCalledTimes(1)
expect(wrapper.emitted('changed')).toBeUndefined()
finishApproval?.()
await flushPromises()
expect(wrapper.emitted('changed')).toHaveLength(1)
expect(getMeasurementReviewDetail).toHaveBeenCalledTimes(1)
})
it('keeps the review panel open and does not emit when approval fails', async () => {
vi.mocked(approveMeasurement).mockRejectedValue(new Error('审核写入失败'))
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
await wrapper.findAll('button').find((button) => button.text() === '通过')!.trigger('click')
await flushPromises()
expect(wrapper.emitted('changed')).toBeUndefined()
expect(wrapper.find('.review-panel').exists()).toBe(true)
expect(wrapper.findAll('button').some((button) => button.text() === '通过')).toBe(true)
})
it('blocks approval while the order detail navigation is loading', async () => {
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
const wrapper = mount(MeasurementReviewPanel, {
props: { customerId: '10001', inboundId: '90001', postApproveLoading: true },
global: { plugins: [ElementPlus] },
})
await flushPromises()
await wrapper.findAll('button').find((button) => button.text() === '通过')!.trigger('click')
expect(approveMeasurement).not.toHaveBeenCalled()
})
it('does not submit approval again after approval succeeded but navigation needs retrying', async () => {
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
const wrapper = mount(MeasurementReviewPanel, {
props: { customerId: '10001', inboundId: '90001', approvalCompleted: true },
global: { plugins: [ElementPlus] },
})
await flushPromises()
const approveButton = wrapper.findAll('button').find((button) => button.text().includes('审核已通过'))!
expect(approveButton.attributes('disabled')).toBeDefined()
await approveButton.trigger('click')
expect(approveMeasurement).not.toHaveBeenCalled()
})
it('shows historical rejected data read-only without new reject or release operations', async () => {
vi.mocked(getMeasurementReviewDetail).mockResolvedValue({ ...detail, processStatus: 'MEASURE_REJECTED', measureReviewStatus: 'REJECTED', reviewReason: '历史审核说明' })
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('历史记录为审核驳回,仅供查看')
expect(wrapper.text()).toContain('历史审核说明')
expect(wrapper.text()).not.toContain('修改重量')
expect(wrapper.text()).not.toContain('异常放行')
})
})