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,
|
|
|
|
|
|
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: '蓝牙音箱',
|
|
|
|
|
|
items: [{
|
|
|
|
|
|
id: '9101',
|
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,
|
|
|
|
|
|
remark: '设备自动回传',
|
|
|
|
|
|
}],
|
|
|
|
|
|
weightResults: [],
|
|
|
|
|
|
...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',
|
|
|
|
|
|
]) {
|
|
|
|
|
|
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' },
|
|
|
|
|
|
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-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-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-08-14 19:59:32 +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')
|
|
|
|
|
|
expect(text).toContain('差异 -0.2 m³')
|
2026-08-14 19:59:32 +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-08-17 18:32:41 +08:00
|
|
|
|
expect(source).toContain('.measurement-data-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));')
|
2026-08-13 17:24:43 +08:00
|
|
|
|
expect(source).toContain('.measurement-table { width: 100%; font-size: 12px; }')
|
2026-08-13 17:43:04 +08:00
|
|
|
|
expect(source).toContain('.measurement-data-grid :deep(.el-input__wrapper)')
|
2026-08-18 11:33:01 +08:00
|
|
|
|
expect(source).toContain('defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, maySubmit, saving, submitting, canEdit, canSubmit, isDirty })')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('renders customer declaration facts as plain read-only values', async () => {
|
|
|
|
|
|
const wrapper = await mountPanel()
|
|
|
|
|
|
const declaration = wrapper.find('.measurement-declaration')
|
|
|
|
|
|
|
|
|
|
|
|
expect(declaration.text()).toContain('申报箱数')
|
|
|
|
|
|
expect(declaration.text()).toContain('10')
|
|
|
|
|
|
expect(declaration.text()).toContain('申报毛重(kg)')
|
|
|
|
|
|
expect(declaration.text()).toContain('118.5')
|
|
|
|
|
|
expect(declaration.text()).toContain('申报体积(m³)')
|
|
|
|
|
|
expect(declaration.text()).toContain('0.8')
|
|
|
|
|
|
expect(declaration.text()).toContain('原单据号码')
|
|
|
|
|
|
expect(declaration.text()).toContain('DOC-001')
|
|
|
|
|
|
expect(declaration.findAll('.measurement-readonly-field')).toHaveLength(4)
|
|
|
|
|
|
expect(declaration.findAll('input')).toHaveLength(1)
|
|
|
|
|
|
expect(declaration.find('.measurement-goods-name input').exists()).toBe(true)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
expect(wrapper.find('.measurement-metrics').text()).not.toContain('差异')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
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('蓝牙音箱')
|
|
|
|
|
|
expect(declaration.findAll('.measurement-readonly-field')).toHaveLength(5)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('saves the corrected actual goods name without overriding declaration facts', async () => {
|
|
|
|
|
|
const wrapper = await mountPanel()
|
|
|
|
|
|
await wrapper.find('.measurement-goods-name input').setValue('修正后的蓝牙音箱')
|
|
|
|
|
|
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')
|
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-08-18 11:33:01 +08:00
|
|
|
|
wrapper.vm.updateActualWeights(125.75, 115.75)
|
2026-08-14 14:50:44 +08:00
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
2026-08-14 19:59:32 +08:00
|
|
|
|
expect(wrapper.find('.measurement-metrics').text()).toContain('125.75')
|
2026-08-18 11:33:01 +08:00
|
|
|
|
expect(wrapper.find('.measurement-metrics').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
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
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-08-04 21:01:39 +08:00
|
|
|
|
new ApiResponseError('数据版本已变化,请刷新后重试', 'A0409', 200, null, 'req-conflict', 'trace-conflict'),
|
|
|
|
|
|
)
|
|
|
|
|
|
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(
|
|
|
|
|
|
expect.stringContaining('准备托盘与库位占用'),
|
|
|
|
|
|
'提交测量审核',
|
|
|
|
|
|
expect.objectContaining({ confirmButtonText: '确认提交' }),
|
|
|
|
|
|
)
|
|
|
|
|
|
expect(submitInboundMeasurement).toHaveBeenCalledWith('10001', '7001', { measurementId: '9001', version: 2 })
|
|
|
|
|
|
expect(wrapper.emitted('changed')).toHaveLength(1)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|