fix(overseas): synchronize receipt input accessibility with command lock

This commit is contained in:
2026-10-04 12:43:07 +08:00
parent a8ff0ef1de
commit 1d73a40a07
2 changed files with 38 additions and 4 deletions
@@ -1,5 +1,6 @@
import { flushPromises, mount } from '@vue/test-utils' import { flushPromises, mount } from '@vue/test-utils'
import { defineComponent, h } from 'vue' import { defineComponent, h } from 'vue'
import { ElForm, ElFormItem, ElInput, ElInputNumber } from 'element-plus'
import { beforeEach, expect, it, vi } from 'vitest' import { beforeEach, expect, it, vi } from 'vitest'
import { ApiResponseError } from '@/shared/api/http' import { ApiResponseError } from '@/shared/api/http'
import ExceptionProcessingPanel from './ExceptionProcessingPanel.vue' import ExceptionProcessingPanel from './ExceptionProcessingPanel.vue'
@@ -25,9 +26,10 @@ function snapshot(number = 'EX-1', version = 1, remaining = 4): ExceptionDetail
reason: '拒收', exceptionAt: '', updatedAt: '', evidenceFileIds: ['91'] }, reason: '拒收', exceptionAt: '', updatedAt: '', evidenceFileIds: ['91'] },
returnSources: remaining ? [{ outboundPalletItemId: '10', inventoryPositionId: '20', palletNo: 'ORIGINAL', skuCode: 'SKU', maxReturnPackages: String(remaining) }] : [], returnReceipts: [] } as unknown as ExceptionDetail) returnSources: remaining ? [{ outboundPalletItemId: '10', inventoryPositionId: '20', palletNo: 'ORIGINAL', skuCode: 'SKU', maxReturnPackages: String(remaining) }] : [], returnReceipts: [] } as unknown as ExceptionDetail)
} }
function open() { return mount(ExceptionProcessingPanel, { props: { customerId: 'C', exceptionNo: 'EX-1', active: true }, global: { function open(realControls = false) { return mount(ExceptionProcessingPanel, { props: { customerId: 'C', exceptionNo: 'EX-1', active: true }, global: {
directives: { loading: () => {} }, stubs: { ElDescriptions: shell, ElDescriptionsItem: shell, ElForm: shell, ElFormItem: shell, components: { ElForm, ElFormItem, ElInput, ElInputNumber },
ElInput: field, ElInputNumber: quantity, ElSelect: field, ElOption: true, RouterLink: { template: '<a><slot /></a>' }, ElButton: { template: '<button><slot /></button>' } }, directives: { loading: () => {} }, stubs: { ElDescriptions: shell, ElDescriptionsItem: shell, ElForm: realControls ? false : shell, ElFormItem: realControls ? false : shell,
ElInput: realControls ? false : field, ElInputNumber: realControls ? false : quantity, ElSelect: field, ElOption: true, RouterLink: { template: '<a><slot /></a>' }, ElButton: { template: '<button><slot /></button>' } },
} }) } } }) }
async function fill(wrapper: ReturnType<typeof open>, amount = 2) { async function fill(wrapper: ReturnType<typeof open>, amount = 2) {
if (wrapper.find('[data-test="history-0"]').exists()) await wrapper.get('[data-test="history-0"]').setValue(true) if (wrapper.find('[data-test="history-0"]').exists()) await wrapper.get('[data-test="history-0"]').setValue(true)
@@ -75,6 +77,37 @@ beforeEach(() => {
vi.mocked(getPallet).mockResolvedValue({ id: '30', warehouseId: '92', locationId: '40', palletCode: 'TARGET' } as Awaited<ReturnType<typeof getPallet>>) vi.mocked(getPallet).mockResolvedValue({ id: '30', warehouseId: '92', locationId: '40', palletCode: 'TARGET' } as Awaited<ReturnType<typeof getPallet>>)
vi.mocked(getOverseasLocation).mockResolvedValue({ id: '40', warehouseId: '92', locationCode: 'LOC' } as Awaited<ReturnType<typeof getOverseasLocation>>) vi.mocked(getOverseasLocation).mockResolvedValue({ id: '40', warehouseId: '92', locationCode: 'LOC' } as Awaited<ReturnType<typeof getOverseasLocation>>)
}) })
it('real receipt quantity control becomes accessible after starting processing', async () => {
const pending = snapshot('EX-1', 0, 2)
pending.order.status = 'PENDING'; pending.order.allowedActions = ['START_PROCESSING']; pending.returnSources = []
vi.mocked(getExceptionDetail).mockResolvedValueOnce(pending).mockResolvedValueOnce(snapshot('EX-1', 1, 2))
vi.mocked(commandException).mockResolvedValue({ exceptionNo: 'EX-1', version: 1, status: 'PROCESSING', restoredPackages: 0, replayed: false })
const wrapper = open(true); try { await flushPromises()
await wrapper.get('[data-test="start"]').trigger('click'); await flushPromises()
const input = wrapper.getComponent(ElInputNumber).get('input')
expect((input.element as HTMLInputElement).disabled).toBe(false)
expect(input.attributes('aria-disabled')).toBe('false')
} finally { wrapper.unmount() }
})
it('real quantity control reflects in-flight lock and unlocks after a partial receipt reload', async () => {
vi.mocked(getExceptionDetail).mockResolvedValueOnce(snapshot('EX-1', 1, 2)).mockResolvedValueOnce(snapshot('EX-1', 2, 1))
let finish!: (value: Awaited<ReturnType<typeof commandException>>) => void
vi.mocked(commandException).mockReturnValue(new Promise(resolve => { finish = resolve }))
const wrapper = open(true); try { await flushPromises()
await wrapper.get('[data-test="history-0"]').setValue(true)
wrapper.getComponent(ElInputNumber).vm.$emit('update:modelValue', 1)
await wrapper.get('[data-test="target-0"]').setValue('30'); await flushPromises()
await wrapper.get('[data-test="complete"]').trigger('click'); await flushPromises()
expect((wrapper.getComponent(ElInputNumber).get('input').element as HTMLInputElement).disabled).toBe(true)
expect(wrapper.getComponent(ElInputNumber).get('input').attributes('aria-disabled')).toBe('true')
finish({ exceptionNo: 'EX-1', version: 2, status: 'PROCESSING', restoredPackages: 1, replayed: false }); await flushPromises()
const input = wrapper.getComponent(ElInputNumber).get('input')
expect((input.element as HTMLInputElement).disabled).toBe(false)
expect(input.attributes('aria-disabled')).toBe('false')
expect(wrapper.text()).toContain('剩余可收 1 箱')
expect(commandException).toHaveBeenCalledTimes(1)
} finally { wrapper.unmount() }
})
it('submits two actual batches using refreshed remaining sources and independent command keys', async () => { it('submits two actual batches using refreshed remaining sources and independent command keys', async () => {
vi.mocked(getExceptionDetail).mockResolvedValueOnce(snapshot()).mockResolvedValueOnce(snapshot('EX-1', 2, 2)).mockResolvedValueOnce(snapshot('EX-1', 3, 0)) vi.mocked(getExceptionDetail).mockResolvedValueOnce(snapshot()).mockResolvedValueOnce(snapshot('EX-1', 2, 2)).mockResolvedValueOnce(snapshot('EX-1', 3, 0))
vi.mocked(commandException).mockResolvedValueOnce({ exceptionNo: 'EX-1', version: 2, status: 'PROCESSING', restoredPackages: 2, replayed: false }) vi.mocked(commandException).mockResolvedValueOnce({ exceptionNo: 'EX-1', version: 2, status: 'PROCESSING', restoredPackages: 2, replayed: false })
@@ -208,7 +208,8 @@ watch(() => [props.customerId, props.exceptionNo, props.active], () => { ++seque
<p>只填写本批实际收到的箱数;未收齐保持处理中。目标库位取所选托盘的真实库位。</p> <p>只填写本批实际收到的箱数;未收齐保持处理中。目标库位取所选托盘的真实库位。</p>
<div v-for="(row, index) in receipts" :key="`${row.outboundPalletItemId}:${row.inventoryPositionId}`"> <div v-for="(row, index) in receipts" :key="`${row.outboundPalletItemId}:${row.inventoryPositionId}`">
<p>{{ row.sourcePalletNo }} · SKU {{ row.skuCode || '—' }} · 剩余可收 {{ row.maxReturnPackages }} 箱</p> <p>{{ row.sourcePalletNo }} · SKU {{ row.skuCode || '—' }} · 剩余可收 {{ row.maxReturnPackages }} 箱</p>
<el-form-item label="本批实收箱数"><el-input-number v-model="row.quantity" :data-test="`quantity-${index}`" :min="0" :max="row.maxReturnPackages" :precision="0" placeholder="未收到可留空" /></el-form-item> <!-- Element Plus initializes number-input ARIA on mount; remount when the form lock changes. -->
<el-form-item label="本批实收箱数"><el-input-number :key="locked ? 'locked' : 'editable'" v-model="row.quantity" :data-test="`quantity-${index}`" :min="0" :max="row.maxReturnPackages" :precision="0" placeholder="未收到可留空" /></el-form-item>
<el-form-item label="实际收货目标托盘"><el-select :model-value="row.target" :data-test="`target-${index}`" filterable remote :remote-method="searchTargets" placeholder="输入托盘码查询" @visible-change="onTargetVisibility" @update:model-value="onTargetChange(index, $event)"><el-option v-for="target in targets" :key="target.id" :value="String(target.id)" :label="target.palletCode" /></el-select></el-form-item> <el-form-item label="实际收货目标托盘"><el-select :model-value="row.target" :data-test="`target-${index}`" filterable remote :remote-method="searchTargets" placeholder="输入托盘码查询" @visible-change="onTargetVisibility" @update:model-value="onTargetChange(index, $event)"><el-option v-for="target in targets" :key="target.id" :value="String(target.id)" :label="target.palletCode" /></el-select></el-form-item>
<p>目标托盘:{{ row.palletNo || '—' }};实际库位:{{ row.locationCode || '—' }}</p> <p>目标托盘:{{ row.palletNo || '—' }};实际库位:{{ row.locationCode || '—' }}</p>
</div> </div>