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-09-01 11:10:22 +08:00
|
|
|
|
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'
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
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')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
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
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('renders the authoritative whole-inbound reservation summary and an empty reserved slot', async () => {
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
const guide = wrapper.get('.putaway-guide')
|
|
|
|
|
|
expect(guide.text()).toContain('整票预留库位')
|
|
|
|
|
|
expect(guide.text()).toContain('A01 / A02 / A03')
|
|
|
|
|
|
expect(guide.text()).toContain('预留槽位数3')
|
|
|
|
|
|
expect(guide.text()).toContain('实际托数2')
|
|
|
|
|
|
expect(guide.text()).toContain('预留空位数1')
|
|
|
|
|
|
expect(guide.text()).toContain('A03预留空位本票预留空位')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
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 12:18:39 +08:00
|
|
|
|
expect(wrapper.get('.putaway-guide__slots li').classes()).toContain('is-overflow')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
expect(wrapper.get('.putaway-guide').text()).toContain('预留空位数1')
|
|
|
|
|
|
expect(wrapper.get('.putaway-guide').text()).toContain('A01预留空位本票预留空位')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2026-09-01 11:10:22 +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-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-09-01 11:10:22 +08:00
|
|
|
|
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)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-09-01 11:10:22 +08:00
|
|
|
|
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()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-09-01 11:10:22 +08:00
|
|
|
|
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)
|
2026-09-01 11:10:22 +08:00
|
|
|
|
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()
|
2026-09-01 11:10:22 +08:00
|
|
|
|
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')
|
|
|
|
|
|
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
|
|
|
|
})
|