Files
admin-web/src/modules/inbounds/review/MeasurementReviewPanel.test.ts
T

429 lines
25 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, tableMode: Boolean },
emits: ['changed'],
methods: { reload: async () => {} },
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: [],
}
const measuredItem: MeasurementReviewDetail['items'][number] = {
id: 'item-raw', boxCode: 'BOX-RAW-001', itemNo: '1', boxSpecification: null, packageCount: 22,
lengthCm: 62, widthCm: 52, heightCm: 52, girthCm: 270, unitWeightKg: 25.9318,
unitVolumeCbm: 0.3275, totalWeightKg: 570.5, totalVolumeCbm: 7.205, volumeWeightKg: 1203.235,
intelligentImageFileId: null, machineNo: null, deviceMeasuredAt: null, overLimitWarning: null,
outerLengthCm: 75, outerWidthCm: 66, outerHeightCm: 66, remark: null,
}
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 context in a scrolling toolbar and moves weight actions and totals to the weight details', 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()).not.toContain('修改重量')
const weightHeading = wrapper.get('.review-weight-items header')
expect(weightHeading.text()).toContain('修改重量')
expect(weightHeading.text()).toContain('实测毛重 120 kg')
expect(weightHeading.text()).toContain('货物重量 110 kg')
expect(weightHeading.find('.lucide-scale').exists()).toBe(true)
expect(weightHeading.find('.lucide-pencil').exists()).toBe(true)
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).not.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([
{ fulfillmentType: 'ONE_CLICK_DROPSHIP', processStatus: 'WAIT_PALLETIZE', targetLabel: '待一件代发组托', initialPricingSucceeded: true },
{ fulfillmentType: 'ONE_CLICK_DROPSHIP', processStatus: 'WAIT_PALLETIZE', targetLabel: '待一件代发组托', initialPricingSucceeded: false },
{ fulfillmentType: 'FBA', processStatus: 'WAIT_SPLIT', targetLabel: '待分柜', initialPricingSucceeded: true },
{ fulfillmentType: 'FBA', processStatus: 'WAIT_SPLIT', targetLabel: '待分柜', initialPricingSucceeded: false },
])('describes $fulfillmentType approval target and uses the server result when pricing succeeds=$initialPricingSucceeded', async ({ fulfillmentType, processStatus, targetLabel, initialPricingSucceeded }) => {
vi.mocked(approveMeasurement).mockResolvedValue({ inboundId: '90001', measurementId: '70001', processStatus, 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 }, 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(`审核通过后,入库单将进入${targetLabel}`, '确认测量', expect.objectContaining({ confirmButtonText: '通过' }))
expect(initialPricingSucceeded ? success : warning).toHaveBeenCalledWith(expect.stringContaining(`入库单已进入${targetLabel}`))
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, raw measured volume, weight 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', '0.72', '差异 -1.28 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).not.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('uses original measured dimensions for unit volume, row volume and summary without replacing billing snapshots', async () => {
const value = { ...detail, originalVolumeCbm: 3.3, actualVolumeCbm: 7.205, items: [{ ...measuredItem }] }
vi.mocked(getMeasurementReviewDetail).mockResolvedValue(value)
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
const volumeRow = wrapper.get('.review-comparison tbody tr:last-child')
expect(volumeRow.findAll('td').map(cell => cell.text())).toEqual(['3.3', '3.688', '差异 +0.388 m³'])
const cells = wrapper.get('.review-items .el-table__body tbody tr').findAll('td')
expect(cells.slice(5, 7).map(cell => cell.text())).toEqual(['0.168', '3.688'])
expect(wrapper.get('.review-items').text()).not.toContain('1,203.235')
expect(value.actualVolumeCbm).toBe(7.205)
expect(value.items[0]).toMatchObject({ unitVolumeCbm: 0.3275, totalVolumeCbm: 7.205, outerLengthCm: 75 })
})
it('sums all unrounded measured row volumes before formatting the review comparison', async () => {
vi.mocked(getMeasurementReviewDetail).mockResolvedValue({ ...detail, originalVolumeCbm: 3.3, actualVolumeCbm: 9.999,
items: [{ ...measuredItem }, { ...measuredItem, id: 'item-2', boxCode: 'BOX-RAW-002', lengthCm: 50, widthCm: 40, heightCm: 30, packageCount: 2 }] })
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.get('.review-comparison tbody tr:last-child').findAll('td').map(cell => cell.text())).toEqual(['3.3', '3.808', '差异 +0.508 m³'])
})
it('separates warehouse overrides and their saved volume from original measured data', async () => {
const corrected = { ...measuredItem, outerLengthCm: 63, outerWidthCm: 53, outerHeightCm: 53,
unitVolumeCbm: 0.177, totalVolumeCbm: 3.894, volumeWeightKg: 650.298 }
const unmodified = { ...measuredItem, id: 'item-unmodified', boxCode: 'BOX-RAW-002',
lengthCm: 10, widthCm: 20, heightCm: 30, packageCount: 2,
outerLengthCm: null, outerWidthCm: null, outerHeightCm: null,
unitVolumeCbm: 0.006, totalVolumeCbm: 0.012, volumeWeightKg: 2.004 }
const value = { ...detail, originalVolumeCbm: 3.3, actualPackageCount: 24,
actualVolumeCbm: 3.906, items: [corrected, unmodified] }
vi.mocked(getMeasurementReviewDetail).mockResolvedValue(value)
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.get('.review-comparison tbody tr:last-child').findAll('td').map(cell => cell.text())).toEqual(['3.3', '3.7', '差异 +0.4 m³'])
const measuredCells = wrapper.get('.review-items .el-table__body tbody tr').findAll('td')
expect(measuredCells[3]!.text()).toBe('62 × 52 × 52')
expect(measuredCells.slice(5, 7).map(cell => cell.text())).toEqual(['0.168', '3.688'])
const outer = wrapper.get('.review-outer-items')
expect(outer.get('h3').text()).toBe('仓库修改外箱')
expect(outer.findAll('.el-table__body tbody tr')).toHaveLength(1)
expect(outer.get('.el-table__body tbody tr').findAll('td').map(cell => cell.text())).toEqual([
'1', 'BOX-RAW-001', '63', '53', '53', '22', '0.177', '3.894', '650.298',
])
expect(outer.get('.el-table__footer').findAll('td').map(cell => cell.text())).toEqual([
'合计', '', '', '', '', '22', '—', '3.894', '650.298',
])
expect(value.items[0]).toBe(corrected)
expect(corrected).toMatchObject({ lengthCm: 62, unitVolumeCbm: 0.177, totalVolumeCbm: 3.894 })
})
it('hides the warehouse outer section when no manual dimension was entered', async () => {
vi.mocked(getMeasurementReviewDetail).mockResolvedValue({ ...detail, items: [{ ...measuredItem,
outerLengthCm: null, outerWidthCm: null, outerHeightCm: null }] })
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.find('.review-outer-items').exists()).toBe(false)
expect(wrapper.get('.review-items').text()).toContain('3.688')
})
it('keeps partial outer edits and missing saved volumes explicit, then removes the section after clearing', async () => {
const item = { ...measuredItem, outerLengthCm: 63, outerWidthCm: null, outerHeightCm: null,
unitVolumeCbm: null, totalVolumeCbm: null, volumeWeightKg: null }
vi.mocked(getMeasurementReviewDetail).mockResolvedValue({ ...detail, items: [item] })
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
const outer = wrapper.get('.review-outer-items')
expect(outer.get('.el-table__body tbody tr').findAll('td').slice(2).map(cell => cell.text())).toEqual([
'63', '—', '—', '22', '—', '—', '—',
])
expect(outer.get('.el-table__footer').findAll('td').slice(7).map(cell => cell.text())).toEqual(['—', '—'])
vi.mocked(getMeasurementReviewDetail).mockResolvedValue({ ...detail, version: 5, items: [{ ...item, outerLengthCm: null }] })
await wrapper.get('.review-refresh').trigger('click')
await flushPromises()
expect(wrapper.find('.review-outer-items').exists()).toBe(false)
expect(wrapper.get('.review-items').text()).toContain('3.688')
})
it('totals only the saved correction rows before formatting', async () => {
vi.mocked(getMeasurementReviewDetail).mockResolvedValue({ ...detail, items: [
{ ...measuredItem, totalVolumeCbm: 1.2344 },
{ ...measuredItem, id: 'item-2', boxCode: 'BOX-RAW-002', totalVolumeCbm: 1.2344 },
] })
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.get('.review-outer-items .el-table__footer').findAll('td')[7]!.text()).toBe('2.469')
})
it.each([{ lengthCm: null }, { widthCm: 0 }, { heightCm: Number.NaN }, { packageCount: null }, { packageCount: 1.5 }])('keeps incomplete or invalid measured volume unknown instead of using billing totals: %o', async (invalid) => {
vi.mocked(getMeasurementReviewDetail).mockResolvedValue({ ...detail, items: [{ ...measuredItem }, { ...measuredItem, ...invalid, id: 'item-invalid' }] })
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.get('.review-comparison tbody tr:last-child').findAll('td').map(cell => cell.text())).toEqual(['2', '—', '—'])
expect(wrapper.findAll('.review-items .el-table__body tbody tr')[1]!.findAll('td')[6]!.text()).toBe('—')
})
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(['—', '—', '—'])
})
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('通过')
await wrapper.get('.review-weight-summary button').trigger('click')
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('异常放行')
})
})