style(admin): unify dialog layouts and dense readonly presentation
This commit is contained in:
@@ -1,18 +1,23 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import WarehouseTransferReceiptPanel from './WarehouseTransferReceiptPanel.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import DataTable from '@/shared/components/DataTable.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import { ApiResponseError } from '@/shared/api/http'
|
||||
import { getWarehouseTransfer, getWarehouseTransferCartonLabel, increaseWarehouseTransferForecast, type WarehouseTransferDetailDto } from '../api/warehouseTransfers'
|
||||
|
||||
vi.mock('../api/warehouseTransfers', () => ({ getWarehouseTransfer: vi.fn(), getWarehouseTransferCartonLabel: vi.fn(), increaseWarehouseTransferForecast: vi.fn() }))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
|
||||
const can = vi.hoisted(() => vi.fn(() => true))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) }))
|
||||
enableAutoUnmount(afterEach)
|
||||
const DataTableStub = defineComponent({
|
||||
props: ['data', 'columns'],
|
||||
setup(props, { slots }) { return () => h('div', { class: 'test-table' }, [JSON.stringify(props.data), ...(props.data || []).flatMap((row: object) => [slots.boxAction?.({ row }), slots.quantityDifference?.({ row }), slots.boxDifference?.({ row })])]) },
|
||||
})
|
||||
const DialogStub = defineComponent({ props: ['modelValue'], setup(props, { slots }) { return () => props.modelValue ? h('section', { 'data-test': 'box-dialog' }, slots.default?.()) : null } })
|
||||
const DialogStub = defineComponent({ props: ['modelValue', 'confirmText', 'confirmDisabled', 'confirmLoading'], emits: ['confirm'], setup(props, { slots, emit }) { return () => props.modelValue ? h('section', { 'data-test': 'box-dialog' }, [slots.default?.(), props.confirmText ? h('button', { disabled: props.confirmDisabled || props.confirmLoading, onClick: () => emit('confirm') }, props.confirmText) : null]) : null } })
|
||||
const render = (transferNo = 'WT-SOURCE') => mount(WarehouseTransferReceiptPanel, { props: { customerId: 'CUSTOMER', transferNo }, global: { plugins: [ElementPlus], stubs: { DataTable: DataTableStub, AppDialog: DialogStub } } })
|
||||
function fixture(): WarehouseTransferDetailDto {
|
||||
return {
|
||||
@@ -27,7 +32,7 @@ function fixture(): WarehouseTransferDetailDto {
|
||||
pallets: [{ id: '100', sequenceNo: 1, palletNo: 'PALLET-A', cartonCount: 2, grossWeightKg: 6, volumeCbm: .2, status: 'COMPLETED' }, { id: '200', sequenceNo: 2, palletNo: 'PALLET-B', cartonCount: 1, grossWeightKg: 4, volumeCbm: .1, status: 'COMPLETED' }], relabelBindings: [],
|
||||
}
|
||||
}
|
||||
beforeEach(() => { vi.mocked(getWarehouseTransfer).mockReset(); vi.mocked(getWarehouseTransferCartonLabel).mockReset() })
|
||||
beforeEach(() => { can.mockReset().mockReturnValue(true); vi.mocked(getWarehouseTransfer).mockReset(); vi.mocked(getWarehouseTransferCartonLabel).mockReset(); vi.mocked(increaseWarehouseTransferForecast).mockReset() })
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
describe('转运预报与实际签收明细', () => {
|
||||
it('shows every SKU in a mixed box and counts its physical measurements once', async () => {
|
||||
@@ -145,3 +150,153 @@ it('平台可补整单预报箱数,无需选择SKU', async () => {
|
||||
expect(increaseWarehouseTransferForecast).toHaveBeenCalledWith('CUSTOMER', 'WT-SOURCE', { expectedPackageCount: 4, packageCount: 6 })
|
||||
expect(wrapper.text()).toContain('预报箱数 6 箱')
|
||||
})
|
||||
|
||||
async function nativeReceipt() {
|
||||
const wrapper = mount(WarehouseTransferReceiptPanel, { attachTo: document.body, props: { customerId: 'CUSTOMER', transferNo: 'WT-SOURCE' }, global: { plugins: [ElementPlus], stubs: { transition: false } } })
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
function popup(wrapper: VueWrapper, title: string) { return wrapper.findAllComponents(AppDialog).find(dialog => dialog.props('title').startsWith(title))! }
|
||||
async function openNativeForecast() {
|
||||
const wrapper = await nativeReceipt()
|
||||
await wrapper.findAll('button').find(button => button.text() === '增加预报箱数')!.trigger('click'); await flushPromises()
|
||||
return { wrapper, dialog: popup(wrapper, '增加预报箱数') }
|
||||
}
|
||||
function forecastFixture() { return { ...fixture(), inboundCode: 'WT-SOURCE', status: 'WAIT_PDA_RECEIVING' } }
|
||||
|
||||
describe('receipt native popup integration', () => {
|
||||
it('keeps counted boxes and mixed SKU contents through two independent fullscreen frames', async () => {
|
||||
const value = fixture()
|
||||
value.cartons[0] = { ...value.cartons[0]!, quantity: 10, discrepancyReason: '少收两件,客户确认', items: [{ transferItemId: '10', skuCode: 'SKU-A', quantity: 3 }, { transferItemId: '20', skuCode: 'SKU-B', quantity: 7 }] }
|
||||
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
|
||||
const wrapper = await nativeReceipt()
|
||||
await wrapper.findAll('button').find(button => button.text() === '查看箱号明细')!.trigger('click'); await flushPromises()
|
||||
const cartonsDialog = popup(wrapper, '签收箱号明细')
|
||||
expect(cartonsDialog.props('title')).toBe('签收箱号明细 · WT-SOURCE')
|
||||
expect(cartonsDialog.get('[aria-label="已清点箱号"]').findAll('.el-table__body-wrapper .el-table__row')).toHaveLength(3)
|
||||
expect(cartonsDialog.text()).not.toContain('BOX-MISSING')
|
||||
await cartonsDialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises()
|
||||
await cartonsDialog.findAll('.el-table__body-wrapper button').find(button => button.text() === '箱内明细')!.trigger('click'); await flushPromises()
|
||||
const boxDialog = popup(wrapper, '箱内清点明细')
|
||||
expect(boxDialog.props('title')).toBe('箱内清点明细 · BOX-1')
|
||||
await boxDialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises()
|
||||
expect(boxDialog.findComponent({ name: 'ElDialog' }).props('fullscreen')).toBe(true)
|
||||
expect(boxDialog.findComponent(DataTable).props('data')).toEqual([{ sku: 'SKU-A', name: '产品 A', quantity: 3 }, { sku: 'SKU-B', name: '产品 B', quantity: 7 }])
|
||||
expect(boxDialog.text()).toContain('少收两件,客户确认')
|
||||
expect(boxDialog.get('[aria-label="当前签收箱"]').text()).toContain('2026-10-15')
|
||||
await boxDialog.get('.el-dialog__headerbtn').trigger('click'); await new Promise(resolve => setTimeout(resolve, 80)); await flushPromises()
|
||||
expect(boxDialog.props('modelValue')).toBe(false)
|
||||
expect(cartonsDialog.props('modelValue')).toBe(true)
|
||||
expect(cartonsDialog.findComponent({ name: 'ElDialog' }).props('fullscreen')).toBe(true)
|
||||
expect(getWarehouseTransfer).toHaveBeenCalledExactlyOnceWith('CUSTOMER', 'WT-SOURCE')
|
||||
})
|
||||
it('retains every counted box in the main dialog body without a nested height cap', async () => {
|
||||
const value = fixture()
|
||||
value.cartons = Array.from({ length: 30 }, (_, index) => ({ ...value.cartons[0]!, cartonMark: `BOX-${index + 1}`, cartonIndex: index + 1, totalCartons: 30 }))
|
||||
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
|
||||
const wrapper = await nativeReceipt()
|
||||
await wrapper.findAll('button').find(button => button.text() === '查看箱号明细')!.trigger('click'); await flushPromises()
|
||||
const dialog = popup(wrapper, '签收箱号明细')
|
||||
expect(dialog.get('[aria-label="实收箱数核对"]').text()).toContain('30 箱')
|
||||
expect(dialog.findAll('.el-table__body-wrapper .el-table__row')).toHaveLength(30)
|
||||
expect(dialog.text()).toContain('BOX-30')
|
||||
expect(dialog.findComponent(DataTable).props('maxHeight')).toBe('')
|
||||
expect((dialog.get('.el-table__body-wrapper .el-scrollbar__wrap').element as HTMLElement).style.maxHeight).toBe('')
|
||||
})
|
||||
it('freezes the original forecast count through fullscreen and sends only the scoped count command', async () => {
|
||||
const value = forecastFixture()
|
||||
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
|
||||
vi.mocked(increaseWarehouseTransferForecast).mockResolvedValue({ ...value, packageCount: 6 })
|
||||
const { wrapper, dialog } = await openNativeForecast()
|
||||
expect(dialog.props('title')).toBe('增加预报箱数 · WT-SOURCE')
|
||||
expect(dialog.get('[aria-label="预报箱数核对"]').findAll('input')).toHaveLength(0)
|
||||
dialog.findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 2); await flushPromises()
|
||||
await dialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises()
|
||||
expect(dialog.get('[aria-label="预报箱数核对"]').text()).toContain('增加后6 箱')
|
||||
expect(dialog.findComponent({ name: 'ElInputNumber' }).props('modelValue')).toBe(2)
|
||||
await dialog.findAll('button').find(button => button.text() === '确认增加')!.trigger('click'); await flushPromises()
|
||||
expect(increaseWarehouseTransferForecast).toHaveBeenCalledExactlyOnceWith('CUSTOMER', 'WT-SOURCE', { expectedPackageCount: 4, packageCount: 6 })
|
||||
expect(dialog.props('modelValue')).toBe(false)
|
||||
expect(wrapper.text()).toContain('预报箱数 6 箱')
|
||||
})
|
||||
it('locks pending edits and closing, preserves the failed count and retries the same snapshot', async () => {
|
||||
const value = forecastFixture()
|
||||
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
|
||||
let reject!: (reason: unknown) => void
|
||||
vi.mocked(increaseWarehouseTransferForecast).mockReturnValueOnce(new Promise((_, no) => { reject = no })).mockResolvedValueOnce({ ...value, packageCount: 6 })
|
||||
const { dialog } = await openNativeForecast()
|
||||
dialog.findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 2); await flushPromises()
|
||||
dialog.vm.$emit('confirm'); dialog.vm.$emit('confirm'); await flushPromises()
|
||||
expect(increaseWarehouseTransferForecast).toHaveBeenCalledTimes(1)
|
||||
expect(dialog.findComponent({ name: 'ElDialog' }).props()).toMatchObject({ showClose: false, closeOnPressEscape: false })
|
||||
expect(dialog.get('input[placeholder="请输入新增箱数"]').attributes('disabled')).toBeDefined()
|
||||
expect(dialog.findAll('button').find(button => button.text() === '取消')!.attributes('disabled')).toBeDefined()
|
||||
reject(new ApiResponseError('箱数已被其他操作修改', 'A0409', 409, null, 'forecast-frame-request')); await flushPromises()
|
||||
expect(dialog.props('modelValue')).toBe(true)
|
||||
expect(dialog.findComponent({ name: 'ElInputNumber' }).props('modelValue')).toBe(2)
|
||||
expect(dialog.text()).toContain('forecast-frame-request')
|
||||
expect(dialog.get('[aria-label="预报箱数核对"]').text()).toContain('原预报箱数4 箱')
|
||||
await dialog.findAll('button').find(button => button.text() === '重试本次增加')!.trigger('click'); await flushPromises()
|
||||
expect(vi.mocked(increaseWarehouseTransferForecast).mock.calls).toEqual([
|
||||
['CUSTOMER', 'WT-SOURCE', { expectedPackageCount: 4, packageCount: 6 }],
|
||||
['CUSTOMER', 'WT-SOURCE', { expectedPackageCount: 4, packageCount: 6 }],
|
||||
])
|
||||
})
|
||||
it('keeps whole-box and 5000-box limits at both the frame and submit handler', async () => {
|
||||
const value = { ...forecastFixture(), packageCount: 4999 }
|
||||
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
|
||||
vi.mocked(increaseWarehouseTransferForecast).mockResolvedValue({ ...value, packageCount: 5000 })
|
||||
const { dialog } = await openNativeForecast()
|
||||
const input = dialog.findComponent({ name: 'ElInputNumber' })
|
||||
expect(input.props('max')).toBe(1)
|
||||
for (const count of [0, -1, 1.5, Number.NaN, 2]) {
|
||||
input.vm.$emit('update:modelValue', count)
|
||||
dialog.vm.$emit('confirm'); await flushPromises()
|
||||
expect(increaseWarehouseTransferForecast).not.toHaveBeenCalled()
|
||||
input.vm.$emit('update:modelValue', 1); await flushPromises()
|
||||
}
|
||||
input.vm.$emit('update:modelValue', 1); await flushPromises()
|
||||
expect(dialog.props('confirmDisabled')).toBe(false)
|
||||
dialog.vm.$emit('confirm'); await flushPromises()
|
||||
expect(increaseWarehouseTransferForecast).toHaveBeenCalledExactlyOnceWith('CUSTOMER', 'WT-SOURCE', { expectedPackageCount: 4999, packageCount: 5000 })
|
||||
})
|
||||
it('resets a cancelled draft and its failure context when reopened', async () => {
|
||||
vi.mocked(getWarehouseTransfer).mockResolvedValue(forecastFixture())
|
||||
vi.mocked(increaseWarehouseTransferForecast).mockRejectedValue(new ApiResponseError('增加失败', 'A0503', 503, null, 'old-forecast-request'))
|
||||
const { wrapper, dialog } = await openNativeForecast()
|
||||
dialog.findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 2); await flushPromises()
|
||||
dialog.vm.$emit('confirm'); await flushPromises()
|
||||
expect(dialog.text()).toContain('old-forecast-request')
|
||||
await dialog.findAll('button').find(button => button.text() === '取消')!.trigger('click'); await flushPromises()
|
||||
await wrapper.findAll('button').find(button => button.text() === '增加预报箱数')!.trigger('click'); await flushPromises()
|
||||
expect(dialog.findComponent({ name: 'ElInputNumber' }).props('modelValue')).toBe(1)
|
||||
expect(dialog.findComponent(LoadErrorState).exists()).toBe(false)
|
||||
expect(increaseWarehouseTransferForecast).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
it('closes the old forecast frame on a scope change and ignores a late failure', async () => {
|
||||
vi.mocked(getWarehouseTransfer).mockResolvedValueOnce(forecastFixture()).mockResolvedValueOnce({ ...forecastFixture(), transferNo: 'WT-NEW', inboundCode: 'WT-NEW', packageCount: 8 })
|
||||
let reject!: (reason: unknown) => void
|
||||
vi.mocked(increaseWarehouseTransferForecast).mockReturnValueOnce(new Promise((_, no) => { reject = no }))
|
||||
const { wrapper, dialog } = await openNativeForecast()
|
||||
dialog.findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 2); await flushPromises()
|
||||
dialog.vm.$emit('confirm'); await flushPromises()
|
||||
await wrapper.setProps({ customerId: 'CUSTOMER-NEW', transferNo: 'WT-NEW' }); await flushPromises()
|
||||
expect(dialog.props('modelValue')).toBe(false)
|
||||
reject(new ApiResponseError('旧票响应', 'A0503', 503, null, 'stale-forecast-request')); await flushPromises()
|
||||
expect(wrapper.text()).toContain('预报箱数 8 箱')
|
||||
expect(wrapper.text()).not.toContain('stale-forecast-request')
|
||||
await wrapper.findAll('button').find(button => button.text() === '增加预报箱数')!.trigger('click'); await flushPromises()
|
||||
expect(dialog.props('title')).toBe('增加预报箱数 · WT-NEW')
|
||||
expect(dialog.get('[aria-label="预报箱数核对"]').text()).toContain('原预报箱数8 箱')
|
||||
expect(dialog.findComponent({ name: 'ElInputNumber' }).props('modelValue')).toBe(1)
|
||||
expect(increaseWarehouseTransferForecast).toHaveBeenCalledExactlyOnceWith('CUSTOMER', 'WT-SOURCE', { expectedPackageCount: 4, packageCount: 6 })
|
||||
})
|
||||
it('keeps forecast modification unavailable without approval permission', async () => {
|
||||
can.mockReturnValue(false)
|
||||
vi.mocked(getWarehouseTransfer).mockResolvedValue(forecastFixture())
|
||||
const wrapper = await nativeReceipt()
|
||||
expect(wrapper.findAll('button').some(button => button.text() === '增加预报箱数')).toBe(false)
|
||||
popup(wrapper, '增加预报箱数').vm.$emit('confirm'); await flushPromises()
|
||||
expect(increaseWarehouseTransferForecast).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user