2026-08-07 18:44:55 +08:00
|
|
|
import ElementPlus from 'element-plus'
|
2026-08-07 20:07:37 +08:00
|
|
|
import { ElMessageBox } from 'element-plus'
|
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-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-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'
|
|
|
|
|
|
|
|
|
|
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-07 20:07:37 +08:00
|
|
|
uploadWeightImage: vi.fn(),
|
2026-08-07 18:44:55 +08:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const pendingWeight: WeightResultResource = {
|
|
|
|
|
id: '30001',
|
|
|
|
|
fileId: '40001',
|
|
|
|
|
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,
|
|
|
|
|
finalWeightKg: null,
|
|
|
|
|
recognitionStatus: 'PENDING_CONFIRM',
|
|
|
|
|
adjustmentReason: null,
|
|
|
|
|
adjustedBy: null,
|
|
|
|
|
adjustedAt: null,
|
|
|
|
|
confirmedBy: null,
|
|
|
|
|
confirmedAt: null,
|
|
|
|
|
version: 0,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const workspace: WeightWorkspaceResource = {
|
|
|
|
|
inboundId: '20001',
|
|
|
|
|
inboundCode: 'RK-20001',
|
|
|
|
|
processStatus: 'MEASURING',
|
|
|
|
|
actualGrossWeightKg: null,
|
|
|
|
|
weightResults: [pendingWeight],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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',
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
function mountPanel(permissions = allWeightPermissions) {
|
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,
|
|
|
|
|
props: { customerId: '10001', inboundId: '20001' },
|
|
|
|
|
global: { plugins: [pinia, ElementPlus] },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-08 11:05:45 +08:00
|
|
|
async function openWeightDialog(wrapper: VueWrapper, reviewEvidence = true) {
|
2026-08-07 20:07:37 +08:00
|
|
|
await wrapper.get('button[aria-label="查看称重图片 1"]').trigger('click')
|
|
|
|
|
await flushPromises()
|
2026-08-08 11:05:45 +08:00
|
|
|
const evidence = document.body.querySelector<HTMLInputElement>('.ocr-review-check input[type="checkbox"]')
|
|
|
|
|
if (reviewEvidence && evidence) await new DOMWrapper(evidence).setValue(true)
|
2026-08-07 20:07:37 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dialogConfirmButton() {
|
|
|
|
|
const button = [...document.body.querySelectorAll<HTMLButtonElement>('.el-dialog__footer button')]
|
|
|
|
|
.find((candidate) => candidate.textContent?.trim() === '确认重量')
|
|
|
|
|
if (!button) throw new Error('weight confirmation button is missing')
|
|
|
|
|
return new DOMWrapper(button)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-08 11:05:45 +08:00
|
|
|
function dialogEvidenceCheckbox() {
|
|
|
|
|
const input = document.body.querySelector<HTMLInputElement>('.ocr-review-check input[type="checkbox"]')
|
|
|
|
|
if (!input) throw new Error('OCR evidence checkbox is missing')
|
|
|
|
|
return new DOMWrapper(input)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-07 18:44:55 +08:00
|
|
|
describe('WeightOcrPanel confirmation behavior', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks()
|
|
|
|
|
document.body.innerHTML = ''
|
|
|
|
|
vi.mocked(getWeightWorkspace).mockResolvedValue(workspace)
|
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',
|
|
|
|
|
finalWeightKg: 285.5,
|
|
|
|
|
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()
|
|
|
|
|
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)
|
|
|
|
|
expect(document.body.textContent).toContain('确认称重图片')
|
|
|
|
|
expect(document.body.textContent).toContain('识别重量 12.500 KG')
|
|
|
|
|
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')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('deletes a pending image with its optimistic-lock version', async () => {
|
|
|
|
|
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
|
|
|
|
vi.mocked(deleteWeightResult).mockResolvedValue(undefined)
|
|
|
|
|
const wrapper = mountPanel()
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
await wrapper.get('button[aria-label="删除称重图片 1"]').trigger('click')
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
expect(deleteWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', 0)
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-08 11:05:45 +08:00
|
|
|
it('requires image review before confirming even a high-confidence OCR value', async () => {
|
2026-08-07 20:07:37 +08:00
|
|
|
const wrapper = mountPanel()
|
|
|
|
|
await flushPromises()
|
2026-08-08 11:05:45 +08:00
|
|
|
await openWeightDialog(wrapper, false)
|
2026-08-07 20:07:37 +08:00
|
|
|
|
2026-08-08 11:05:45 +08:00
|
|
|
expect(document.body.textContent).toContain('OCR 原文')
|
|
|
|
|
expect(document.body.textContent).toContain('285.5')
|
|
|
|
|
expect(document.body.textContent).toContain('98%')
|
|
|
|
|
expect(document.body.textContent).toContain('OCR 可能误读数字或小数点')
|
|
|
|
|
expect(dialogConfirmButton().attributes('disabled')).toBeDefined()
|
|
|
|
|
await dialogEvidenceCheckbox().setValue(true)
|
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-08 11:05:45 +08:00
|
|
|
it('requires explicit image review before confirming a low-confidence OCR value', async () => {
|
|
|
|
|
vi.mocked(getWeightWorkspace).mockResolvedValue({
|
|
|
|
|
...workspace,
|
|
|
|
|
weightResults: [{ ...pendingWeight, ocrRawText: '5360', ocrWeightValue: 5360, confidence: 0.51863 }],
|
|
|
|
|
})
|
|
|
|
|
const wrapper = mountPanel()
|
|
|
|
|
await flushPromises()
|
|
|
|
|
await openWeightDialog(wrapper, false)
|
|
|
|
|
|
|
|
|
|
expect(document.body.textContent).toContain('OCR 可能误读数字或小数点')
|
|
|
|
|
expect(document.body.textContent).toContain('OCR 原文')
|
|
|
|
|
expect(document.body.textContent).toContain('5360')
|
|
|
|
|
expect(document.body.textContent).toContain('52%')
|
|
|
|
|
expect(dialogConfirmButton().attributes('disabled')).toBeDefined()
|
|
|
|
|
|
|
|
|
|
await dialogEvidenceCheckbox().setValue(true)
|
|
|
|
|
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')
|
|
|
|
|
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-07 20:07:37 +08:00
|
|
|
await dialogWeightInput().setValue('10.125')
|
|
|
|
|
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,
|
|
|
|
|
adjustedWeightValue: 10.125,
|
|
|
|
|
adjustedWeightUnit: 'LB',
|
|
|
|
|
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 10.125 LB',
|
|
|
|
|
})
|
|
|
|
|
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()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('accepts the smallest LB value that remains representable in 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.002')
|
|
|
|
|
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,
|
|
|
|
|
adjustedWeightValue: 0.002,
|
|
|
|
|
adjustedWeightUnit: 'LB',
|
|
|
|
|
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 0.002 LB',
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
await input.setValue('1.2345')
|
|
|
|
|
expect((input.element as HTMLInputElement).value).toBe('1.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,
|
|
|
|
|
adjustedWeightValue: 1.235,
|
|
|
|
|
adjustedWeightUnit: 'KG',
|
|
|
|
|
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 1.235 KG',
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
await dialogWeightInput().setValue('1.500')
|
2026-08-08 11:05:45 +08:00
|
|
|
await dialogEvidenceCheckbox().setValue(true)
|
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: 1.5,
|
|
|
|
|
adjustedWeightUnit: 'KG',
|
|
|
|
|
adjustmentReason: 'OCR 原值 —,人工确认 1.500 KG',
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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-12 12:35:32 +08:00
|
|
|
vi.mocked(confirmWeightResult).mockRejectedValueOnce(new ApiResponseError('数据版本已变化,请刷新后重试', 'A0409', 409))
|
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-12 12:35:32 +08:00
|
|
|
expect(wrapper.text()).toContain('数据版本已变化,请刷新后重试')
|
2026-08-07 19:23:43 +08:00
|
|
|
expect(confirmWeightResult).toHaveBeenCalledTimes(1)
|
|
|
|
|
})
|
2026-08-07 18:44:55 +08:00
|
|
|
})
|