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

218 lines
13 KiB
TypeScript
Raw Normal View History

2026-08-04 21:01:39 +08:00
import ElementPlus, { 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'
2026-08-14 19:59:32 +08:00
import { downloadWeightImage, downloadWeightThumbnail } from '../weight/api/weightOcr'
import WeightImageThumbnail from '../weight/components/WeightImageThumbnail.vue'
2026-08-13 20:00:38 +08:00
import { approveMeasurement, getMeasurementReviewDetail, listMeasurementReviewHistory, rejectMeasurement } from './api'
2026-08-04 21:01:39 +08:00
import MeasurementReviewPanel from './MeasurementReviewPanel.vue'
vi.mock('./api', () => ({
approveMeasurement: vi.fn(),
getMeasurementReviewDetail: vi.fn(),
listMeasurementReviewHistory: vi.fn(),
rejectMeasurement: vi.fn(),
releaseRejectedMeasurement: vi.fn(),
}))
2026-08-14 19:59:32 +08:00
vi.mock('../weight/api/weightOcr', () => ({
downloadWeightImage: vi.fn(),
downloadWeightThumbnail: vi.fn(),
}))
2026-08-04 21:01:39 +08:00
const detail = {
inboundId: '90001', inboundCode: 'RK90001', companyName: '柏悦客户', channelCode: 'SEA', channelName: '海运',
processStatus: 'MEASURE_PENDING', measureReviewStatus: 'PENDING', recommendedPalletType: 'STANDARD', reviewReason: null, reviewerId: null, reviewerName: null,
2026-08-13 20:00:38 +08:00
measurementId: '70001', sourceType: 'DEVICE', measurementStatus: 'SUBMITTED', inboundVersion: 7, version: 4,
2026-08-07 16:17:45 +08:00
originalDocumentNo: 'ORD-001', originalGrossWeightKg: 100, originalVolumeCbm: 2, originalPackageCount: 10,
2026-08-04 21:01:39 +08:00
actualPackageCount: 10, actualGrossWeightKg: 110, actualVolumeCbm: 2.2, volumeWeightKg: 367.4, actualGoodsName: '测试货物',
2026-08-07 16:17:45 +08:00
items: [{ id: '1', boxCode: 'RK90001#10-1', itemNo: '001', boxSpecification: '60×40×30', packageCount: 10, lengthCm: 60, widthCm: 40, heightCm: 30, girthCm: 240, unitWeightKg: 11, unitVolumeCbm: 0.072, totalWeightKg: 110, totalVolumeCbm: 0.72, volumeWeightKg: 120.24, intelligentImageFileId: '30001', machineNo: 'MJ20210327', measuredAt: '2026-08-04T10:30:00+08:00', overLimitWarning: '周长超过渠道限制,请人工核验尺寸后再作出审核决定', outerLengthCm: 61, outerWidthCm: 41, outerHeightCm: 31, remark: '外箱存在轻微变形' }],
2026-08-14 17:38:06 +08:00
weightResults: [{ id: '2', fileId: 'file-1', palletId: '50001', palletCode: 'TP-001', palletType: '木托盘', palletCapacityCbm: 1.2, palletTareWeightKg: 10, ocrProvider: 'OCR', ocrRawText: '设备屏幕原始识别内容 120.5 KG,包含一段需要自动换行展示的较长文本', ocrWeightValue: 120.5, ocrWeightUnit: 'KG', confidence: 0.98, adjustedWeightValue: 120, adjustedWeightUnit: 'KG', grossWeightKg: 120, netWeightKg: 110, recognitionStatus: 'CONFIRMED', adjustmentReason: '修正 OCR 毛重读数', adjustedBy: '20001', adjustedAt: '2026-08-04T09:58:00', confirmedBy: '20002', confirmedAt: '2026-08-04T10:00:00', version: 1 }],
2026-08-04 21:01:39 +08:00
}
enableAutoUnmount(afterEach)
describe('MeasurementReviewPanel', () => {
beforeEach(() => {
vi.clearAllMocks()
2026-08-14 19:59:32 +08:00
Object.defineProperty(URL, 'createObjectURL', {
configurable: true,
value: vi.fn((blob: Blob) => blob.size > 5 ? 'blob:weight-original' : 'blob:weight-thumbnail'),
})
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() })
2026-08-04 21:01:39 +08:00
setActivePinia(createPinia())
const auth = useAuthStore()
auth.token = 'token'
auth.permissions = ['firstleg:inbound:measurement-review', 'firstleg:inbound:measurement-review-history', 'firstleg:inbound:exception-release']
useAccessPreviewStore().initializeAfterLogin()
vi.mocked(getMeasurementReviewDetail).mockResolvedValue(detail)
vi.mocked(listMeasurementReviewHistory).mockResolvedValue({ records: [], page: 1, size: 20, total: 0, pages: 0 })
2026-08-14 19:59:32 +08:00
vi.mocked(downloadWeightThumbnail).mockResolvedValue(new Blob(['thumb'], { type: 'image/jpeg' }))
vi.mocked(downloadWeightImage).mockResolvedValue(new Blob(['original-image'], { type: 'image/jpeg' }))
2026-08-04 21:01:39 +08:00
})
2026-08-14 19:59:32 +08:00
it('keeps the review focused on weight evidence and omits carton measurement details', async () => {
2026-08-04 21:01:39 +08:00
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
2026-08-14 19:59:32 +08:00
expect(wrapper.text()).toContain('入仓码')
expect(wrapper.text()).toContain('RK90001')
expect(wrapper.text()).toContain('柏悦客户 · 海运')
expect(wrapper.text()).toContain('合计货物重量')
expect(wrapper.text()).toContain('全部称重图片')
2026-08-17 12:31:27 +08:00
expect(wrapper.text()).toContain('托盘数量')
expect(wrapper.text()).toContain('1 托')
2026-08-14 19:59:32 +08:00
expect(wrapper.text()).toContain('称重图片')
2026-08-14 17:38:06 +08:00
expect(wrapper.text()).toContain('货物重量')
2026-08-14 19:59:32 +08:00
expect(wrapper.text()).toContain('110 kg')
2026-08-17 18:00:42 +08:00
expect(wrapper.text()).toContain('托盘类型:木托盘')
expect(wrapper.text()).toContain('服务端确认毛重:120 kg')
expect(wrapper.text()).toContain('托盘皮重:10 kg')
expect(wrapper.text()).not.toContain('TP-001')
2026-08-14 19:59:32 +08:00
expect(wrapper.text()).not.toContain('申报毛重')
expect(wrapper.text()).not.toContain('OCR 原文')
expect(wrapper.text()).not.toContain('设备屏幕原始识别内容')
expect(wrapper.text()).not.toContain('置信度')
expect(wrapper.text()).not.toContain('调整原因')
expect(wrapper.findAllComponents(WeightImageThumbnail)).toHaveLength(1)
expect(wrapper.text()).not.toContain('测量明细')
expect(wrapper.text()).not.toContain('实测长 × 宽 × 高(cm)')
expect(wrapper.text()).not.toContain('设备实测体积')
expect(wrapper.text()).not.toContain('材积重')
2026-08-04 21:01:39 +08:00
expect(wrapper.text()).not.toContain('租户')
2026-08-14 14:27:03 +08:00
expect(wrapper.text()).toContain('通过并进入待分柜')
2026-08-04 21:01:39 +08:00
expect(wrapper.text()).not.toContain('确认费用')
expect(getMeasurementReviewDetail).toHaveBeenCalledWith('10001', '90001')
2026-08-14 19:59:32 +08:00
expect(downloadWeightThumbnail).toHaveBeenCalledWith('10001', '90001', 'file-1')
2026-08-12 16:31:46 +08:00
const source = await import('./MeasurementReviewPanel.vue?raw').then((module) => module.default)
expect(source).toContain('defineExpose({ confirmApprove, openReasonDialog, isPending, isRejected, canReview, canRelease, actionLoading })')
2026-08-14 19:59:32 +08:00
expect(source).not.toContain('measurementColumns')
expect(source.indexOf('全部称重图片')).toBeLessThan(source.indexOf('审核结论'))
expect(source).not.toContain('<DataTable')
expect(source).toContain('<el-collapse class="review-history">')
})
it('shows every weight image at once with its cargo weight below', async () => {
vi.mocked(getMeasurementReviewDetail).mockResolvedValue({
...detail,
actualGrossWeightKg: 330,
weightResults: [
detail.weightResults[0],
{ ...detail.weightResults[0], id: '3', fileId: 'file-2', netWeightKg: 100 },
{ ...detail.weightResults[0], id: '4', fileId: 'file-3', netWeightKg: 120 },
],
})
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.findAll('.weight-evidence-card')).toHaveLength(3)
expect(wrapper.findAllComponents(WeightImageThumbnail)).toHaveLength(3)
expect(wrapper.findAll('.weight-evidence-card__weight').map((card) => card.text())).toEqual([
'货物重量110 kg',
'货物重量100 kg',
'货物重量120 kg',
])
expect(wrapper.find('.weight-overview').text()).toContain('330 kg')
2026-08-17 12:31:27 +08:00
expect(wrapper.find('.weight-overview').text()).toContain('3 托')
2026-08-14 19:59:32 +08:00
expect(downloadWeightThumbnail).toHaveBeenCalledTimes(3)
})
it('opens the authenticated original image with the reviewed weight facts', async () => {
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] }, attachTo: document.body })
await flushPromises()
await wrapper.find('.weight-evidence-card .weight-thumb').trigger('click')
await flushPromises()
expect(downloadWeightImage).toHaveBeenCalledWith('10001', '90001', 'file-1')
const preview = document.body.querySelector('.weight-preview')
2026-08-17 18:00:42 +08:00
expect(preview?.textContent).toContain('托盘类型')
expect(preview?.textContent).toContain('木托盘')
expect(preview?.textContent).not.toContain('TP-001')
2026-08-14 19:59:32 +08:00
expect(preview?.textContent).toContain('120 kg')
expect(preview?.textContent).toContain('10 kg')
expect(preview?.textContent).toContain('110 kg')
expect(preview?.querySelector('img')?.getAttribute('src')).toBe('blob:weight-original')
2026-08-04 21:01:39 +08:00
})
2026-08-17 18:00:42 +08:00
it('shows the empty pallet type explicitly without falling back to the pallet code', async () => {
vi.mocked(getMeasurementReviewDetail).mockResolvedValueOnce({
...detail,
weightResults: [{ ...detail.weightResults[0], palletType: '' }],
})
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('托盘类型:未记录类型')
expect(wrapper.text()).not.toContain('TP-001')
})
2026-08-04 21:01:39 +08:00
it('shows the rejected release action only when the permission and rejected state exist', 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('重量图片不清晰')
const auth = useAuthStore()
auth.permissions = ['firstleg:inbound:measurement-review-history']
useAccessPreviewStore().initializeAfterLogin()
await wrapper.vm.$nextTick()
expect(wrapper.findAll('button').some((button) => button.text().includes('异常放行'))).toBe(false)
})
it('emits changed after a successful review write', async () => {
2026-08-14 14:27:03 +08:00
vi.mocked(approveMeasurement).mockResolvedValue({ inboundId: '90001', measurementId: '70001', processStatus: 'WAIT_SPLIT', feeStatus: 'CALCULATED', initialPricingSucceeded: true, initialPricingMessage: null, initialFee: null })
2026-08-04 21:01:39 +08:00
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
const buttons = wrapper.findAll('button')
2026-08-14 14:27:03 +08:00
const approve = buttons.find((button) => button.text().includes('通过并进入待分柜'))
2026-08-04 21:01:39 +08:00
expect(approve).toBeDefined()
await approve?.trigger('click')
await flushPromises()
expect(ElMessageBox.confirm).toHaveBeenCalledWith(
2026-08-14 14:27:03 +08:00
expect.stringContaining('初算失败不影响审核通过'),
2026-08-04 21:01:39 +08:00
'确认测量审核通过',
2026-08-14 14:27:03 +08:00
expect.objectContaining({ confirmButtonText: '通过并进入待分柜' }),
2026-08-04 21:01:39 +08:00
)
2026-08-13 20:00:38 +08:00
expect(approveMeasurement).toHaveBeenCalledWith('10001', '90001', { measurementId: '70001', expectedVersion: 7 })
2026-08-04 21:01:39 +08:00
expect(wrapper.emitted('changed')).toHaveLength(1)
})
2026-08-13 20:00:38 +08:00
2026-08-14 14:27:03 +08:00
it('keeps approval successful when initial pricing fails', async () => {
vi.mocked(approveMeasurement).mockResolvedValue({ inboundId: '90001', measurementId: '70001', processStatus: 'WAIT_SPLIT', feeStatus: 'UNPRICED', initialPricingSucceeded: false, initialPricingMessage: '缺少有效价格', 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.findAll('button').find((button) => button.text().includes('通过并进入待分柜'))?.trigger('click')
await flushPromises()
2026-08-14 19:59:32 +08:00
expect(approveMeasurement).toHaveBeenCalledOnce()
2026-08-14 14:27:03 +08:00
expect(wrapper.emitted('changed')).toHaveLength(1)
2026-08-14 19:59:32 +08:00
expect(wrapper.text()).not.toContain('初算失败不阻断审核')
2026-08-14 14:27:03 +08:00
})
2026-08-13 20:00:38 +08:00
it('rejects with the inbound optimistic-lock version instead of the measurement version', async () => {
vi.mocked(rejectMeasurement).mockResolvedValue(undefined)
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
const reject = wrapper.findAll('button').find((button) => button.text().includes('驳回'))
await reject?.trigger('click')
await wrapper.find('textarea').setValue('重量图片需要重新核对')
const confirm = wrapper.findAll('button').find((button) => button.text().includes('确认驳回'))
await confirm?.trigger('click')
await flushPromises()
expect(rejectMeasurement).toHaveBeenCalledWith('10001', '90001', {
measurementId: '70001',
expectedVersion: 7,
reason: '重量图片需要重新核对',
})
})
2026-08-04 21:01:39 +08:00
})