Files
admin-web/src/modules/inbounds/measurement/InboundMeasurementPanel.test.ts
T
2026-10-20 21:33:46 +08:00

664 lines
31 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 { 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<string, string>()
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> = {}): 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('<section class="measurement-declaration"')
expect(source).not.toContain('<el-collapse-item name="declaration"')
expect(source).toContain('grid-template-columns: repeat(4, minmax(0, 1fr))')
expect(source).toContain('.measurement-data-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));')
expect(source).toContain('.measurement-table { width: 100%; font-size: var(--admin-font-caption, 12px); }')
expect(source).toContain('.measurement-data-grid :deep(.el-input__wrapper)')
expect(source).toContain('defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlockReason, mayMaintain, maySubmit, saving, submitting, canEdit, canSubmit, isDirty })')
})
it('aligns declared and measured values and keeps the declaration outside the measured section', async () => {
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()
})
})