Files
admin-web/src/modules/inbounds/weight/components/WeightOcrPanel.behavior.test.ts
T

1062 lines
41 KiB
TypeScript
Raw Normal View History

2026-08-07 18:44:55 +08:00
import ElementPlus from 'element-plus'
2026-08-14 19:59:32 +08:00
import { ElMessage, ElMessageBox } from 'element-plus'
import { AxiosError } from 'axios'
2026-08-07 18:44:55 +08:00
import { createPinia, setActivePinia } from 'pinia'
2026-08-07 20:07:37 +08:00
import { DOMWrapper, enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils'
2026-08-07 18:44:55 +08:00
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
2026-08-12 12:35:32 +08:00
import { ApiResponseError } from '@/shared/api/http'
2026-08-14 19:59:32 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
2026-08-07 18:44:55 +08:00
import {
confirmWeightResult,
correctWeightResult,
2026-08-07 20:07:37 +08:00
createWeightResult,
deleteWeightResult,
downloadWeightImage,
downloadWeightThumbnail,
2026-08-07 18:44:55 +08:00
getWeightWorkspace,
2026-08-14 17:38:06 +08:00
getWeightPalletOptions,
2026-08-07 20:07:37 +08:00
uploadWeightImage,
2026-08-07 18:44:55 +08:00
} from '../api/weightOcr'
import type { WeightResultResource, WeightWorkspaceResource } from '../types'
import WeightOcrPanel from './WeightOcrPanel.vue'
import InboundMeasurementDialog from '../../measurement/InboundMeasurementDialog.vue'
2026-08-07 18:44:55 +08:00
vi.mock('../api/weightOcr', async () => {
const actual = await vi.importActual<typeof import('../api/weightOcr')>('../api/weightOcr')
return {
...actual,
confirmWeightResult: vi.fn(),
correctWeightResult: vi.fn(),
2026-08-07 20:07:37 +08:00
createWeightResult: vi.fn(),
deleteWeightResult: vi.fn(),
downloadWeightImage: vi.fn(),
downloadWeightThumbnail: vi.fn(),
2026-08-07 18:44:55 +08:00
getWeightWorkspace: vi.fn(),
2026-08-14 17:38:06 +08:00
getWeightPalletOptions: vi.fn(),
2026-08-07 20:07:37 +08:00
uploadWeightImage: vi.fn(),
2026-08-07 18:44:55 +08:00
}
})
const pendingWeight: WeightResultResource = {
id: '30001',
fileId: '40001',
2026-08-14 17:38:06 +08:00
palletId: '50001',
palletCode: 'TP-001',
palletType: '木托盘',
palletCapacityCbm: 1.2,
palletTareWeightKg: 12,
2026-08-07 18:44:55 +08:00
ocrProvider: 'FILE_WEIGHT_OCR',
ocrRawText: '285.5',
ocrWeightValue: 285.5,
ocrWeightUnit: 'KG',
2026-08-08 11:05:45 +08:00
confidence: 0.98,
2026-08-07 18:44:55 +08:00
adjustedWeightValue: null,
adjustedWeightUnit: null,
2026-08-14 17:38:06 +08:00
grossWeightKg: 285.5,
netWeightKg: null,
2026-08-07 18:44:55 +08:00
recognitionStatus: 'PENDING_CONFIRM',
adjustmentReason: null,
adjustedBy: null,
adjustedAt: null,
confirmedBy: null,
confirmedAt: null,
2026-09-04 12:18:39 +08:00
palletSequence: 1,
blockId: '70001',
slotNo: 1,
reservationId: '71001',
locationId: '72001',
locationCode: 'A01',
sharedOverflow: false,
slotStatus: 'BOUND',
2026-08-07 18:44:55 +08:00
version: 0,
}
const workspace: WeightWorkspaceResource = {
inboundId: '20001',
inboundCode: 'RK-20001',
processStatus: 'MEASURING',
actualGrossWeightKg: null,
2026-08-18 11:33:01 +08:00
actualNetWeightKg: null,
2026-08-07 18:44:55 +08:00
weightResults: [pendingWeight],
2026-09-04 12:18:39 +08:00
palletLocationGuide: {
blockId: '70001',
reservedSlotCount: 1,
actualPalletCount: 1,
reservedEmptySlotCount: 0,
slots: [{
slotNo: 1,
reservationId: '71001',
locationId: '72001',
locationCode: 'A01',
sharedOverflow: false,
slotStatus: 'BOUND',
boundWeightResultId: '30001',
palletSequence: 1,
palletId: '50001',
palletCode: 'TP-001',
}],
},
2026-08-07 18:44:55 +08:00
}
const confirmedReviewWorkspace: WeightWorkspaceResource = {
...workspace,
processStatus: 'MEASURE_PENDING',
actualGrossWeightKg: 120,
actualNetWeightKg: 108,
weightResults: [{
...pendingWeight, recognitionStatus: 'CONFIRMED', adjustedWeightValue: 120,
adjustedWeightUnit: 'KG', grossWeightKg: 120, netWeightKg: 108, version: 1,
}],
}
2026-08-27 13:50:41 +08:00
function palletPage(records = [{
palletId: '50001', palletCode: 'TP-001', palletType: '木托盘', capacityCbm: 1.2, tareWeightKg: 12,
}]) {
return { records, page: 1, size: 20, total: records.length, pages: records.length ? 1 : 0 }
}
2026-08-07 18:44:55 +08:00
enableAutoUnmount(afterEach)
2026-08-07 19:23:43 +08:00
const allWeightPermissions = [
'platform:firstleg:inbound:measure:view',
'platform:firstleg:inbound:weight:edit',
'platform:firstleg:inbound:weight:confirm',
]
2026-08-18 11:33:01 +08:00
function mountPanel(permissions = allWeightPermissions, props: Record<string, unknown> = {}) {
2026-08-07 18:44:55 +08:00
const pinia = createPinia()
setActivePinia(pinia)
const auth = useAuthStore()
auth.token = 'token'
2026-08-07 19:23:43 +08:00
auth.permissions = permissions
2026-08-07 18:44:55 +08:00
useAccessPreviewStore().initializeAfterLogin()
return mount(WeightOcrPanel, {
attachTo: document.body,
2026-08-18 11:33:01 +08:00
props: { customerId: '10001', inboundId: '20001', ...props },
2026-08-07 18:44:55 +08:00
global: { plugins: [pinia, ElementPlus] },
})
}
2026-08-14 19:59:32 +08:00
async function openWeightDialog(wrapper: VueWrapper) {
2026-08-07 20:07:37 +08:00
await wrapper.get('button[aria-label="查看称重图片 1"]').trigger('click')
await flushPromises()
}
function dialogWeightInput() {
const input = document.body.querySelector<HTMLInputElement>('input[aria-label="当前图片确认重量"]')
if (!input) throw new Error('weight confirmation input is missing')
return new DOMWrapper(input)
}
function dialogUnit(index: number) {
const option = document.body.querySelectorAll<HTMLElement>('.el-dialog .el-segmented__item').item(index)
return new DOMWrapper(option)
}
2026-08-18 11:33:01 +08:00
function dialogConfirmButton(text = '确认重量') {
2026-08-07 20:07:37 +08:00
const button = [...document.body.querySelectorAll<HTMLButtonElement>('.el-dialog__footer button')]
2026-08-18 11:33:01 +08:00
.find((candidate) => candidate.textContent?.trim() === text)
2026-08-07 20:07:37 +08:00
if (!button) throw new Error('weight confirmation button is missing')
return new DOMWrapper(button)
}
2026-08-07 18:44:55 +08:00
describe('WeightOcrPanel confirmation behavior', () => {
it('keeps the weight confirmation outside its measurement parent with independent fullscreen and cancel state', async () => {
const pinia = createPinia()
setActivePinia(pinia)
const auth = useAuthStore()
auth.token = 'token'
auth.permissions = allWeightPermissions
useAccessPreviewStore().initializeAfterLogin()
const wrapper = mount(InboundMeasurementDialog, {
attachTo: document.body,
props: { modelValue: true, customerId: '10001', inboundId: '20001', inboundCode: 'RK-20001' },
global: {
plugins: [pinia, ElementPlus],
stubs: { InboundMeasurementPanel: { template: '<section class="measurement-panel" />' } },
},
})
await flushPromises()
const panel = wrapper.getComponent(WeightOcrPanel)
await openWeightDialog(panel)
const popup = panel.getComponent(AppDialog)
const parent = document.querySelector('.inbound-measurement-workspace-dialog')!
const child = document.querySelector('input[aria-label="当前图片确认重量"]')!.closest('.el-dialog')!
expect(document.body.contains(child)).toBe(true)
expect(parent.contains(child)).toBe(false)
expect(child.classList.contains('admin-workbench')).toBe(true)
await dialogWeightInput().setValue('286.125')
await new DOMWrapper(child).get('button[aria-label="展开全屏"]').trigger('click')
expect(child.classList.contains('is-fullscreen')).toBe(true)
expect(parent.classList.contains('is-fullscreen')).toBe(false)
expect(dialogWeightInput().element.value).toBe('286.125')
await new DOMWrapper(child).findAll('button').find(button => button.text() === '取消')!.trigger('click')
await flushPromises()
expect(popup.props('modelValue')).toBe(false)
expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(true)
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
await openWeightDialog(panel)
expect(popup.props('modelValue')).toBe(true)
expect(child.classList.contains('is-fullscreen')).toBe(false)
expect(confirmWeightResult).not.toHaveBeenCalled()
})
2026-08-07 18:44:55 +08:00
beforeEach(() => {
vi.clearAllMocks()
document.body.innerHTML = ''
vi.mocked(getWeightWorkspace).mockResolvedValue(workspace)
2026-08-27 13:50:41 +08:00
vi.mocked(getWeightPalletOptions).mockResolvedValue(palletPage())
2026-08-07 20:07:37 +08:00
vi.mocked(downloadWeightImage).mockResolvedValue(new Blob(['image'], { type: 'image/jpeg' }))
vi.mocked(downloadWeightThumbnail).mockResolvedValue(new Blob(['thumb'], { type: 'image/jpeg' }))
vi.stubGlobal('URL', {
createObjectURL: vi.fn(() => 'blob:test-image'),
revokeObjectURL: vi.fn(),
})
2026-08-07 18:44:55 +08:00
vi.mocked(confirmWeightResult).mockResolvedValue({
...pendingWeight,
recognitionStatus: 'CONFIRMED',
2026-08-14 17:38:06 +08:00
grossWeightKg: 297.5,
netWeightKg: 285.5,
2026-08-07 18:44:55 +08:00
version: 1,
})
})
2026-08-07 20:07:37 +08:00
it('opens the editable confirmation dialog immediately after upload', async () => {
vi.mocked(uploadWeightImage).mockResolvedValue({
fileId: '40002',
fileName: 'weight.jpg',
contentType: 'image/jpeg',
sizeBytes: 100,
recognitionStatus: 'SUCCESS',
weightValue: 12.5,
weightUnit: 'KG',
confidence: 0.98,
error: null,
})
vi.mocked(createWeightResult).mockResolvedValue({
...pendingWeight,
id: '30002',
fileId: '40002',
ocrWeightValue: 12.5,
})
const wrapper = mountPanel()
await flushPromises()
2026-08-14 17:38:06 +08:00
await wrapper.find('.pallet-picker .el-select').trigger('click')
2026-08-17 18:32:41 +08:00
expect(document.body.textContent).toContain('木托盘 · 皮重 12.000 KG · 容量 1.2 m³')
2026-08-14 17:38:06 +08:00
await new DOMWrapper(document.body).find('.el-select-dropdown__item').trigger('click')
2026-08-07 20:07:37 +08:00
const input = wrapper.get('input[type="file"]')
const file = new File(['image'], 'weight.jpg', { type: 'image/jpeg' })
Object.defineProperty(input.element, 'files', { configurable: true, value: [file] })
await input.trigger('change')
await flushPromises()
expect(uploadWeightImage).toHaveBeenCalledWith('10001', '20001', file)
2026-08-14 17:38:06 +08:00
expect(createWeightResult).toHaveBeenCalledWith('10001', '20001', expect.objectContaining({ palletId: '50001' }))
2026-08-07 20:07:37 +08:00
expect(document.body.textContent).toContain('确认称重图片')
2026-08-14 19:59:32 +08:00
expect(document.body.textContent).toContain('当前托盘皮重')
expect(document.body.textContent).toContain('OCR 识别毛重')
expect(document.body.textContent).toContain('12.500 KG')
2026-08-07 20:07:37 +08:00
expect(document.body.querySelector('input[aria-label="当前图片确认重量"]')).not.toBeNull()
})
it('renders thirty compact image cards without technical OCR details', async () => {
vi.mocked(getWeightWorkspace).mockResolvedValue({
...workspace,
weightResults: Array.from({ length: 30 }, (_, index) => ({
...pendingWeight,
id: String(30001 + index),
fileId: String(40001 + index),
ocrRawText: `raw-${index}`,
})),
})
2026-08-07 18:44:55 +08:00
const wrapper = mountPanel()
await flushPromises()
2026-08-07 20:07:37 +08:00
expect(wrapper.findAll('.weight-card')).toHaveLength(30)
expect(wrapper.findAll('.weight-thumb')).toHaveLength(30)
expect(wrapper.text()).not.toContain('置信度')
expect(wrapper.text()).not.toContain('OCR 原文')
expect(wrapper.text()).not.toContain('FILE_WEIGHT_OCR')
expect(wrapper.text()).not.toContain('raw-0')
})
2026-09-04 14:04:57 +08:00
it('renders the server-bound location and status once in the compact card facts', async () => {
2026-09-04 12:18:39 +08:00
const wrapper = mountPanel()
await flushPromises()
const card = wrapper.get('.weight-card')
2026-09-04 14:04:57 +08:00
expect(card.text()).toContain('库位A01 · 待上架')
2026-09-04 12:18:39 +08:00
expect(card.text()).toContain('待上架')
2026-09-04 14:04:57 +08:00
expect(card.find('.weight-card__putaway').exists()).toBe(false)
expect(card.get('.weight-card__facts').text()).toContain('库位A01 · 待上架')
2026-09-04 12:18:39 +08:00
})
2026-09-04 14:41:29 +08:00
it('keeps whole-inbound reservation guidance out of the panel and shows location on each weight card', async () => {
2026-09-04 12:18:39 +08:00
vi.mocked(getWeightWorkspace).mockResolvedValue({
...workspace,
palletLocationGuide: {
blockId: '70001',
reservedSlotCount: 3,
actualPalletCount: 2,
reservedEmptySlotCount: 1,
slots: [
{ slotNo: 1, reservationId: '71001', locationId: '72001', locationCode: 'A01', sharedOverflow: false, slotStatus: 'BOUND', boundWeightResultId: '30001', palletSequence: 1, palletId: '50001', palletCode: 'TP-001' },
{ slotNo: 2, reservationId: '71002', locationId: '72002', locationCode: 'A02', sharedOverflow: false, slotStatus: 'TRANSFERRED', boundWeightResultId: '30002', palletSequence: 2, palletId: '50002', palletCode: 'TP-002' },
{ slotNo: 3, reservationId: '71003', locationId: '72003', locationCode: 'A03', sharedOverflow: false, slotStatus: 'RESERVED_EMPTY', boundWeightResultId: null, palletSequence: null, palletId: null, palletCode: null },
],
},
})
const wrapper = mountPanel()
await flushPromises()
2026-09-04 14:41:29 +08:00
expect(wrapper.find('.putaway-guide').exists()).toBe(false)
expect(wrapper.text()).not.toContain('整票上架引导')
expect(wrapper.get('.weight-card__facts').text()).toContain('库位A01 · 待上架')
2026-09-04 12:18:39 +08:00
})
2026-09-04 14:04:57 +08:00
it('does not render a full-width placeholder when no whole-inbound guide exists', async () => {
2026-09-04 12:18:39 +08:00
vi.mocked(getWeightWorkspace).mockResolvedValue({ ...workspace, palletLocationGuide: null })
const wrapper = mountPanel()
await flushPromises()
2026-09-04 14:04:57 +08:00
expect(wrapper.text()).not.toContain('尚未分配整票预留库位')
expect(wrapper.find('.putaway-guide-empty').exists()).toBe(false)
2026-09-04 12:18:39 +08:00
})
it('marks A0 as a temporary overflow location from the server flag', async () => {
vi.mocked(getWeightWorkspace).mockResolvedValue({
...workspace,
weightResults: [{ ...pendingWeight, locationCode: 'A0', sharedOverflow: true }],
palletLocationGuide: {
...workspace.palletLocationGuide!,
slots: [{
...workspace.palletLocationGuide!.slots[0]!,
locationCode: 'A0',
sharedOverflow: true,
}],
},
})
const wrapper = mountPanel()
await flushPromises()
expect(wrapper.text()).toContain('A0 临时溢出位')
2026-09-04 14:04:57 +08:00
expect(wrapper.get('.weight-card__facts').text()).toContain('库位A0 · 待上架 · A0 临时溢出位')
2026-09-04 14:41:29 +08:00
expect(wrapper.find('.putaway-guide').exists()).toBe(false)
2026-09-04 12:18:39 +08:00
})
it.each([
['TRANSFERRED', '已上架'],
['RELEASED', '已释放'],
['RESERVED_EMPTY', '预留空位'],
] as const)('renders the authoritative %s slot status as %s', async (slotStatus, label) => {
vi.mocked(getWeightWorkspace).mockResolvedValue({
...workspace,
weightResults: [{ ...pendingWeight, slotStatus }],
})
const wrapper = mountPanel()
await flushPromises()
expect(wrapper.text()).toContain(label)
})
it('shows an explicit unassigned state when the server has not bound a slot', async () => {
vi.mocked(getWeightWorkspace).mockResolvedValue({
...workspace,
weightResults: [{
...pendingWeight,
palletSequence: null,
blockId: null,
slotNo: null,
reservationId: null,
locationId: null,
locationCode: null,
sharedOverflow: null,
slotStatus: null,
}],
})
const wrapper = mountPanel()
await flushPromises()
2026-09-04 14:04:57 +08:00
expect(wrapper.text()).not.toContain('托盘未分配')
expect(wrapper.get('.weight-card__facts').text()).toContain('库位未分配')
2026-09-04 12:18:39 +08:00
})
2026-08-07 20:07:37 +08:00
it('deletes a pending image with its optimistic-lock version', async () => {
2026-09-04 12:18:39 +08:00
const confirm = vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
2026-08-07 20:07:37 +08:00
vi.mocked(deleteWeightResult).mockResolvedValue(undefined)
2026-09-04 12:18:39 +08:00
vi.mocked(getWeightWorkspace)
.mockResolvedValueOnce(workspace)
.mockResolvedValueOnce({
...workspace,
weightResults: [],
palletLocationGuide: {
...workspace.palletLocationGuide!,
actualPalletCount: 0,
reservedEmptySlotCount: 1,
slots: [{
...workspace.palletLocationGuide!.slots[0]!,
slotStatus: 'RESERVED_EMPTY',
boundWeightResultId: null,
palletSequence: null,
palletId: null,
palletCode: null,
}],
},
})
2026-08-07 20:07:37 +08:00
const wrapper = mountPanel()
await flushPromises()
await wrapper.get('button[aria-label="删除称重图片 1"]').trigger('click')
await flushPromises()
expect(deleteWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', 0)
2026-09-04 12:18:39 +08:00
expect(confirm).toHaveBeenCalledWith(
expect.stringContaining('对应库位仍为本票保留,不会释放给其他订单'),
'删除称重图片',
expect.any(Object),
)
expect(getWeightWorkspace).toHaveBeenCalledTimes(2)
expect(wrapper.findAll('.weight-card')).toHaveLength(0)
2026-09-04 16:28:44 +08:00
expect(wrapper.text()).toContain('尚未上传称重图片')
2026-09-04 14:41:29 +08:00
expect(wrapper.find('.putaway-guide').exists()).toBe(false)
2026-09-04 12:18:39 +08:00
})
it('keeps the image and server workspace visible when deletion fails', async () => {
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
const errorMessage = vi.spyOn(ElMessage, 'error')
vi.mocked(deleteWeightResult).mockRejectedValueOnce(
new ApiResponseError('无权删除当前称重图片', 'A0403', 403),
)
const wrapper = mountPanel()
await flushPromises()
await wrapper.get('button[aria-label="删除称重图片 1"]').trigger('click')
await flushPromises()
expect(wrapper.findAll('.weight-card')).toHaveLength(1)
expect(wrapper.text()).toContain('A01')
expect(getWeightWorkspace).toHaveBeenCalledTimes(1)
expect(errorMessage).toHaveBeenCalledWith(expect.objectContaining({
message: '当前账号无权访问该数据,请联系管理员确认权限',
}))
})
it('shows a retryable workspace load failure without fabricating putaway guidance', async () => {
vi.mocked(getWeightWorkspace).mockRejectedValueOnce(
new ApiResponseError('称重工作区暂不可用', 'A0500', 503),
)
const wrapper = mountPanel()
await flushPromises()
expect(wrapper.text()).toContain('称重数据加载失败')
expect(wrapper.text()).toContain('称重工作区暂不可用')
expect(wrapper.find('.putaway-guide').exists()).toBe(false)
expect(wrapper.find('.putaway-guide-empty').exists()).toBe(false)
2026-08-07 20:07:37 +08:00
})
2026-08-14 19:59:32 +08:00
it('shows only the key weights and allows direct confirmation', async () => {
2026-08-07 20:07:37 +08:00
const wrapper = mountPanel()
await flushPromises()
2026-08-14 19:59:32 +08:00
await openWeightDialog(wrapper)
2026-08-07 20:07:37 +08:00
const dialogText = document.body.querySelector('.el-dialog')?.textContent || ''
2026-08-14 19:59:32 +08:00
expect(dialogText).toContain('当前托盘皮重')
expect(dialogText).toContain('12.000 KG')
expect(dialogText).toContain('OCR 识别毛重')
expect(dialogText).toContain('285.500 KG')
expect(dialogText).toContain('确认含托盘毛重')
2026-08-17 18:00:42 +08:00
expect(dialogText).toContain('托盘类型')
expect(dialogText).toContain('木托盘')
expect(dialogText).not.toContain('TP-001')
for (const hiddenLabel of ['托盘编码', '托盘容量', '识别状态', '置信度', '服务端毛重', '服务端净重', 'OCR 原文']) {
2026-08-14 19:59:32 +08:00
expect(dialogText).not.toContain(hiddenLabel)
}
expect(dialogConfirmButton().attributes('disabled')).toBeUndefined()
2026-08-07 20:07:37 +08:00
await dialogConfirmButton().trigger('click')
2026-08-07 18:44:55 +08:00
await flushPromises()
expect(confirmWeightResult).toHaveBeenCalledTimes(1)
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', { version: 0 })
expect(correctWeightResult).not.toHaveBeenCalled()
})
2026-08-14 14:50:44 +08:00
it('emits the latest server total after confirming a weight', async () => {
vi.mocked(getWeightWorkspace)
.mockResolvedValueOnce(workspace)
.mockResolvedValueOnce({
...workspace,
2026-08-18 11:33:01 +08:00
actualGrossWeightKg: 1787.5,
actualNetWeightKg: 1747.5,
2026-08-14 17:38:06 +08:00
weightResults: [{ ...pendingWeight, recognitionStatus: 'CONFIRMED', grossWeightKg: 297.5, netWeightKg: 285.5, version: 1 }],
2026-08-14 14:50:44 +08:00
})
const wrapper = mountPanel()
await flushPromises()
await openWeightDialog(wrapper)
await dialogConfirmButton().trigger('click')
await flushPromises()
2026-09-11 16:17:21 +08:00
expect(wrapper.emitted('summaryChanged')).toEqual([[1787.5, 1747.5, true]])
2026-08-14 14:50:44 +08:00
expect(wrapper.emitted('changed')).toHaveLength(1)
2026-08-18 11:33:01 +08:00
expect(wrapper.text()).toContain('实测毛重')
expect(wrapper.text()).toContain('货物重量')
expect(wrapper.text()).toContain('1787.500 KG')
expect(wrapper.text()).toContain('1747.500 KG')
})
it('opens the explicit table correction action and atomically confirms an approved weight during measurement review', async () => {
2026-08-18 11:33:01 +08:00
const confirmed = {
...pendingWeight,
recognitionStatus: 'CONFIRMED',
adjustedWeightValue: 120,
adjustedWeightUnit: 'KG',
grossWeightKg: 120,
netWeightKg: 108,
version: 1,
}
vi.mocked(getWeightWorkspace).mockResolvedValue({
...workspace,
processStatus: 'MEASURE_PENDING',
actualGrossWeightKg: 120,
actualNetWeightKg: 108,
weightResults: [confirmed],
})
vi.mocked(confirmWeightResult).mockResolvedValue({ ...confirmed, grossWeightKg: 125, netWeightKg: 113, version: 2 })
const wrapper = mountPanel(allWeightPermissions, { reviewMode: true, correctionMode: true, tableMode: true })
2026-08-18 11:33:01 +08:00
await flushPromises()
expect(wrapper.find('input[type="file"]').exists()).toBe(false)
expect(wrapper.text()).not.toContain('删除图片')
expect(wrapper.find('.weight-data-table').exists()).toBe(true)
expect(wrapper.find('.weight-grid').exists()).toBe(false)
expect(wrapper.get('.weight-data-table thead th').text()).toBe('操作')
expect(wrapper.find('.pallet-picker').exists()).toBe(false)
await wrapper.get('button[aria-label="修改重量 1"]').trigger('click')
await flushPromises()
2026-08-18 11:33:01 +08:00
await dialogWeightInput().setValue('125')
const reason = document.body.querySelector<HTMLTextAreaElement>('textarea[placeholder="请填写本次毛重修正原因"]')
expect(reason).not.toBeNull()
await new DOMWrapper(reason!).setValue('复核磅秤读数')
await dialogConfirmButton('保存重量修正').trigger('click')
await flushPromises()
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
version: 1,
adjustedWeightValue: 125,
adjustedWeightUnit: 'KG',
adjustmentReason: '复核磅秤读数',
})
expect(correctWeightResult).not.toHaveBeenCalled()
2026-08-14 14:50:44 +08:00
})
it.each([
{ permissions: allWeightPermissions.filter(permission => !permission.endsWith('weight:edit')), status: 'MEASURE_PENDING', correctionMode: true },
{ permissions: allWeightPermissions.filter(permission => !permission.endsWith('weight:confirm')), status: 'MEASURE_PENDING', correctionMode: true },
{ permissions: allWeightPermissions, status: 'WAIT_SPLIT', correctionMode: true },
{ permissions: allWeightPermissions, status: 'MEASURE_PENDING', correctionMode: false },
])('keeps table correction unavailable for missing permissions or inactive review: %j', async ({ permissions, status, correctionMode }) => {
vi.mocked(getWeightWorkspace).mockResolvedValue({ ...confirmedReviewWorkspace, processStatus: status })
const wrapper = mountPanel(permissions, { reviewMode: true, correctionMode, tableMode: true })
await flushPromises()
expect(wrapper.find('button[aria-label="修改重量 1"]').exists()).toBe(false)
await openWeightDialog(wrapper)
expect(dialogWeightInput().attributes('disabled')).toBeDefined()
expect(wrapper.getComponent(AppDialog).props('showConfirm')).toBe(false)
expect(confirmWeightResult).not.toHaveBeenCalled()
})
it('allows cancelling and reopening correction, and preserves the draft for retry after a save fails', async () => {
vi.mocked(getWeightWorkspace).mockResolvedValue(confirmedReviewWorkspace)
vi.mocked(confirmWeightResult)
.mockRejectedValueOnce(new Error('重量写入失败'))
.mockResolvedValueOnce({ ...confirmedReviewWorkspace.weightResults[0], grossWeightKg: 125, netWeightKg: 113, version: 2 })
const wrapper = mountPanel(allWeightPermissions, { reviewMode: true, correctionMode: true, tableMode: true })
await flushPromises()
const correctionButton = wrapper.get('button[aria-label="修改重量 1"]')
await correctionButton.trigger('click')
await flushPromises()
await dialogWeightInput().setValue('125')
await new DOMWrapper(document.body.querySelector<HTMLTextAreaElement>('#weight-correction-reason')!).setValue('复核磅秤读数')
const cancelButton = [...document.body.querySelectorAll<HTMLButtonElement>('.el-dialog__footer button')].find(button => button.textContent?.trim() === '取消')!
await new DOMWrapper(cancelButton).trigger('click')
await flushPromises()
expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(false)
expect(confirmWeightResult).not.toHaveBeenCalled()
await correctionButton.trigger('click')
await flushPromises()
expect(Number(dialogWeightInput().element.value)).toBe(125)
await dialogConfirmButton('保存重量修正').trigger('click')
await flushPromises()
expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(true)
expect(Number(dialogWeightInput().element.value)).toBe(125)
expect(dialogConfirmButton('保存重量修正').attributes('disabled')).toBeUndefined()
expect(wrapper.emitted('changed')).toBeUndefined()
await dialogConfirmButton('保存重量修正').trigger('click')
await flushPromises()
expect(confirmWeightResult).toHaveBeenCalledTimes(2)
expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(false)
expect(wrapper.emitted('changed')).toHaveLength(1)
})
2026-08-14 19:59:32 +08:00
it('closes the confirmation dialog immediately after a successful confirmation', async () => {
const wrapper = mountPanel()
await flushPromises()
await openWeightDialog(wrapper)
expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(true)
await dialogConfirmButton().trigger('click')
await flushPromises()
expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(false)
})
2026-08-14 17:38:06 +08:00
it('requires a pallet selection before opening the file picker', async () => {
const wrapper = mountPanel()
await flushPromises()
const click = vi.spyOn(wrapper.get('input[type="file"]').element as HTMLInputElement, 'click')
await wrapper.findAll('button').find((button) => button.text().includes('上传称重图片'))?.trigger('click')
expect(click).not.toHaveBeenCalled()
})
it('shows an actionable empty state when the warehouse has no pallet options', async () => {
2026-08-27 13:50:41 +08:00
vi.mocked(getWeightPalletOptions).mockResolvedValueOnce(palletPage([]))
2026-08-14 17:38:06 +08:00
const wrapper = mountPanel()
await flushPromises()
expect(wrapper.text()).toContain('当前仓库暂无可选托盘')
expect(wrapper.findAll('button').some((button) => button.text().includes('重新加载'))).toBe(true)
})
2026-08-27 13:50:41 +08:00
it('renders records from the formal paged response without showing a load failure', async () => {
vi.mocked(getWeightPalletOptions).mockResolvedValueOnce({
records: [
{ palletId: '2092803000000000001', palletCode: 'TP-11', palletType: '木托盘', capacityCbm: 1.2, tareWeightKg: 12 },
{ palletId: '2092803000000000002', palletCode: 'TP-12', palletType: '塑料托盘', capacityCbm: 1.1, tareWeightKg: 10 },
],
page: 1,
size: 20,
total: 2,
pages: 1,
})
const wrapper = mountPanel()
await flushPromises()
await wrapper.find('.pallet-picker .el-select').trigger('click')
await flushPromises()
expect(document.body.textContent).toContain('木托盘 · 皮重 12.000 KG · 容量 1.2 m³')
expect(document.body.textContent).toContain('塑料托盘 · 皮重 10.000 KG · 容量 1.1 m³')
expect(wrapper.text()).not.toContain('可选托盘加载失败')
})
it('remote search keyword uses page 1 and size 50', async () => {
const wrapper = mountPanel()
await flushPromises()
vi.useFakeTimers()
try {
await wrapper.find('.pallet-picker .el-select').trigger('click')
await wrapper.find('.pallet-picker input').setValue(' 木托盘 ')
await vi.advanceTimersByTimeAsync(350)
await flushPromises()
expect(getWeightPalletOptions).toHaveBeenLastCalledWith('10001', '20001', expect.objectContaining({
page: 1, size: 50, keyword: '木托盘', signal: expect.any(AbortSignal),
}))
} finally {
vi.useRealTimers()
}
})
2026-08-14 17:38:06 +08:00
it('shows pallet loading failures without hiding existing weight results', async () => {
vi.mocked(getWeightPalletOptions).mockRejectedValueOnce(new ApiResponseError('托盘服务暂不可用', 'A0500', 503))
const wrapper = mountPanel()
await flushPromises()
expect(wrapper.text()).toContain('托盘服务暂不可用')
expect(wrapper.findAll('.weight-card')).toHaveLength(1)
})
it('shows a network error as a pallet candidate load failure without hiding historical snapshots', async () => {
vi.mocked(getWeightPalletOptions).mockRejectedValueOnce(new AxiosError('Network Error', 'ERR_NETWORK'))
const wrapper = mountPanel()
await flushPromises()
expect(wrapper.text()).toContain('可选托盘加载失败')
expect(wrapper.findAll('.weight-card')).toHaveLength(1)
expect(wrapper.text()).toContain('木托盘')
expect(wrapper.text()).toContain('12.000 KG')
})
2026-08-14 17:38:06 +08:00
it('does not load weight or pallet data without measurement view permission', async () => {
const wrapper = mountPanel([])
await flushPromises()
expect(wrapper.text()).toContain('缺少入库测量查看权限')
expect(getWeightWorkspace).not.toHaveBeenCalled()
expect(getWeightPalletOptions).not.toHaveBeenCalled()
})
it('keeps historical palletCode snapshots visible for a read-only operator', async () => {
2026-08-27 11:08:03 +08:00
const wrapper = mountPanel(['platform:firstleg:inbound:measure:view'])
await flushPromises()
expect(getWeightWorkspace).toHaveBeenCalledOnce()
expect(getWeightPalletOptions).not.toHaveBeenCalled()
expect(wrapper.find('.pallet-picker .el-select').exists()).toBe(false)
expect(wrapper.text()).toContain('TP-001')
2026-08-27 11:08:03 +08:00
expect(wrapper.text()).toContain('木托盘')
expect(wrapper.text()).toContain('12.000 KG')
})
it('does not request pallet candidates when the inbound status is not editable', async () => {
vi.mocked(getWeightWorkspace).mockResolvedValue({ ...workspace, processStatus: 'MEASURE_PENDING' })
mountPanel()
await flushPromises()
expect(getWeightPalletOptions).not.toHaveBeenCalled()
})
it('virtualizes one thousand pallet candidates instead of rendering one thousand option nodes', async () => {
2026-08-27 13:50:41 +08:00
vi.mocked(getWeightPalletOptions).mockResolvedValue(palletPage(Array.from({ length: 1_000 }, (_, index) => ({
2026-08-27 11:08:03 +08:00
palletId: String(60_000 + index),
palletCode: `TP-${index}`,
palletType: `托盘类型 ${index}`,
capacityCbm: 1.2,
tareWeightKg: 12,
2026-08-27 13:50:41 +08:00
}))))
2026-08-27 11:08:03 +08:00
const wrapper = mountPanel()
await flushPromises()
await wrapper.find('.pallet-picker .el-select').trigger('click')
await flushPromises()
const renderedOptions = document.body.querySelectorAll('.el-select-dropdown__item')
expect(renderedOptions.length).toBeGreaterThan(0)
expect(renderedOptions.length).toBeLessThan(100)
})
it('aborts an in-flight pallet request and ignores its late result when the panel closes', async () => {
let resolveOptions: ((value: Awaited<ReturnType<typeof getWeightPalletOptions>>) => void) | undefined
vi.mocked(getWeightPalletOptions).mockImplementationOnce(() => new Promise((resolve) => {
resolveOptions = resolve
}))
const wrapper = mountPanel()
await flushPromises()
const signal = vi.mocked(getWeightPalletOptions).mock.calls[0]?.[2]?.signal
2026-08-27 11:08:03 +08:00
wrapper.unmount()
2026-08-27 13:50:41 +08:00
resolveOptions?.(palletPage([{
2026-08-27 11:08:03 +08:00
palletId: 'late', palletCode: 'TP-LATE', palletType: '过期托盘', capacityCbm: 1, tareWeightKg: 10,
2026-08-27 13:50:41 +08:00
}]))
2026-08-27 11:08:03 +08:00
await flushPromises()
expect(signal?.aborted).toBe(true)
expect(document.body.textContent).not.toContain('过期托盘')
})
2026-08-14 19:59:32 +08:00
it('keeps a low-confidence OCR result focused on the editable weight', async () => {
2026-08-08 11:05:45 +08:00
vi.mocked(getWeightWorkspace).mockResolvedValue({
...workspace,
weightResults: [{ ...pendingWeight, ocrRawText: '5360', ocrWeightValue: 5360, confidence: 0.51863 }],
})
const wrapper = mountPanel()
await flushPromises()
2026-08-14 19:59:32 +08:00
await openWeightDialog(wrapper)
2026-08-08 11:05:45 +08:00
2026-08-14 19:59:32 +08:00
const dialogText = document.body.textContent || ''
expect(dialogText).toContain('OCR 识别毛重')
expect(dialogText).toContain('5360.000 KG')
expect(dialogText).toContain('确认含托盘毛重')
expect(dialogText).not.toContain('置信度')
expect(dialogText).not.toContain('OCR 原文')
expect(dialogText).not.toContain('OCR 可能误读数字或小数点')
expect(dialogConfirmButton().attributes('disabled')).toBeUndefined()
2026-08-08 11:05:45 +08:00
await dialogConfirmButton().trigger('click')
await flushPromises()
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', { version: 0 })
})
2026-08-07 18:44:55 +08:00
it('sends the adjusted value and a specific audit reason in the same confirmation request', async () => {
const wrapper = mountPanel()
await flushPromises()
2026-08-07 20:07:37 +08:00
await openWeightDialog(wrapper)
2026-08-07 18:44:55 +08:00
2026-08-07 20:07:37 +08:00
await dialogWeightInput().setValue('286.125')
const expand = document.body.querySelector<HTMLButtonElement>('button[aria-label="展开全屏"]')
expect(expand).not.toBeNull()
await new DOMWrapper(expand!).trigger('click')
await flushPromises()
expect(document.body.querySelector('.el-dialog.is-fullscreen')).not.toBeNull()
expect(dialogWeightInput().element.value).toBe('286.125')
const collapse = document.body.querySelector<HTMLButtonElement>('button[aria-label="退出全屏"]')
await new DOMWrapper(collapse!).trigger('click')
await flushPromises()
expect(dialogWeightInput().element.value).toBe('286.125')
expect(confirmWeightResult).not.toHaveBeenCalled()
2026-08-07 20:07:37 +08:00
await dialogConfirmButton().trigger('click')
2026-08-07 18:44:55 +08:00
await flushPromises()
expect(confirmWeightResult).toHaveBeenCalledTimes(1)
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
version: 0,
adjustedWeightValue: 286.125,
adjustedWeightUnit: 'KG',
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 286.125 KG',
})
expect(correctWeightResult).not.toHaveBeenCalled()
})
2026-08-07 19:23:43 +08:00
it('sends an LB adjustment atomically after switching units', async () => {
const wrapper = mountPanel()
await flushPromises()
2026-08-07 20:07:37 +08:00
await openWeightDialog(wrapper)
2026-08-07 19:23:43 +08:00
2026-08-14 19:59:32 +08:00
await dialogWeightInput().setValue('30.125')
2026-08-07 20:07:37 +08:00
await dialogUnit(1).trigger('click')
await dialogConfirmButton().trigger('click')
2026-08-07 19:23:43 +08:00
await flushPromises()
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
version: 0,
2026-08-14 19:59:32 +08:00
adjustedWeightValue: 30.125,
2026-08-07 19:23:43 +08:00
adjustedWeightUnit: 'LB',
2026-08-14 19:59:32 +08:00
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 30.125 LB',
2026-08-07 19:23:43 +08:00
})
expect(correctWeightResult).not.toHaveBeenCalled()
})
it('rejects an LB value that would round to zero kilograms', async () => {
const wrapper = mountPanel()
await flushPromises()
2026-08-07 20:07:37 +08:00
await openWeightDialog(wrapper)
2026-08-07 19:23:43 +08:00
2026-08-07 20:07:37 +08:00
await dialogWeightInput().setValue('0.001')
await dialogUnit(1).trigger('click')
2026-08-07 19:23:43 +08:00
2026-08-07 20:07:37 +08:00
expect(dialogConfirmButton().attributes('disabled')).toBeDefined()
2026-08-07 19:23:43 +08:00
expect(confirmWeightResult).not.toHaveBeenCalled()
})
2026-08-14 19:59:32 +08:00
it('rejects a positive gross weight that does not exceed the pallet tare', async () => {
const warning = vi.spyOn(ElMessage, 'warning')
2026-08-07 19:23:43 +08:00
const wrapper = mountPanel()
await flushPromises()
2026-08-07 20:07:37 +08:00
await openWeightDialog(wrapper)
2026-08-07 19:23:43 +08:00
2026-08-07 20:07:37 +08:00
await dialogWeightInput().setValue('0.002')
await dialogUnit(1).trigger('click')
await dialogConfirmButton().trigger('click')
2026-08-07 19:23:43 +08:00
await flushPromises()
2026-08-14 19:59:32 +08:00
expect(confirmWeightResult).not.toHaveBeenCalled()
expect(warning).toHaveBeenCalledWith('称重毛重必须大于托盘皮重 12.000 KG')
2026-08-07 19:23:43 +08:00
})
it('clamps a value outside the safe three-decimal JavaScript range', async () => {
const wrapper = mountPanel()
await flushPromises()
2026-08-07 20:07:37 +08:00
await openWeightDialog(wrapper)
const input = dialogWeightInput()
2026-08-07 19:23:43 +08:00
await input.setValue('9000000000001')
expect((input.element as HTMLInputElement).value).toBe('9000000000000.000')
2026-08-07 20:07:37 +08:00
await dialogConfirmButton().trigger('click')
2026-08-07 19:23:43 +08:00
await flushPromises()
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
version: 0,
adjustedWeightValue: 9_000_000_000_000,
adjustedWeightUnit: 'KG',
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 9000000000000.000 KG',
})
})
it('rounds an input with more than three decimals before confirmation', async () => {
const wrapper = mountPanel()
await flushPromises()
2026-08-07 20:07:37 +08:00
await openWeightDialog(wrapper)
const input = dialogWeightInput()
2026-08-07 19:23:43 +08:00
2026-08-14 19:59:32 +08:00
await input.setValue('13.2345')
expect((input.element as HTMLInputElement).value).toBe('13.235')
2026-08-07 20:07:37 +08:00
await dialogConfirmButton().trigger('click')
2026-08-07 19:23:43 +08:00
await flushPromises()
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
version: 0,
2026-08-14 19:59:32 +08:00
adjustedWeightValue: 13.235,
2026-08-07 19:23:43 +08:00
adjustedWeightUnit: 'KG',
2026-08-14 19:59:32 +08:00
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 13.235 KG',
2026-08-07 19:23:43 +08:00
})
})
it('confirms an existing corrected value without creating another correction', async () => {
vi.mocked(getWeightWorkspace).mockResolvedValue({
...workspace,
weightResults: [{
...pendingWeight,
adjustedWeightValue: 286.125,
adjustedWeightUnit: 'KG',
adjustmentReason: '首次修正',
version: 1,
}],
})
const wrapper = mountPanel()
await flushPromises()
2026-08-07 20:07:37 +08:00
await openWeightDialog(wrapper)
2026-08-07 19:23:43 +08:00
2026-08-07 20:07:37 +08:00
await dialogConfirmButton().trigger('click')
2026-08-07 19:23:43 +08:00
await flushPromises()
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', { version: 1 })
expect(correctWeightResult).not.toHaveBeenCalled()
})
it('allows a confirm-only operator to accept an unchanged OCR value', async () => {
const wrapper = mountPanel([
'platform:firstleg:inbound:measure:view',
'platform:firstleg:inbound:weight:confirm',
])
await flushPromises()
2026-08-07 20:07:37 +08:00
await openWeightDialog(wrapper)
2026-08-07 19:23:43 +08:00
2026-08-07 20:07:37 +08:00
expect(dialogWeightInput().attributes('disabled')).toBeDefined()
await dialogConfirmButton().trigger('click')
2026-08-07 19:23:43 +08:00
await flushPromises()
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', { version: 0 })
})
it('does not render confirmation for an edit-only operator', async () => {
const wrapper = mountPanel([
'platform:firstleg:inbound:measure:view',
'platform:firstleg:inbound:weight:edit',
])
await flushPromises()
2026-08-07 20:07:37 +08:00
await openWeightDialog(wrapper)
2026-08-07 19:23:43 +08:00
2026-08-07 20:07:37 +08:00
const confirmButtons = [...document.body.querySelectorAll<HTMLButtonElement>('.el-dialog__footer button')]
.filter((button) => button.textContent?.trim() === '确认重量')
expect(confirmButtons).toHaveLength(0)
2026-08-07 19:23:43 +08:00
expect(confirmWeightResult).not.toHaveBeenCalled()
})
it('supports manual entry when OCR did not return a value', async () => {
vi.mocked(getWeightWorkspace).mockResolvedValue({
...workspace,
weightResults: [{
...pendingWeight,
ocrRawText: null,
ocrWeightValue: null,
ocrWeightUnit: null,
recognitionStatus: 'NEEDS_MANUAL',
}],
})
const wrapper = mountPanel()
await flushPromises()
2026-08-07 20:07:37 +08:00
await openWeightDialog(wrapper)
2026-08-07 19:23:43 +08:00
2026-08-07 20:07:37 +08:00
expect(dialogConfirmButton().attributes('disabled')).toBeDefined()
2026-08-14 19:59:32 +08:00
await dialogWeightInput().setValue('13.500')
2026-08-07 20:07:37 +08:00
await dialogConfirmButton().trigger('click')
2026-08-07 19:23:43 +08:00
await flushPromises()
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
version: 0,
2026-08-14 19:59:32 +08:00
adjustedWeightValue: 13.5,
2026-08-07 19:23:43 +08:00
adjustedWeightUnit: 'KG',
2026-08-14 19:59:32 +08:00
adjustmentReason: 'OCR 原值 —,人工确认 13.500 KG',
2026-08-07 19:23:43 +08:00
})
})
it('normalizes a supported OCR unit alias with surrounding whitespace', async () => {
vi.mocked(getWeightWorkspace).mockResolvedValue({
...workspace,
weightResults: [{ ...pendingWeight, ocrWeightUnit: ' lbs ' }],
})
const wrapper = mountPanel()
await flushPromises()
2026-08-07 20:07:37 +08:00
await openWeightDialog(wrapper)
2026-08-07 19:23:43 +08:00
2026-08-07 20:07:37 +08:00
await dialogConfirmButton().trigger('click')
2026-08-07 19:23:43 +08:00
await flushPromises()
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', { version: 0 })
})
it('sends an explicit correction when OCR returns an unsupported unit', async () => {
vi.mocked(getWeightWorkspace).mockResolvedValue({
...workspace,
weightResults: [{ ...pendingWeight, ocrWeightUnit: 'G' }],
})
const wrapper = mountPanel()
await flushPromises()
2026-08-07 20:07:37 +08:00
await openWeightDialog(wrapper)
2026-08-07 19:23:43 +08:00
2026-08-07 20:07:37 +08:00
await dialogConfirmButton().trigger('click')
2026-08-07 19:23:43 +08:00
await flushPromises()
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
version: 0,
adjustedWeightValue: 285.5,
adjustedWeightUnit: 'KG',
adjustmentReason: 'OCR 原值 285.500 G,人工确认 285.500 KG',
})
})
it('sends an explicit correction when OCR returns a value without a unit', async () => {
vi.mocked(getWeightWorkspace).mockResolvedValue({
...workspace,
weightResults: [{ ...pendingWeight, ocrWeightUnit: null }],
})
const wrapper = mountPanel()
await flushPromises()
2026-08-07 20:07:37 +08:00
await openWeightDialog(wrapper)
2026-08-07 19:23:43 +08:00
2026-08-07 20:07:37 +08:00
await dialogConfirmButton().trigger('click')
2026-08-07 19:23:43 +08:00
await flushPromises()
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
version: 0,
adjustedWeightValue: 285.5,
adjustedWeightUnit: 'KG',
adjustmentReason: 'OCR 原值 285.500(单位缺失),人工确认 285.500 KG',
})
})
it('keeps the edited draft available when confirmation fails', async () => {
2026-08-14 19:59:32 +08:00
const errorMessage = vi.spyOn(ElMessage, 'error')
vi.mocked(confirmWeightResult).mockRejectedValueOnce(
new ApiResponseError('数据版本已变化,请刷新后重试', 'A0409', 409, undefined, 'request-1'),
)
2026-08-07 19:23:43 +08:00
const wrapper = mountPanel()
await flushPromises()
2026-08-07 20:07:37 +08:00
await openWeightDialog(wrapper)
const input = dialogWeightInput()
2026-08-07 19:23:43 +08:00
await input.setValue('286.125')
2026-08-07 20:07:37 +08:00
await dialogConfirmButton().trigger('click')
2026-08-07 19:23:43 +08:00
await flushPromises()
expect((input.element as HTMLInputElement).value).toBe('286.125')
2026-08-14 19:59:32 +08:00
expect(wrapper.text()).not.toContain('操作未完成')
expect(errorMessage).toHaveBeenCalledWith(expect.objectContaining({
message: '数据版本已变化,请刷新后重试(请求 ID:request-1)',
duration: 5_000,
showClose: true,
}))
expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(true)
2026-08-07 19:23:43 +08:00
expect(confirmWeightResult).toHaveBeenCalledTimes(1)
})
2026-08-07 18:44:55 +08:00
})