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: '', }, })) vi.mock('../measurement/components/MeasurementEvidenceThumbnail.vue', () => ({ default: { name: 'MeasurementEvidenceThumbnail', props: { customerId: String, inboundId: String, fileId: String, alt: String, emptyText: String }, emits: ['open'], template: '', }, })) 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(' { 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('异常放行') }) })