Files
admin-web/src/modules/inbounds/measurement/InboundMeasurementPanel.test.ts
T

664 lines
31 KiB
TypeScript
Raw Normal View History

2026-08-18 09:09:34 +08:00
import ElementPlus, { ElMessage, ElMessageBox } from 'element-plus'
2026-08-04 21:01:39 +08:00
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 {
2026-08-07 17:14:42 +08:00
downloadInboundMeasurementImage,
2026-08-12 14:08:33 +08:00
createManualCartonScan,
2026-08-04 21:01:39 +08:00
getInboundMeasurement,
2026-08-13 19:00:37 +08:00
saveInboundMeasurementDraft,
2026-08-04 21:01:39 +08:00
submitInboundMeasurement,
2026-08-07 17:14:42 +08:00
updateConfirmedPackageCount,
2026-08-04 21:01:39 +08:00
} from './api'
import type { InboundMeasurementDetail } from './types'
import InboundMeasurementPanel from './InboundMeasurementPanel.vue'
vi.mock('./api', () => ({
2026-08-12 14:08:33 +08:00
createManualCartonScan: vi.fn(),
2026-08-07 17:14:42 +08:00
downloadInboundMeasurementImage: vi.fn(),
2026-08-04 21:01:39 +08:00
getInboundMeasurement: vi.fn(),
saveInboundMeasurementDraft: vi.fn(),
submitInboundMeasurement: vi.fn(),
2026-08-07 17:14:42 +08:00
updateConfirmedPackageCount: vi.fn(),
2026-08-04 21:01:39 +08:00
}))
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,
2026-09-18 11:41:27 +08:00
customerFeeConfirmed: false,
2026-08-04 21:01:39 +08:00
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,
2026-08-07 16:17:45 +08:00
originalPackageCount: 10,
2026-08-04 21:01:39 +08:00
actualPackageCount: 10,
actualGrossWeightKg: 120,
2026-08-18 11:33:01 +08:00
actualNetWeightKg: 110,
2026-08-04 21:01:39 +08:00
actualVolumeCbm: 0.6,
volumeWeightKg: 100.2,
actualGoodsName: '蓝牙音箱',
2026-09-03 11:13:28 +08:00
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,
}],
},
2026-08-04 21:01:39 +08:00
items: [{
id: '9101',
2026-09-03 11:13:28 +08:00
inboundDetailId: '8101',
2026-09-08 17:38:09 +08:00
inboundCode: 'RK202608040001',
fbaShipmentNo: 'FBA18ABC1234',
scannedShipmentId: 'FBA18SCAN5678',
2026-08-07 16:17:45 +08:00
boxCode: 'RK202608040001#10-1',
2026-08-04 21:01:39 +08:00
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',
2026-08-07 16:17:45 +08:00
machineNo: 'MJ20210327',
2026-08-18 16:09:43 +08:00
deviceMeasuredAt: '2026-08-04T10:30:00+08:00',
2026-08-04 21:01:39 +08:00
overLimitWarning: null,
outerLengthCm: null,
outerWidthCm: null,
outerHeightCm: null,
2026-09-11 16:17:21 +08:00
calculatedOuterLengthCm: 50,
calculatedOuterWidthCm: 40,
calculatedOuterHeightCm: 30,
calculationRuleVersion: 'DIMENSION_ACTUAL_V1',
2026-08-04 21:01:39 +08:00
remark: '设备自动回传',
}],
2026-09-10 17:13:08 +08:00
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,
}],
2026-08-04 21:01:39 +08:00
...overrides,
}
}
async function mountPanel(permissions = [
'platform:firstleg:inbound:measure:view',
'platform:firstleg:inbound:measure:edit',
2026-08-12 14:57:02 +08:00
'platform:firstleg:inbound:measure:manual-entry',
2026-08-04 21:01:39 +08:00
'platform:firstleg:inbound:measure:submit',
2026-09-18 11:41:27 +08:00
], props: Partial<{ customerId: string; inboundId: string; customerName: string }> = {}) {
2026-08-04 21:01:39 +08:00
const pinia = createPinia()
setActivePinia(pinia)
const auth = useAuthStore()
auth.token = 'token'
auth.permissions = permissions
useAccessPreviewStore().initializeAfterLogin()
const wrapper = mount(InboundMeasurementPanel, {
2026-09-18 11:41:27 +08:00
props: { customerId: '10001', inboundId: '7001', ...props },
2026-08-04 21:01:39 +08:00
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())
2026-08-07 17:14:42 +08:00
vi.mocked(updateConfirmedPackageCount).mockReset().mockResolvedValue({
measurementId: '9001', measurementVersion: 3, itemId: '9101',
boxCode: 'RK202608040001#10-1', confirmedPackageCount: 10,
})
2026-08-13 19:00:37 +08:00
vi.mocked(saveInboundMeasurementDraft).mockReset().mockResolvedValue({ measurementId: '9001', version: 3, status: 'DRAFT' })
2026-08-04 21:01:39 +08:00
vi.mocked(submitInboundMeasurement).mockReset().mockResolvedValue({
measurementId: '9001', version: 2, processStatus: 'MEASURE_PENDING', measureReviewStatus: 'PENDING',
recommendedPalletType: 'STANDARD', feeId: null, feeNo: null, feeStatus: null,
})
2026-08-12 14:08:33 +08:00
vi.mocked(createManualCartonScan).mockReset().mockResolvedValue({
measurementId: '9001', measurementVersion: 3, itemId: '9102', boxCode: 'RK202608040001#10-2',
confirmedPackageCount: 1,
})
2026-08-04 21:01:39 +08:00
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')
2026-09-08 17:38:09 +08:00
expect(wrapper.find('.measurement-table').text()).toContain('FBA18ABC1234')
expect(wrapper.find('.measurement-table').text()).toContain('FBA18SCAN5678')
2026-08-13 17:24:43 +08:00
expect(wrapper.find('.measurement-table').text()).toContain('0.6')
2026-08-14 19:59:32 +08:00
expect(wrapper.find('.measurement-table').text()).not.toContain('周长')
2026-08-13 17:24:43 +08:00
await wrapper.find('.el-table__expand-icon').trigger('click')
await flushPromises()
2026-08-14 19:59:32 +08:00
expect(wrapper.text()).toContain('箱规周长')
expect(wrapper.text()).toContain('190 cm')
2026-08-07 17:14:42 +08:00
expect(wrapper.text()).toContain('MJ20210327')
2026-08-13 17:24:43 +08:00
expect(wrapper.text()).toContain('2026-08-04 10:30:00+08:00')
expect(wrapper.find('button[aria-label="预览测量图片"]').exists()).toBe(true)
2026-08-04 21:01:39 +08:00
})
2026-09-18 11:41:27 +08:00
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('柏悦物流')
})
2026-09-03 11:13:28 +08:00
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)
})
2026-08-12 14:08:33 +08:00
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('保存箱规')
})
2026-08-12 14:57:02 +08:00
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('手动录入箱规')
})
2026-08-12 09:25:55 +08:00
it('does not expose unknown measurement source codes', async () => {
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ sourceType: 'LEGACY_DEVICE' }))
const wrapper = await mountPanel()
2026-08-14 19:59:32 +08:00
expect(wrapper.text()).toContain('未知来源')
2026-08-12 09:25:55 +08:00
expect(wrapper.text()).not.toContain('LEGACY_DEVICE')
})
2026-08-17 18:32:41 +08:00
it('keeps key totals compact and shows declaration information without another disclosure card', async () => {
2026-08-04 21:01:39 +08:00
const wrapper = await mountPanel()
const text = wrapper.text()
2026-10-08 17:08:03 +08:00
expect(text).toContain('箱数')
2026-08-18 16:09:43 +08:00
expect(text).toContain('货物净重')
2026-08-14 19:59:32 +08:00
expect(text).toContain('总体积')
expect(text).toContain('材积重')
2026-08-18 16:09:43 +08:00
expect(text).toContain('客户申报')
expect(text).toContain('平台实测')
expect(text).toContain('差异 +1.5 kg')
2026-10-20 21:33:46 +08:00
expect(text).toContain('差异 -0.200 m³')
2026-10-08 17:08:03 +08:00
expect(text).toContain('平台实测箱规')
2026-08-04 21:01:39 +08:00
expect(text).not.toContain('租户')
2026-08-14 19:59:32 +08:00
expect(text).not.toContain('数据版本')
2026-08-04 21:01:39 +08:00
2026-08-13 17:24:43 +08:00
expect(wrapper.find('.measurement-table').exists()).toBe(true)
expect(wrapper.find('.measurement-item').exists()).toBe(false)
2026-08-14 19:59:32 +08:00
expect(wrapper.find('.measurement-table').text()).not.toContain('周长')
2026-08-18 16:09:43 +08:00
expect(wrapper.text()).not.toContain('每箱毛重')
expect(wrapper.text()).not.toContain('总重量(kg)')
2026-08-12 16:31:46 +08:00
const source = await import('./InboundMeasurementPanel.vue?raw').then((module) => module.default)
2026-08-17 18:32:41 +08:00
expect(source).toContain('<section class="measurement-declaration"')
expect(source).not.toContain('<el-collapse-item name="declaration"')
2026-08-14 19:59:32 +08:00
expect(source).toContain('grid-template-columns: repeat(4, minmax(0, 1fr))')
2026-10-08 17:08:03 +08:00
expect(source).toContain('.measurement-data-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));')
2026-09-14 17:54:21 +08:00
expect(source).toContain('.measurement-table { width: 100%; font-size: var(--admin-font-caption, 12px); }')
2026-08-13 17:43:04 +08:00
expect(source).toContain('.measurement-data-grid :deep(.el-input__wrapper)')
2026-09-11 16:17:21 +08:00
expect(source).toContain('defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlockReason, mayMaintain, maySubmit, saving, submitting, canEdit, canSubmit, isDirty })')
2026-08-18 11:33:01 +08:00
})
2026-10-08 17:08:03 +08:00
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([
2026-10-20 21:33:46 +08:00
['10', '10', '一致'], ['118.5', '120', '差异 +1.5 kg'], ['0.800', '0.600', '差异 -0.200 m³'],
2026-10-08 17:08:03 +08:00
])
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('申报与实测对比')
2026-10-20 21:33:46 +08:00
expect(wrapper.find('.measurement-declaration-header').text()).not.toContain('DOC-001')
expect(wrapper.find('.measurement-section__heading .measurement-goods-name').exists()).toBe(false)
2026-10-08 17:08:03 +08:00
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('请先完成箱规测量')
})
2026-08-18 11:33:01 +08:00
it('renders customer declaration facts as plain read-only values', async () => {
const wrapper = await mountPanel()
const declaration = wrapper.find('.measurement-declaration')
2026-10-08 17:08:03 +08:00
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')
2026-10-20 21:33:46 +08:00
expect(declaration.text()).not.toContain('原单据号码')
expect(declaration.text()).not.toContain('DOC-001')
expect(declaration.findAll('.measurement-readonly-field')).toHaveLength(0)
2026-10-08 17:08:03 +08:00
expect(declaration.findAll('input')).toHaveLength(0)
2026-10-20 21:33:46 +08:00
expect(wrapper.find('.measurement-items-section .measurement-goods-name').exists()).toBe(false)
2026-09-11 16:17:21 +08:00
expect(declaration.find('[aria-label="客户申报箱规明细"]').text()).toContain('蓝牙音箱')
expect(declaration.find('[aria-label="客户申报箱规明细"]').text()).toContain('50 × 40 × 30')
2026-08-18 11:33:01 +08:00
})
2026-08-18 16:09:43 +08:00
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()
2026-10-08 17:08:03 +08:00
expect(wrapper.findAll('.measurement-comparison tbody tr').slice(1).map(row => row.findAll('td')[2].text())).toEqual(['—', '—'])
2026-08-18 16:09:43 +08:00
})
2026-08-18 11:33:01 +08:00
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('蓝牙音箱')
2026-10-20 21:33:46 +08:00
expect(declaration.findAll('.measurement-readonly-field')).toHaveLength(0)
expect(wrapper.find('.measurement-items-section .measurement-goods-name').exists()).toBe(false)
2026-08-18 11:33:01 +08:00
})
2026-10-20 21:33:46 +08:00
it('preserves the hidden actual goods name without overriding declaration facts', async () => {
2026-08-18 11:33:01 +08:00
const wrapper = await mountPanel()
2026-10-20 21:33:46 +08:00
expect(wrapper.text()).not.toContain('实际品名')
await wrapper.find('input[aria-label="仓库修改外箱长"]').setValue('55')
2026-08-18 11:33:01 +08:00
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,
2026-10-20 21:33:46 +08:00
actualGoodsName: '蓝牙音箱',
2026-08-18 11:33:01 +08:00
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')
2026-08-14 14:50:44 +08:00
})
2026-09-03 12:00:07 +08:00
it('allows saving one warehouse outer dimension against the measured sequence', async () => {
2026-09-03 10:46:07 +08:00
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({
2026-09-03 12:00:07 +08:00
itemNo: '001', outerLengthCm: 55, outerWidthCm: null, outerHeightCm: null,
2026-09-03 10:46:07 +08:00
})
})
2026-08-14 14:50:44 +08:00
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')
2026-09-11 16:17:21 +08:00
wrapper.vm.updateWeightEvidence(125.75, 115.75, true)
2026-08-14 14:50:44 +08:00
await flushPromises()
2026-10-08 17:08:03 +08:00
expect(wrapper.find('.measurement-summary-section').text()).toContain('125.75')
expect(wrapper.find('.measurement-summary-section').text()).toContain('115.75')
2026-08-14 14:50:44 +08:00
expect((countInput.element as HTMLInputElement).value).toBe('9')
2026-08-04 21:01:39 +08:00
})
2026-09-11 16:17:21 +08:00
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')
})
2026-08-04 21:01:39 +08:00
it('reuses the established first-leg detail surface and status patterns', async () => {
const wrapper = await mountPanel()
expect(wrapper.classes()).toContain('surface-panel')
2026-08-14 19:59:32 +08:00
expect(wrapper.find('.measurement-summary-header').exists()).toBe(true)
expect(wrapper.find('.measurement-summary-status').text()).toContain('测量中')
2026-08-18 11:33:01 +08:00
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('美西海运')
2026-08-04 21:01:39 +08:00
})
2026-08-07 17:14:42 +08:00
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,
})
2026-08-04 21:01:39 +08:00
const wrapper = await mountPanel()
2026-08-13 17:24:43 +08:00
const countInput = wrapper.find('.measurement-count-editor input')
2026-08-07 17:14:42 +08:00
await countInput?.setValue('9')
2026-08-13 19:00:37 +08:00
const save = wrapper.findAll('button').find((button) => button.text().includes('保存测量数据'))
2026-08-04 21:01:39 +08:00
await save?.trigger('click')
await flushPromises()
2026-08-07 17:14:42 +08:00
expect(updateConfirmedPackageCount).toHaveBeenCalledWith('10001', '7001', '9101', {
measurementVersion: 2,
confirmedPackageCount: 9,
})
2026-08-04 21:01:39 +08:00
})
2026-09-10 10:22:48 +08:00
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()
})
2026-08-04 21:01:39 +08:00
it('preserves the edited form when the backend reports a version conflict', async () => {
2026-08-07 17:14:42 +08:00
vi.mocked(updateConfirmedPackageCount).mockRejectedValueOnce(
2026-09-10 10:22:48 +08:00
new ApiResponseError('测量数据已被其他人修改,请刷新后重试', 'A0409', 200, null, 'req-conflict', 'trace-conflict'),
2026-08-04 21:01:39 +08:00
)
const wrapper = await mountPanel()
2026-08-13 17:24:43 +08:00
const countInput = wrapper.find('.measurement-count-editor input')
2026-08-07 17:14:42 +08:00
await countInput?.setValue('9')
2026-08-13 19:00:37 +08:00
const save = wrapper.findAll('button').find((button) => button.text().includes('保存测量数据'))
2026-08-04 21:01:39 +08:00
await save?.trigger('click')
await flushPromises()
expect(wrapper.text()).toContain('数据版本已变化,当前修改尚未丢失')
expect(wrapper.text()).toContain('req-conflict')
2026-08-07 17:14:42 +08:00
expect((countInput?.element as HTMLInputElement).value).toBe('9')
2026-08-04 21:01:39 +08:00
expect(getInboundMeasurement).toHaveBeenCalledTimes(1)
})
2026-08-14 17:38:06 +08:00
it('shows a normal A0409 business reason instead of relabeling it as a version conflict', async () => {
2026-08-18 09:09:34 +08:00
const messageSpy = vi.spyOn(ElMessage, 'error')
2026-08-14 17:38:06 +08:00
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()
2026-08-18 09:09:34 +08:00
expect(messageSpy).toHaveBeenCalledWith(expect.objectContaining({
message: '该托盘已被占用,请重新选择',
showClose: true,
duration: 4500,
}))
expect(wrapper.text()).not.toContain('操作未完成')
2026-08-14 17:38:06 +08:00
expect(wrapper.text()).not.toContain('数据版本已变化,当前修改尚未丢失')
})
2026-08-18 09:09:34 +08:00
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('请修正测量草稿中的问题')
})
2026-08-04 21:01:39 +08:00
it('requires the dirty draft to be saved before submitting for review', async () => {
const wrapper = await mountPanel()
2026-08-13 17:24:43 +08:00
const countInput = wrapper.find('.measurement-count-editor input')
2026-08-07 17:14:42 +08:00
await countInput?.setValue('9')
2026-08-04 21:01:39 +08:00
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(
2026-09-18 11:41:27 +08:00
expect.stringContaining('提交成功后本批次不能继续编辑'),
2026-08-04 21:01:39 +08:00
'提交测量审核',
expect.objectContaining({ confirmButtonText: '确认提交' }),
)
expect(submitInboundMeasurement).toHaveBeenCalledWith('10001', '7001', { measurementId: '9001', version: 2 })
expect(wrapper.emitted('changed')).toHaveLength(1)
})
2026-10-08 17:08:03 +08:00
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)
2026-10-20 21:33:46 +08:00
expect(wrapper.find('.measurement-items-section .measurement-goods-name').exists()).toBe(false)
2026-10-08 17:08:03 +08:00
})
2026-09-10 17:13:08 +08:00
it('requires at least one confirmed weight image before submitting for review', async () => {
2026-09-11 16:17:21 +08:00
const messageSpy = vi.spyOn(ElMessage, 'warning')
2026-09-10 17:13:08 +08:00
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ weightResults: [] }))
const wrapper = await mountPanel()
2026-09-11 16:17:21 +08:00
expect(wrapper.text()).toContain('即使只有一箱,也必须上传并确认至少一张称重图片后才能提交审核')
2026-09-10 17:13:08 +08:00
const submit = wrapper.findAll('button').find((button) => button.text().includes('提交审核'))
expect(submit?.attributes('disabled')).toBeDefined()
2026-09-11 16:17:21 +08:00
await wrapper.vm.submitForReview()
expect(messageSpy).toHaveBeenCalledWith('即使只有一箱,也必须上传并确认至少一张称重图片后才能提交审核')
2026-09-10 17:13:08 +08:00
expect(submitInboundMeasurement).not.toHaveBeenCalled()
})
2026-09-11 16:17:21 +08:00
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)
})
2026-08-17 11:23:31 +08:00
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()
})
2026-08-18 16:09:43 +08:00
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')
})
2026-08-07 16:25:56 +08:00
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()
2026-08-13 19:00:37 +08:00
expect(wrapper.text()).not.toContain('确认箱数尚未与订单总箱数匹配')
2026-08-07 16:25:56 +08:00
const submit = wrapper.findAll('button').find((button) => button.text().includes('提交审核'))
expect(submit?.attributes('disabled')).toBeDefined()
expect(submitInboundMeasurement).not.toHaveBeenCalled()
})
2026-08-07 17:14:42 +08:00
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()
2026-08-13 17:24:43 +08:00
const countInput = wrapper.find('.measurement-count-editor input')
2026-08-07 17:14:42 +08:00
expect((countInput?.element as HTMLInputElement).matches(':disabled')).toBe(false)
2026-08-13 19:00:37 +08:00
expect(wrapper.findAll('button').find((button) => button.text().includes('保存测量数据'))?.attributes('disabled')).toBeUndefined()
2026-08-07 17:14:42 +08:00
expect(wrapper.findAll('button').find((button) => button.text().includes('提交审核'))?.attributes('disabled')).toBeDefined()
expect(vi.mocked(downloadInboundMeasurementImage)).not.toHaveBeenCalled()
})
2026-08-04 21:01:39 +08:00
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')
2026-08-14 19:59:32 +08:00
expect(wrapper.find('.measurement-summary-header').text()).toContain('第二企业')
expect(wrapper.find('.measurement-summary-header').text()).not.toContain('测试企业')
2026-08-04 21:01:39 +08:00
})
it('renders a permission-specific read-only state without requesting data', async () => {
const wrapper = await mountPanel([])
expect(wrapper.text()).toContain('无测量查看权限')
expect(getInboundMeasurement).not.toHaveBeenCalled()
})
})