Files
admin-web/src/modules/inbounds/measurement/InboundMeasurementPanel.test.ts
T
2026-08-04 21:01:39 +08:00

256 lines
9.6 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 {
getInboundMeasurement,
saveInboundMeasurementDraft,
submitInboundMeasurement,
} from './api'
import type { InboundMeasurementDetail } from './types'
import InboundMeasurementPanel from './InboundMeasurementPanel.vue'
vi.mock('./api', () => ({
getInboundMeasurement: vi.fn(),
saveInboundMeasurementDraft: vi.fn(),
submitInboundMeasurement: 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,
actualPackageCount: 10,
actualGrossWeightKg: 120,
actualVolumeCbm: 0.6,
volumeWeightKg: 100.2,
actualGoodsName: '蓝牙音箱',
items: [{
id: '9101',
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',
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: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(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.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('第三方设备')
expect(wrapper.text()).toContain('RK202608040001')
expect(wrapper.text()).toContain('箱规周长')
expect(wrapper.text()).toContain('190 cm')
expect(wrapper.text()).toContain('0.6 CBM')
expect((wrapper.find('input[placeholder="请输入本箱规的补充说明"]').element as HTMLInputElement).value).toBe('设备自动回传')
})
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).not.toContain('租户')
const labels = wrapper.findAll('.measurement-item__grid label > span').map((label) => label.text())
expect(labels.slice(0, 8)).toEqual([
'明细编号 *',
'箱规 *',
'箱规长(cm)*',
'箱规宽(cm)*',
'箱规高(cm)*',
'箱数 *',
'每箱毛重(kg)*',
'智能图片文件 ID',
])
})
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').classes()).toContain('panel-heading')
expect(wrapper.find('.status-badge').text()).toBe('测量中')
})
it('saves only editable raw fields with the current measurement version', async () => {
const wrapper = await mountPanel()
await wrapper.find('input[placeholder="请输入实际货物名称"]').setValue('智能音箱')
const save = wrapper.findAll('button').find((button) => button.text().includes('保存草稿'))
await save?.trigger('click')
await flushPromises()
expect(saveInboundMeasurementDraft).toHaveBeenCalledOnce()
expect(saveInboundMeasurementDraft).toHaveBeenCalledWith('10001', '7001', expect.objectContaining({
measurementId: '9001',
version: 2,
actualGoodsName: '智能音箱',
items: [expect.objectContaining({
itemNo: '001', packageCount: 10, lengthCm: 50, intelligentImageFileId: '88001',
})],
}))
const payload = vi.mocked(saveInboundMeasurementDraft).mock.calls[0][2]
expect(payload.items[0]).not.toHaveProperty('totalVolumeCbm')
})
it('preserves the edited form when the backend reports a version conflict', async () => {
vi.mocked(saveInboundMeasurementDraft).mockRejectedValueOnce(
new ApiResponseError('数据版本已变化,请刷新后重试', 'A0409', 200, null, 'req-conflict', 'trace-conflict'),
)
const wrapper = await mountPanel()
const goodsInput = wrapper.find('input[placeholder="请输入实际货物名称"]')
await goodsInput.setValue('保留我的修改')
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((goodsInput.element as HTMLInputElement).value).toBe('保留我的修改')
expect(getInboundMeasurement).toHaveBeenCalledTimes(1)
})
it('requires the dirty draft to be saved before submitting for review', async () => {
const wrapper = await mountPanel()
await wrapper.find('input[placeholder="请输入实际货物名称"]').setValue('尚未保存')
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('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.text()).toContain('RK202608040002')
expect(wrapper.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()
})
})