292 lines
16 KiB
TypeScript
292 lines
16 KiB
TypeScript
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.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().includes('通过并进入待一件代发组托'))!.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__row')
|
||
expect(comparisonRows).toHaveLength(2)
|
||
expect(comparisonRows[0].text()).toContain('客户申报')
|
||
expect(comparisonRows[0].text()).toContain('箱数9')
|
||
expect(comparisonRows[0].text()).toContain('毛重118 kg')
|
||
expect(comparisonRows[0].text()).not.toContain('实际品名')
|
||
expect(comparisonRows[0].text()).not.toContain('原单据号码')
|
||
expect(comparisonRows[1].text()).toContain('平台实测')
|
||
expect(comparisonRows[1].text()).toContain('箱数10 差异 +1')
|
||
expect(comparisonRows[1].text()).toContain('毛重120 kg · 差异 +2 kg')
|
||
expect(comparisonRows[1].text()).toContain('货物净重110 kg')
|
||
expect(comparisonRows[1].text()).toContain('体积2.2 m³ · 差异 +0.2 m³')
|
||
expect(comparisonRows[1].text()).toContain('体积重367.4 kg')
|
||
|
||
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).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()
|
||
|
||
expect(wrapper.get('.review-comparison').text()).not.toContain('差异')
|
||
})
|
||
|
||
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().includes('通过并进入待分柜'))?.trigger('click')
|
||
await flushPromises()
|
||
|
||
expect(getMeasurementReviewDetail).toHaveBeenCalledTimes(2)
|
||
expect(approveMeasurement).toHaveBeenCalledWith('10001', '90001', { measurementId: '70001', expectedVersion: 8 })
|
||
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().includes('通过并进入待分柜'))!
|
||
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().includes('通过并进入待分柜'))!.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().includes('通过并进入待分柜'))).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().includes('通过并进入待分柜'))!.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('异常放行')
|
||
})
|
||
})
|