Files
admin-web/src/modules/inbounds/measurement/InboundMeasurementPanel.test.ts
T
2026-08-13 19:00:37 +08:00

329 lines
14 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, { 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,
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,
actualVolumeCbm: 0.6,
volumeWeightKg: 100.2,
actualGoodsName: '蓝牙音箱',
items: [{
id: '9101',
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',
measuredAt: '2026-08-04T10:30:00+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',
'platform:firstleg:inbound:measure:manual-entry',
'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())
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.find('input[readonly]').element as HTMLInputElement).value).toBe('RK202608040001')
expect(wrapper.text()).toContain('RK202608040001')
expect(wrapper.text()).toContain('周长(cm)')
expect(wrapper.find('.measurement-table').text()).toContain('190')
expect(wrapper.find('.measurement-table').text()).toContain('0.6')
await wrapper.find('.el-table__expand-icon').trigger('click')
await flushPromises()
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('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.findAll('input[readonly]').some((input) => (input.element as HTMLInputElement).value === '未知来源')).toBe(true)
expect(wrapper.text()).not.toContain('LEGACY_DEVICE')
})
it('follows the prototype grouping, labels, and editable field order', 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('实测总材积重')
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()).toContain('周长(cm)')
const source = await import('./InboundMeasurementPanel.vue?raw').then((module) => module.default)
expect(source).toContain('.measurement-form-grid, .measurement-data-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));')
expect(source).toContain('.measurement-table { width: 100%; font-size: 12px; }')
expect(source).toContain('.measurement-data-grid :deep(.el-input__wrapper)')
expect(source).toContain('defineExpose({ saveDraft, submitForReview, mayMaintain, maySubmit, saving, submitting, canEdit, canSubmit })')
})
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-panel__heading').exists()).toBe(true)
expect(wrapper.findAll('input[readonly]').some((input) => (input.element as HTMLInputElement).value === '测量中')).toBe(true)
})
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('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('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('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.findAll('input[readonly]').some((input) => (input.element as HTMLInputElement).value === 'RK202608040002')).toBe(true)
expect(wrapper.findAll('input[readonly]').some((input) => (input.element as HTMLInputElement).value === '测试企业')).toBe(false)
})
it('renders a permission-specific read-only state without requesting data', async () => {
const wrapper = await mountPanel([])
expect(wrapper.text()).toContain('无测量查看权限')
expect(getInboundMeasurement).not.toHaveBeenCalled()
})
})