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

725 lines
26 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'
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'
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,
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-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', () => {
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')
})
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-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
2026-08-14 19:59:32 +08:00
const dialogText = document.body.textContent || ''
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-08-18 11:33:01 +08:00
expect(wrapper.emitted('summaryChanged')).toEqual([[1787.5, 1747.5]])
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('atomically corrects and confirms an approved weight during measurement review', async () => {
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 })
await flushPromises()
expect(wrapper.find('input[type="file"]').exists()).toBe(false)
expect(wrapper.text()).not.toContain('删除图片')
await wrapper.findAll('button').find((button) => button.text().includes('修改含托盘毛重'))?.trigger('click')
await flushPromises()
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
})
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('可选托盘加载失败')
})
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('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()
})
2026-08-27 11:08:03 +08:00
it('keeps historical pallet snapshots visible without requesting candidates for a read-only operator', async () => {
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('木托盘')
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]
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')
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
})