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() 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 { 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() }) })