import ElementPlus, { ElMessage, ElMessageBox } from 'element-plus' import { createPinia, setActivePinia } from 'pinia' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ApiResponseError } from '@/shared/api/http' import { useAuthStore } from '@/core/auth/auth' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { downloadInboundMeasurementImage, createManualCartonScan, getInboundMeasurement, saveInboundMeasurementDraft, submitInboundMeasurement, updateConfirmedPackageCount, } from './api' import type { InboundMeasurementDetail } from './types' import InboundMeasurementPanel from './InboundMeasurementPanel.vue' vi.mock('./api', () => ({ createManualCartonScan: vi.fn(), downloadInboundMeasurementImage: vi.fn(), getInboundMeasurement: vi.fn(), saveInboundMeasurementDraft: vi.fn(), submitInboundMeasurement: vi.fn(), updateConfirmedPackageCount: vi.fn(), })) class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} } function createMemoryStorage(): Storage { const values = new Map() return { get length() { return values.size }, clear: () => values.clear(), getItem: (key) => values.get(key) ?? null, key: (index) => [...values.keys()][index] ?? null, removeItem: (key) => values.delete(key), setItem: (key, value) => values.set(key, value), } } function measurement(overrides: Partial = {}): InboundMeasurementDetail { return { inboundId: '7001', inboundCode: 'RK202608040001', companyName: '测试企业', channelCode: 'SEA-FBA', channelName: '美西海运', processStatus: 'MEASURING', measureReviewStatus: null, customerFeeConfirmed: false, recommendedPalletType: 'STANDARD', reviewReason: null, reviewerId: null, reviewerName: null, measurementId: '9001', sourceType: 'THIRD_PARTY', measurementStatus: 'RECEIVED', version: 2, originalDocumentNo: 'DOC-001', originalGrossWeightKg: 118.5, originalVolumeCbm: 0.8, originalPackageCount: 10, actualPackageCount: 10, actualGrossWeightKg: 120, actualNetWeightKg: 110, actualVolumeCbm: 0.6, volumeWeightKg: 100.2, actualGoodsName: '蓝牙音箱', latestDeclaration: { referenceId: 'DOC-001', totalPackageCount: 10, grossWeightKg: 118.5, volumeCbm: 0.8, goodsName: '蓝牙音箱', items: [{ inboundDetailId: '8101', itemNo: 1, cargoName: '蓝牙音箱', packageCount: 10, lengthCm: 50, widthCm: 40, heightCm: 30, }], }, items: [{ id: '9101', inboundDetailId: '8101', inboundCode: 'RK202608040001', fbaShipmentNo: 'FBA18ABC1234', scannedShipmentId: 'FBA18SCAN5678', boxCode: 'RK202608040001#10-1', itemNo: '001', boxSpecification: '50×40×30cm', packageCount: 10, lengthCm: 50, widthCm: 40, heightCm: 30, girthCm: 190, unitWeightKg: 12, unitVolumeCbm: 0.06, totalWeightKg: 120, totalVolumeCbm: 0.6, volumeWeightKg: 100.2, intelligentImageFileId: '88001', machineNo: 'MJ20210327', deviceMeasuredAt: '2026-08-04T10:30:00+08:00', overLimitWarning: null, outerLengthCm: null, outerWidthCm: null, outerHeightCm: null, calculatedOuterLengthCm: 50, calculatedOuterWidthCm: 40, calculatedOuterHeightCm: 30, calculationRuleVersion: 'DIMENSION_ACTUAL_V1', remark: '设备自动回传', }], weightResults: [{ id: '30001', fileId: '40001', palletId: '50001', palletCode: 'TP-001', palletType: '木托盘', palletCapacityCbm: 1.2, palletTareWeightKg: 10, ocrProvider: 'FILE_WEIGHT_OCR', ocrRawText: '120', ocrWeightValue: 120, ocrWeightUnit: 'KG', confidence: 0.98, adjustedWeightValue: null, adjustedWeightUnit: null, grossWeightKg: 120, netWeightKg: 110, recognitionStatus: 'CONFIRMED', adjustmentReason: null, adjustedBy: null, adjustedAt: null, confirmedBy: '9', confirmedAt: '2026-08-04T10:35:00+08:00', version: 1, }], ...overrides, } } async function mountPanel(permissions = [ 'platform:firstleg:inbound:measure:view', 'platform:firstleg:inbound:measure:edit', 'platform:firstleg:inbound:measure:manual-entry', 'platform:firstleg:inbound:measure:submit', ], props: Partial<{ customerId: string; inboundId: string; customerName: string }> = {}) { const pinia = createPinia() setActivePinia(pinia) const auth = useAuthStore() auth.token = 'token' auth.permissions = permissions useAccessPreviewStore().initializeAfterLogin() const wrapper = mount(InboundMeasurementPanel, { props: { customerId: '10001', inboundId: '7001', ...props }, attachTo: document.body, global: { plugins: [pinia, ElementPlus] }, }) await flushPromises() return wrapper } enableAutoUnmount(afterEach) afterAll(() => vi.unstubAllGlobals()) describe('InboundMeasurementPanel', () => { beforeEach(() => { vi.stubGlobal('ResizeObserver', ResizeObserverStub) vi.stubGlobal('localStorage', createMemoryStorage()) vi.stubGlobal('sessionStorage', createMemoryStorage()) document.body.innerHTML = '' vi.mocked(getInboundMeasurement).mockReset().mockResolvedValue(measurement()) vi.mocked(updateConfirmedPackageCount).mockReset().mockResolvedValue({ measurementId: '9001', measurementVersion: 3, itemId: '9101', boxCode: 'RK202608040001#10-1', confirmedPackageCount: 10, }) vi.mocked(saveInboundMeasurementDraft).mockReset().mockResolvedValue({ measurementId: '9001', version: 3, status: 'DRAFT' }) vi.mocked(submitInboundMeasurement).mockReset().mockResolvedValue({ measurementId: '9001', version: 2, processStatus: 'MEASURE_PENDING', measureReviewStatus: 'PENDING', recommendedPalletType: 'STANDARD', feeId: null, feeNo: null, feeStatus: null, }) vi.mocked(createManualCartonScan).mockReset().mockResolvedValue({ measurementId: '9001', measurementVersion: 3, itemId: '9102', boxCode: 'RK202608040001#10-2', confirmedPackageCount: 1, }) vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never) }) it('loads third-party facts and renders server-calculated values separately', async () => { const wrapper = await mountPanel() expect(getInboundMeasurement).toHaveBeenCalledWith('10001', '7001') expect(wrapper.text()).toContain('RK202608040001') expect(wrapper.find('.measurement-table').text()).toContain('FBA18ABC1234') expect(wrapper.find('.measurement-table').text()).toContain('FBA18SCAN5678') expect(wrapper.find('.measurement-table').text()).toContain('0.6') expect(wrapper.find('.measurement-table').text()).not.toContain('周长') await wrapper.find('.el-table__expand-icon').trigger('click') await flushPromises() expect(wrapper.text()).toContain('箱规周长') expect(wrapper.text()).toContain('190 cm') expect(wrapper.text()).toContain('MJ20210327') expect(wrapper.text()).toContain('2026-08-04 10:30:00+08:00') expect(wrapper.find('button[aria-label="预览测量图片"]').exists()).toBe(true) }) it('uses the list customer name when a legacy measurement detail has no customer snapshot', async () => { vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ companyName: '' })) const wrapper = await mountPanel(undefined, { customerName: '柏悦物流' }) expect(wrapper.find('.measurement-summary-context').text()).toContain('柏悦物流') }) it('warns when declared dimensions or gross weight differ from measurements by at least ten percent', async () => { vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ actualGrossWeightKg: 132, items: [{ ...measurement().items[0], lengthCm: 55, widthCm: 36, }], })) const wrapper = await mountPanel() const alert = wrapper.find('.measurement-deviation-alert') expect(alert.exists()).toBe(true) expect(alert.text()).toContain('下单箱规与测量数据差异达到 10%') expect(alert.text()).toContain('长偏差 10%') expect(alert.text()).toContain('下单 50 cm → 实测 55 cm') expect(alert.text()).toContain('宽偏差 10%') expect(alert.text()).toContain('重量偏差 11.4%') expect(alert.text()).not.toContain('高偏差') }) it('does not warn when every declared and measured value differs by less than ten percent', async () => { const wrapper = await mountPanel() expect(wrapper.find('.measurement-deviation-alert').exists()).toBe(false) }) it('opens the manual carton entry form in an editable measurement state', async () => { const wrapper = await mountPanel() const button = wrapper.findAll('button').find((candidate) => candidate.text().includes('手动录入箱规')) expect(button?.attributes('disabled')).toBeUndefined() await button?.trigger('click') expect(wrapper.text()).toContain('手动录入箱规') expect(document.body.textContent).toContain('箱号') expect(document.body.textContent).toContain('测量长(cm)') expect(document.body.textContent).toContain('测量图片') expect(document.body.textContent).toContain('保存箱规') }) it('does not expose manual carton entry without its dedicated permission', async () => { const wrapper = await mountPanel([ 'platform:firstleg:inbound:measure:view', 'platform:firstleg:inbound:measure:edit', 'platform:firstleg:inbound:measure:submit', ]) expect(wrapper.text()).not.toContain('手动录入箱规') }) it('does not expose unknown measurement source codes', async () => { vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ sourceType: 'LEGACY_DEVICE' })) const wrapper = await mountPanel() expect(wrapper.text()).toContain('未知来源') expect(wrapper.text()).not.toContain('LEGACY_DEVICE') }) it('keeps key totals compact and shows declaration information without another disclosure card', async () => { const wrapper = await mountPanel() const text = wrapper.text() expect(text).toContain('箱数') expect(text).toContain('货物净重') expect(text).toContain('总体积') expect(text).toContain('材积重') expect(text).toContain('客户申报') expect(text).toContain('平台实测') expect(text).toContain('差异 +1.5 kg') expect(text).toContain('差异 -0.200 m³') expect(text).toContain('平台实测箱规') expect(text).not.toContain('租户') expect(text).not.toContain('数据版本') expect(wrapper.find('.measurement-table').exists()).toBe(true) expect(wrapper.find('.measurement-item').exists()).toBe(false) expect(wrapper.find('.measurement-table').text()).not.toContain('周长') expect(wrapper.text()).not.toContain('每箱毛重') expect(wrapper.text()).not.toContain('总重量(kg)') const source = await import('./InboundMeasurementPanel.vue?raw').then((module) => module.default) expect(source).toContain('
{ const wrapper = await mountPanel() const rows = wrapper.findAll('.measurement-comparison tbody tr') expect(rows.map(row => row.findAll('td').map(cell => cell.text()))).toEqual([ ['10', '10', '一致'], ['118.5', '120', '差异 +1.5 kg'], ['0.800', '0.600', '差异 -0.200 m³'], ]) expect(wrapper.find('.measurement-summary-section .measurement-declaration').exists()).toBe(false) expect(wrapper.find('.measurement-declaration').element.parentElement).toBe(wrapper.find('.measurement-summary-section').element.parentElement) }) it('shows pending comparison rather than a negative count when no boxes have been measured', async () => { vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ items: [], actualPackageCount: null, actualGrossWeightKg: null, actualVolumeCbm: null, })) const wrapper = await mountPanel() const rows = wrapper.findAll('.measurement-comparison tbody tr') for (const row of rows) { expect(row.findAll('td')[1].text()).toBe('未测量') expect(row.findAll('td')[2].text()).toBe('待测量') } }) it('groups titles and related fields into shared header rows without duplicating submission warnings', async () => { vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ measurementId: null, items: [], weightResults: [] })) const wrapper = await mountPanel() expect(wrapper.find('.measurement-summary-header h4').text()).toBe('申报与实测对比') expect(wrapper.find('.measurement-declaration-header').text()).not.toContain('DOC-001') expect(wrapper.find('.measurement-section__heading .measurement-goods-name').exists()).toBe(false) expect(wrapper.find('.measurement-section__heading .measurement-section-actions').text()).toContain('手动录入箱规') expect(wrapper.findAll('.el-alert').some(alert => alert.text().includes('请先完成箱规测量'))).toBe(false) expect(wrapper.find('.measurement-panel__footer').text()).toContain('请先完成箱规测量') }) it('renders customer declaration facts as plain read-only values', async () => { const wrapper = await mountPanel() const declaration = wrapper.find('.measurement-declaration') const comparison = wrapper.find('.measurement-comparison') expect(comparison.text()).toContain('118.5') expect(comparison.text()).toContain('0.8') expect(comparison.findAll('tbody tr')[0].text()).toContain('10') expect(declaration.text()).not.toContain('原单据号码') expect(declaration.text()).not.toContain('DOC-001') expect(declaration.findAll('.measurement-readonly-field')).toHaveLength(0) expect(declaration.findAll('input')).toHaveLength(0) expect(wrapper.find('.measurement-items-section .measurement-goods-name').exists()).toBe(false) expect(declaration.find('[aria-label="客户申报箱规明细"]').text()).toContain('蓝牙音箱') expect(declaration.find('[aria-label="客户申报箱规明细"]').text()).toContain('50 × 40 × 30') }) it('does not render differences when the corresponding declaration is unavailable', async () => { vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ originalGrossWeightKg: null, originalVolumeCbm: null })) const wrapper = await mountPanel() expect(wrapper.findAll('.measurement-comparison tbody tr').slice(1).map(row => row.findAll('td')[2].text())).toEqual(['—', '—']) }) it('shows all declaration fields as plain text when measurement is not editable', async () => { vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ processStatus: 'WAIT_SPLIT' })) const wrapper = await mountPanel() const declaration = wrapper.find('.measurement-declaration') expect(declaration.find('input').exists()).toBe(false) expect(declaration.text()).toContain('蓝牙音箱') expect(declaration.findAll('.measurement-readonly-field')).toHaveLength(0) expect(wrapper.find('.measurement-items-section .measurement-goods-name').exists()).toBe(false) }) it('preserves the hidden actual goods name without overriding declaration facts', async () => { const wrapper = await mountPanel() expect(wrapper.text()).not.toContain('实际品名') await wrapper.find('input[aria-label="仓库修改外箱长"]').setValue('55') const save = wrapper.findAll('button').find((button) => button.text().includes('保存测量数据')) await save?.trigger('click') await flushPromises() expect(saveInboundMeasurementDraft).toHaveBeenCalledWith('10001', '7001', expect.objectContaining({ measurementId: '9001', version: 2, actualGoodsName: '蓝牙音箱', items: expect.any(Array), })) const payload = vi.mocked(saveInboundMeasurementDraft).mock.calls[0]?.[2] expect(payload).not.toHaveProperty('originalDocumentNo') expect(payload).not.toHaveProperty('originalGrossWeightKg') expect(payload).not.toHaveProperty('originalVolumeCbm') }) it('allows saving one warehouse outer dimension against the measured sequence', async () => { const wrapper = await mountPanel() expect(wrapper.text()).toContain('001') await wrapper.find('input[aria-label="仓库修改外箱长"]').setValue('55') const save = wrapper.findAll('button').find((button) => button.text().includes('保存测量数据')) await save?.trigger('click') await flushPromises() const payload = vi.mocked(saveInboundMeasurementDraft).mock.calls[0]?.[2] expect(payload?.items[0]).toMatchObject({ itemNo: '001', outerLengthCm: 55, outerWidthCm: null, outerHeightCm: null, }) }) it('updates the server weight summary without discarding unsaved measurement edits', async () => { const wrapper = await mountPanel() const countInput = wrapper.find('.measurement-count-editor input') await countInput.setValue('9') wrapper.vm.updateWeightEvidence(125.75, 115.75, true) await flushPromises() expect(wrapper.find('.measurement-summary-section').text()).toContain('125.75') expect(wrapper.find('.measurement-summary-section').text()).toContain('115.75') expect((countInput.element as HTMLInputElement).value).toBe('9') }) it('shows the complete server-calculated measurement facts in the expanded row', async () => { const wrapper = await mountPanel() await wrapper.get('.el-table__expand-icon').trigger('click') await flushPromises() const text = wrapper.text() expect(text).toContain('箱规') expect(text).toContain('50×40×30cm') expect(text).toContain('计算方式') expect(text).toContain('实测原尺寸') expect(text).toContain('计算后外箱') expect(text).toContain('50 × 40 × 30 cm') expect(text).toContain('计算规则版本') expect(text).toContain('DIMENSION_ACTUAL_V1') }) it('reuses the established first-leg detail surface and status patterns', async () => { const wrapper = await mountPanel() expect(wrapper.classes()).toContain('surface-panel') expect(wrapper.find('.measurement-summary-header').exists()).toBe(true) expect(wrapper.find('.measurement-summary-status').text()).toContain('测量中') expect(wrapper.find('.measurement-summary-header').text()).not.toContain('RK202608040001') expect(wrapper.find('.measurement-summary-header').text()).toContain('测试企业') expect(wrapper.find('.measurement-summary-header').text()).toContain('美西海运') }) it('saves only the changed confirmed package count with the current measurement version', async () => { vi.mocked(updateConfirmedPackageCount).mockResolvedValueOnce({ measurementId: '9001', measurementVersion: 3, itemId: '9101', boxCode: 'RK202608040001#10-1', confirmedPackageCount: 9, }) const wrapper = await mountPanel() const countInput = wrapper.find('.measurement-count-editor input') await countInput?.setValue('9') const save = wrapper.findAll('button').find((button) => button.text().includes('保存测量数据')) await save?.trigger('click') await flushPromises() expect(updateConfirmedPackageCount).toHaveBeenCalledWith('10001', '7001', '9101', { measurementVersion: 2, confirmedPackageCount: 9, }) }) it('does not submit a second count update when the full draft already saved the changed count', async () => { const wrapper = await mountPanel() await wrapper.find('.measurement-count-editor input').setValue('9') await wrapper.find('input[aria-label="仓库修改外箱长"]').setValue('55') const save = wrapper.findAll('button').find((button) => button.text().includes('保存测量数据')) await save?.trigger('click') await flushPromises() expect(saveInboundMeasurementDraft).toHaveBeenCalledWith('10001', '7001', expect.objectContaining({ version: 2, items: [expect.objectContaining({ packageCount: 9, outerLengthCm: 55 })], })) expect(updateConfirmedPackageCount).not.toHaveBeenCalled() }) it('preserves the edited form when the backend reports a version conflict', async () => { vi.mocked(updateConfirmedPackageCount).mockRejectedValueOnce( new ApiResponseError('测量数据已被其他人修改,请刷新后重试', 'A0409', 200, null, 'req-conflict', 'trace-conflict'), ) const wrapper = await mountPanel() const countInput = wrapper.find('.measurement-count-editor input') await countInput?.setValue('9') const save = wrapper.findAll('button').find((button) => button.text().includes('保存测量数据')) await save?.trigger('click') await flushPromises() expect(wrapper.text()).toContain('数据版本已变化,当前修改尚未丢失') expect(wrapper.text()).toContain('req-conflict') expect((countInput?.element as HTMLInputElement).value).toBe('9') expect(getInboundMeasurement).toHaveBeenCalledTimes(1) }) it('shows a normal A0409 business reason instead of relabeling it as a version conflict', async () => { const messageSpy = vi.spyOn(ElMessage, 'error') vi.mocked(updateConfirmedPackageCount).mockRejectedValueOnce( new ApiResponseError('该托盘已被占用,请重新选择', 'A0409', 409), ) const wrapper = await mountPanel() const countInput = wrapper.find('.measurement-count-editor input') await countInput.setValue('9') const save = wrapper.findAll('button').find((button) => button.text().includes('保存测量数据')) await save?.trigger('click') await flushPromises() expect(messageSpy).toHaveBeenCalledWith(expect.objectContaining({ message: '该托盘已被占用,请重新选择', showClose: true, duration: 4500, })) expect(wrapper.text()).not.toContain('操作未完成') expect(wrapper.text()).not.toContain('数据版本已变化,当前修改尚未丢失') }) it('reports draft validation as a dismissible light message without a fixed page alert', async () => { const messageSpy = vi.spyOn(ElMessage, 'warning') vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ items: [] })) const wrapper = await mountPanel() const save = wrapper.findAll('button').find((button) => button.text().includes('保存测量数据')) await save?.trigger('click') await flushPromises() expect(messageSpy).toHaveBeenCalledWith(expect.objectContaining({ message: '请至少保留一条实际扫描箱测量记录', showClose: true, duration: 4500, })) expect(wrapper.text()).not.toContain('请修正测量草稿中的问题') }) it('requires the dirty draft to be saved before submitting for review', async () => { const wrapper = await mountPanel() const countInput = wrapper.find('.measurement-count-editor input') await countInput?.setValue('9') const submit = wrapper.findAll('button').find((button) => button.text().includes('提交审核')) await submit?.trigger('click') await flushPromises() expect(submitInboundMeasurement).not.toHaveBeenCalled() expect(ElMessageBox.confirm).not.toHaveBeenCalled() }) it('submits the saved version and emits changed after confirmation', async () => { const wrapper = await mountPanel() const submit = wrapper.findAll('button').find((button) => button.text().includes('提交审核')) await submit?.trigger('click') await flushPromises() expect(ElMessageBox.confirm).toHaveBeenCalledWith( expect.stringContaining('提交成功后本批次不能继续编辑'), '提交测量审核', expect.objectContaining({ confirmButtonText: '确认提交' }), ) expect(submitInboundMeasurement).toHaveBeenCalledWith('10001', '7001', { measurementId: '9001', version: 2 }) expect(wrapper.emitted('changed')).toHaveLength(1) }) it('explains missing measurement prerequisites and avoids duplicate empty source status', async () => { vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ measurementId: null, items: [], sourceType: null, weightResults: [], })) const wrapper = await mountPanel() expect(wrapper.vm.maySubmit).toBe(false) expect(wrapper.vm.submissionBlockReason).toContain('请先完成箱规测量') expect(wrapper.vm.submissionBlockReason).toContain('称重图片') expect(wrapper.find('.measurement-summary-status').text()).not.toContain('尚未测量') expect(wrapper.find('.measurement-panel__footer [role="status"]').text()).toContain('请先完成箱规测量') }) it('explains order review and submitted-state blockers before measurement prerequisites', async () => { vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ processStatus: 'PREPARING', measurementId: null })) const pendingOrder = await mountPanel() expect(pendingOrder.vm.submissionBlockReason).toContain('订单尚待客服审核') vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ processStatus: 'MEASURE_PENDING' })) const pendingMeasurement = await mountPanel() expect(pendingMeasurement.vm.submissionBlockReason).toContain('不能提交测量审核') }) it('keeps dropship pallet edits in the platform section and declaration facts read-only', async () => { vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ fulfillmentType: 'ONE_CLICK_DROPSHIP', expectedPalletCount: 2 })) const wrapper = await mountPanel() expect(wrapper.find('.measurement-declaration input').exists()).toBe(false) expect(wrapper.find('.measurement-items-section .measurement-expected-pallets input').exists()).toBe(true) expect(wrapper.find('.measurement-items-section .measurement-goods-name').exists()).toBe(false) }) it('requires at least one confirmed weight image before submitting for review', async () => { const messageSpy = vi.spyOn(ElMessage, 'warning') vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ weightResults: [] })) const wrapper = await mountPanel() expect(wrapper.text()).toContain('即使只有一箱,也必须上传并确认至少一张称重图片后才能提交审核') const submit = wrapper.findAll('button').find((button) => button.text().includes('提交审核')) expect(submit?.attributes('disabled')).toBeDefined() await wrapper.vm.submitForReview() expect(messageSpy).toHaveBeenCalledWith('即使只有一箱,也必须上传并确认至少一张称重图片后才能提交审核') expect(submitInboundMeasurement).not.toHaveBeenCalled() }) it('updates submission eligibility when the weight panel confirms or removes the last image', async () => { const wrapper = await mountPanel() expect(wrapper.vm.maySubmit).toBe(true) wrapper.vm.updateWeightEvidence(null, null, false) await flushPromises() expect(wrapper.vm.maySubmit).toBe(false) expect(wrapper.vm.submissionBlockReason).toContain('必须上传并确认') wrapper.vm.updateWeightEvidence(125.75, 115.75, true) await flushPromises() expect(wrapper.vm.maySubmit).toBe(true) }) it('emits updated without closing the parent task after manual carton entry', async () => { const wrapper = await mountPanel() const source = await import('./InboundMeasurementPanel.vue?raw').then((module) => module.default) expect(source).toContain("emit('updated')") expect(wrapper.emitted('updated')).toBeUndefined() }) it('maps the formal deviceMeasuredAt response field into the read-only measurement time', async () => { const wrapper = await mountPanel() await wrapper.get('.el-table__expand-icon').trigger('click') expect(wrapper.text()).toContain('2026-08-04 10:30:00+08:00') const source = await import('./InboundMeasurementPanel.vue?raw').then((module) => module.default) expect(source).toContain('measuredAt: item.deviceMeasuredAt') expect(source).not.toContain('measuredAt: item.measuredAt') }) it('blocks normal submission when confirmed box counts do not match the order total', async () => { vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ originalPackageCount: 12 })) const wrapper = await mountPanel() expect(wrapper.text()).not.toContain('确认箱数尚未与订单总箱数匹配') const submit = wrapper.findAll('button').find((button) => button.text().includes('提交审核')) expect(submit?.attributes('disabled')).toBeDefined() expect(submitInboundMeasurement).not.toHaveBeenCalled() }) it('keeps device facts read-only but allows manual count correction after submission', async () => { vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ processStatus: 'MEASURE_PENDING' })) const wrapper = await mountPanel() const countInput = wrapper.find('.measurement-count-editor input') expect((countInput?.element as HTMLInputElement).matches(':disabled')).toBe(false) expect(wrapper.findAll('button').find((button) => button.text().includes('保存测量数据'))?.attributes('disabled')).toBeUndefined() expect(wrapper.findAll('button').find((button) => button.text().includes('提交审核'))?.attributes('disabled')).toBeDefined() expect(vi.mocked(downloadInboundMeasurementImage)).not.toHaveBeenCalled() }) it('clears the previous customer context before loading the next one', async () => { const wrapper = await mountPanel() vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ inboundId: '7002', inboundCode: 'RK202608040002', companyName: '第二企业' })) await wrapper.setProps({ customerId: '10002', inboundId: '7002' }) await flushPromises() expect(getInboundMeasurement).toHaveBeenLastCalledWith('10002', '7002') expect(wrapper.find('.measurement-summary-header').text()).toContain('第二企业') expect(wrapper.find('.measurement-summary-header').text()).not.toContain('测试企业') }) it('renders a permission-specific read-only state without requesting data', async () => { const wrapper = await mountPanel([]) expect(wrapper.text()).toContain('无测量查看权限') expect(getInboundMeasurement).not.toHaveBeenCalled() }) })