From a5d4b7d54a4dc8c65cd884bf1d2fea73957793f1 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 1 Sep 2026 19:57:36 +0800 Subject: [PATCH] test: cover phase 5 overseas admin flows --- .../api/customerVisits.test.ts | 5 ++ .../api/deliveryOrders.test.ts | 20 ++++- .../pages/CustomerVisitsView.test.ts | 76 +++++++++++++++++ .../pages/DeliveryOrderDetailView.test.ts | 42 ++++++++++ .../pages/DeliveryOrdersView.test.ts | 78 +++++++++++++++++ .../pages/DispatchDetailView.test.ts | 32 +++++++ .../pages/ExceptionOrdersView.test.ts | 59 +++++++++++++ .../pages/phase5TestStubs.ts | 83 +++++++++++++++++++ 8 files changed, 392 insertions(+), 3 deletions(-) create mode 100644 src/modules/overseas-warehouse/pages/CustomerVisitsView.test.ts create mode 100644 src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.test.ts create mode 100644 src/modules/overseas-warehouse/pages/DeliveryOrdersView.test.ts create mode 100644 src/modules/overseas-warehouse/pages/DispatchDetailView.test.ts create mode 100644 src/modules/overseas-warehouse/pages/ExceptionOrdersView.test.ts create mode 100644 src/modules/overseas-warehouse/pages/phase5TestStubs.ts diff --git a/src/modules/overseas-warehouse/api/customerVisits.test.ts b/src/modules/overseas-warehouse/api/customerVisits.test.ts index 88f1e078..c1645c2e 100644 --- a/src/modules/overseas-warehouse/api/customerVisits.test.ts +++ b/src/modules/overseas-warehouse/api/customerVisits.test.ts @@ -39,6 +39,11 @@ describe('customer visit API', () => { await uploadCustomerVisitAttachment('9', '100', file) const body = vi.mocked(overseasHttp.post).mock.calls[0]?.[1] as FormData expect(body.get('file')).toBe(file) + expect(overseasHttp.post).toHaveBeenCalledWith( + '/platform/v1/last-leg/customer-visits/9/attachments', + body, + { params: { customerId: '100' }, headers: { 'Content-Type': 'multipart/form-data' } }, + ) vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', data: { visit: { id: '9' }, inboundOrders: [] } }) await saveCustomerVisit('9', '100', { selectedInboundOrderId: '7', visitResult: 'NORMAL', attachments: [] }) diff --git a/src/modules/overseas-warehouse/api/deliveryOrders.test.ts b/src/modules/overseas-warehouse/api/deliveryOrders.test.ts index fc27b3ce..cee766b0 100644 --- a/src/modules/overseas-warehouse/api/deliveryOrders.test.ts +++ b/src/modules/overseas-warehouse/api/deliveryOrders.test.ts @@ -1,11 +1,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { overseasHttp } from '@/shared/api/overseasHttp' -import { getDeliveryOrder, listDeliveryOrders } from './deliveryOrders' +import { getDeliveryOrder, listDeliveryOrders, listDeliveryReturnInboundOptions, markDeliveryReturn } from './deliveryOrders' -vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn() } })) +vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn() } })) describe('delivery order API', () => { - beforeEach(() => vi.mocked(overseasHttp.get).mockReset()) + beforeEach(() => { + vi.mocked(overseasHttp.get).mockReset() + vi.mocked(overseasHttp.post).mockReset() + }) it('passes paging and expanded filters and normalizes page numbers', async () => { vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { records: [], page: '2', size: '20', total: '41', pages: '3' } }) @@ -19,4 +22,15 @@ describe('delivery order API', () => { await getDeliveryOrder('DEL/1', '1001') expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1', { params: { customerId: '1001', page: 1, size: 200 } }) }) + + it('loads return options and marks a return in the explicit customer scope', async () => { + vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: [{ inboundOrderId: '9', inboundCode: 'IN-1' }] }) + await listDeliveryReturnInboundOptions('DEL/1', '1001') + expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1/return-inbound-options', { params: { customerId: '1001' } }) + + const payload = { inboundOrderId: '9', reason: '客户拒收', returnDirection: 'OVERSEAS_WAREHOUSE', packageCount: 2 } + vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: { id: '19', exceptionNo: 'EXC-1', status: 'PENDING' } }) + await markDeliveryReturn('DEL/1', '1001', payload) + expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1/mark-return', payload, { params: { customerId: '1001' } }) + }) }) diff --git a/src/modules/overseas-warehouse/pages/CustomerVisitsView.test.ts b/src/modules/overseas-warehouse/pages/CustomerVisitsView.test.ts new file mode 100644 index 00000000..0c448c33 --- /dev/null +++ b/src/modules/overseas-warehouse/pages/CustomerVisitsView.test.ts @@ -0,0 +1,76 @@ +import ElementPlus from 'element-plus' +import { flushPromises, mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { getCustomerVisit, listCustomerVisits, saveCustomerVisit, uploadCustomerVisitAttachment } from '../api/customerVisits' +import CustomerVisitsView from './CustomerVisitsView.vue' +import { commonPhase5Stubs } from './phase5TestStubs' + +const canPermission = vi.hoisted(() => vi.fn((_code: string) => false)) +vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canPermission }) })) +vi.mock('../api/customerVisits', () => ({ listCustomerVisits: vi.fn(), getCustomerVisit: vi.fn(), saveCustomerVisit: vi.fn(), uploadCustomerVisitAttachment: vi.fn() })) + +const visit = { + id: '9', customerId: '100', deliveryNo: 'DEL-1', deliveryCompletedAt: '2026-09-01T10:00:00', + status: 'PENDING' as const, visitCount: 0, attachments: [], +} +function mountView() { return mount(CustomerVisitsView, { global: { plugins: [ElementPlus], stubs: commonPhase5Stubs } }) } + +describe('CustomerVisitsView permissions and visit material', () => { + beforeEach(() => { + canPermission.mockReset().mockReturnValue(false) + vi.mocked(listCustomerVisits).mockReset().mockResolvedValue({ records: [visit], current: 1, size: 10, total: 1, pages: 1, visitable: 1, pending: 1, completed: 0 }) + vi.mocked(getCustomerVisit).mockReset().mockResolvedValue({ + visit, + inboundOrders: [{ inboundOrderId: '7', inboundCode: 'IN-1', lastMileOrderId: '6', lastMileOrderNo: 'LM-1', packageCount: 2 }], + customerFeedback: '', remark: '', + }) + vi.mocked(saveCustomerVisit).mockReset().mockResolvedValue({ visit, inboundOrders: [] }) + vi.mocked(uploadCustomerVisitAttachment).mockReset().mockResolvedValue({ fileId: '8', fileName: 'visit.pdf', sizeBytes: 4 }) + }) + + it('hides the visit action without edit permission', async () => { + const wrapper = mountView() + await flushPromises() + expect(wrapper.find('[data-testid="data-table"]').text()).not.toContain('回访') + wrapper.unmount() + }) + + it('validates and submits a customer-scoped visit', async () => { + canPermission.mockImplementation(code => code.endsWith(':edit')) + const wrapper = mountView() + await flushPromises() + await wrapper.findAll('button').find(button => button.text() === '回访')!.trigger('click') + await flushPromises() + expect(getCustomerVisit).toHaveBeenCalledWith('9', '100') + + const dialog = wrapper.find('[data-dialog="客户回访"]') + await dialog.find('.dialog-confirm').trigger('click') + expect(saveCustomerVisit).not.toHaveBeenCalled() + await dialog.find('select').setValue('NORMAL') + const textareas = dialog.findAll('textarea') + await textareas[0].setValue(' 服务正常 ') + await textareas[1].setValue(' 后续关注 ') + await dialog.find('.dialog-confirm').trigger('click') + await flushPromises() + expect(saveCustomerVisit).toHaveBeenCalledWith('9', '100', { + selectedInboundOrderId: '7', visitResult: 'NORMAL', customerFeedback: '服务正常', remark: '后续关注', attachments: [], + }) + wrapper.unmount() + }) + + it('uploads the selected attachment in the visit customer scope', async () => { + canPermission.mockImplementation(code => code.endsWith(':edit')) + const wrapper = mountView() + await flushPromises() + await wrapper.findAll('button').find(button => button.text() === '回访')!.trigger('click') + await flushPromises() + const input = wrapper.find('[data-dialog="客户回访"] input[type="file"]') + const file = new File(['data'], 'visit.pdf', { type: 'application/pdf' }) + Object.defineProperty(input.element, 'files', { configurable: true, value: [file] }) + await input.trigger('change') + await flushPromises() + expect(uploadCustomerVisitAttachment).toHaveBeenCalledWith('9', '100', file) + expect(wrapper.text()).toContain('visit.pdf') + wrapper.unmount() + }) +}) diff --git a/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.test.ts b/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.test.ts new file mode 100644 index 00000000..23c2b7f8 --- /dev/null +++ b/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.test.ts @@ -0,0 +1,42 @@ +import ElementPlus from 'element-plus' +import { flushPromises, mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { getDeliveryOrder } from '../api/deliveryOrders' +import DeliveryOrderDetailView from './DeliveryOrderDetailView.vue' + +const routeState = vi.hoisted(() => ({ params: { deliveryNo: 'DEL/1' }, query: { customerId: '100' } as Record })) +vi.mock('vue-router', () => ({ useRoute: () => routeState, useRouter: () => ({ push: vi.fn() }) })) +vi.mock('../api/deliveryOrders', () => ({ getDeliveryOrder: vi.fn() })) + +describe('DeliveryOrderDetailView', () => { + beforeEach(() => { + routeState.query = { customerId: '100' } + vi.mocked(getDeliveryOrder).mockReset().mockResolvedValue({ + order: { + id: '1', customerId: '100', deliveryNo: 'DEL/1', outboundNo: 'OUT-1', dispatchNo: 'PC-1', + status: 'DELIVERING', overseasWarehouseName: '洛杉矶仓', palletCount: 1, packageCount: 2, + createdAt: '2026-09-01T10:00:00', + }, + skus: [], fees: [], operations: [], + }) + }) + + it('loads and renders detail in the explicit customer scope', async () => { + const wrapper = mount(DeliveryOrderDetailView, { global: { plugins: [ElementPlus] } }) + await flushPromises() + expect(getDeliveryOrder).toHaveBeenCalledWith('DEL/1', '100') + expect(wrapper.text()).toContain('DEL/1') + expect(wrapper.text()).toContain('派送中') + expect(wrapper.text()).toContain('洛杉矶仓') + wrapper.unmount() + }) + + it('blocks an unsafe detail request without customer scope', async () => { + routeState.query = {} + const wrapper = mount(DeliveryOrderDetailView, { global: { plugins: [ElementPlus] } }) + await flushPromises() + expect(getDeliveryOrder).not.toHaveBeenCalled() + expect(wrapper.text()).toContain('缺少派送单客户范围') + wrapper.unmount() + }) +}) diff --git a/src/modules/overseas-warehouse/pages/DeliveryOrdersView.test.ts b/src/modules/overseas-warehouse/pages/DeliveryOrdersView.test.ts new file mode 100644 index 00000000..d49f27d4 --- /dev/null +++ b/src/modules/overseas-warehouse/pages/DeliveryOrdersView.test.ts @@ -0,0 +1,78 @@ +import ElementPlus from 'element-plus' +import { flushPromises, mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { listDeliveryOrders, listDeliveryReturnInboundOptions, markDeliveryReturn } from '../api/deliveryOrders' +import { updateDeliveryOrderStatus } from '../api/customerVisits' +import DeliveryOrdersView from './DeliveryOrdersView.vue' +import { commonPhase5Stubs } from './phase5TestStubs' + +const canPermission = vi.hoisted(() => vi.fn((_code: string) => false)) +vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canPermission }) })) +vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) })) +vi.mock('../api/deliveryOrders', () => ({ listDeliveryOrders: vi.fn(), listDeliveryReturnInboundOptions: vi.fn(), markDeliveryReturn: vi.fn() })) +vi.mock('../api/customerVisits', () => ({ updateDeliveryOrderStatus: vi.fn() })) +vi.mock('../api/pallets', () => ({ listReceivingOverseasWarehouses: vi.fn() })) + +const row = { + id: '1', customerId: '100', deliveryNo: 'DEL/1', outboundNo: 'OUT-1', dispatchNo: 'PC-1', + status: 'WAIT_DELIVERY', palletCount: 1, packageCount: 2, createdAt: '2026-09-01T10:00:00', +} + +function mountView() { + return mount(DeliveryOrdersView, { global: { plugins: [ElementPlus], stubs: commonPhase5Stubs } }) +} + +describe('DeliveryOrdersView permissions and commands', () => { + beforeEach(() => { + canPermission.mockReset().mockReturnValue(false) + vi.mocked(listDeliveryOrders).mockReset().mockResolvedValue({ records: [row], page: 1, size: 10, total: 1, pages: 1 }) + vi.mocked(listDeliveryReturnInboundOptions).mockReset().mockResolvedValue([{ inboundOrderId: '9', inboundCode: 'IN-1', lastMileOrderNo: 'LM-1', customerId: '100' }]) + vi.mocked(markDeliveryReturn).mockReset().mockResolvedValue({ id: '19', exceptionNo: 'EXC-1', status: 'PENDING' }) + vi.mocked(updateDeliveryOrderStatus).mockReset().mockResolvedValue({ deliveryNo: 'DEL/1', deliveryStatus: 'WAIT_DELIVERY', replayed: false }) + }) + + it('keeps detail read-only while hiding independent write operations', async () => { + const wrapper = mountView() + await flushPromises() + const tableText = wrapper.find('[data-testid="data-table"]').text() + expect(tableText).toContain('详情') + expect(tableText).not.toContain('编辑状态') + expect(tableText).not.toContain('标记退货') + wrapper.unmount() + }) + + it('submits status and return commands with the row customer scope', async () => { + canPermission.mockImplementation(code => code.endsWith(':status') || code.endsWith(':return')) + const wrapper = mountView() + await flushPromises() + + await wrapper.findAll('button').find(button => button.text().includes('编辑状态'))!.trigger('click') + await wrapper.find('[data-dialog="编辑派送状态"] .dialog-confirm').trigger('click') + await flushPromises() + expect(updateDeliveryOrderStatus).toHaveBeenCalledWith('DEL/1', '100', { status: 'WAIT_DELIVERY', remark: undefined }) + + await wrapper.findAll('button').find(button => button.text().includes('标记退货'))!.trigger('click') + await flushPromises() + expect(listDeliveryReturnInboundOptions).toHaveBeenCalledWith('DEL/1', '100') + await wrapper.find('[data-dialog="标记退货"] textarea').setValue(' 客户拒收 ') + await wrapper.find('[data-dialog="标记退货"] .dialog-confirm').trigger('click') + await flushPromises() + expect(markDeliveryReturn).toHaveBeenCalledWith('DEL/1', '100', { + inboundOrderId: '9', reason: '客户拒收', returnDirection: 'OVERSEAS_WAREHOUSE', + returnLogisticsNo: undefined, packageCount: undefined, skuCode: undefined, + }) + wrapper.unmount() + }) + + it('does not submit an incomplete return form', async () => { + canPermission.mockImplementation(code => code.endsWith(':return')) + vi.mocked(listDeliveryReturnInboundOptions).mockResolvedValue([]) + const wrapper = mountView() + await flushPromises() + await wrapper.findAll('button').find(button => button.text().includes('标记退货'))!.trigger('click') + await flushPromises() + await wrapper.find('[data-dialog="标记退货"] .dialog-confirm').trigger('click') + expect(markDeliveryReturn).not.toHaveBeenCalled() + wrapper.unmount() + }) +}) diff --git a/src/modules/overseas-warehouse/pages/DispatchDetailView.test.ts b/src/modules/overseas-warehouse/pages/DispatchDetailView.test.ts new file mode 100644 index 00000000..cf1e8372 --- /dev/null +++ b/src/modules/overseas-warehouse/pages/DispatchDetailView.test.ts @@ -0,0 +1,32 @@ +import ElementPlus from 'element-plus' +import { flushPromises, mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { getDispatch } from '../api/dispatches' +import DispatchDetailView from './DispatchDetailView.vue' +import { DataTableStub } from './phase5TestStubs' + +const routerPush = vi.hoisted(() => vi.fn()) +vi.mock('vue-router', () => ({ useRoute: () => ({ params: { dispatchNo: 'PC/1' } }), useRouter: () => ({ push: routerPush }) })) +vi.mock('../api/dispatches', () => ({ getDispatch: vi.fn() })) + +describe('DispatchDetailView', () => { + beforeEach(() => { + routerPush.mockReset() + vi.mocked(getDispatch).mockReset().mockResolvedValue({ + id: '1', dispatchNo: 'PC/1', vehiclePlate: 'CA-100', driverName: '张师傅', status: 'PENDING_DEPARTURE', + route: 'LAX → ONT8', items: [{ id: '11', planPalletId: '20', orderNo: 'LM-1', warehousePalletId: '21', fbaWarehouseCode: 'ONT8', volumeCbm: 1.25 }], + }) + }) + + it('loads the business dispatch number and renders summary and related orders', async () => { + const wrapper = mount(DispatchDetailView, { global: { plugins: [ElementPlus], stubs: { DataTable: DataTableStub } } }) + await flushPromises() + expect(getDispatch).toHaveBeenCalledWith('PC/1') + expect(wrapper.text()).toContain('PC/1') + expect(wrapper.text()).toContain('CA-100') + expect(wrapper.text()).toContain('待发车') + const table = wrapper.findComponent(DataTableStub) + expect(table.props('data')).toEqual([{ orderNo: 'LM-1', weight: '—', palletCount: 1, fbaWarehouse: 'ONT8', volume: 1.25, containerNo: '—' }]) + wrapper.unmount() + }) +}) diff --git a/src/modules/overseas-warehouse/pages/ExceptionOrdersView.test.ts b/src/modules/overseas-warehouse/pages/ExceptionOrdersView.test.ts new file mode 100644 index 00000000..b63191de --- /dev/null +++ b/src/modules/overseas-warehouse/pages/ExceptionOrdersView.test.ts @@ -0,0 +1,59 @@ +import ElementPlus from 'element-plus' +import { flushPromises, mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createExceptionOrder, listExceptionOrders, resolveExceptionRelation } from '../api/exceptionOrders' +import ExceptionOrdersView from './ExceptionOrdersView.vue' +import { commonPhase5Stubs } from './phase5TestStubs' + +const canPermission = vi.hoisted(() => vi.fn((_code: string) => false)) +vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canPermission }) })) +vi.mock('../api/exceptionOrders', () => ({ listExceptionOrders: vi.fn(), resolveExceptionRelation: vi.fn(), createExceptionOrder: vi.fn() })) + +function mountView() { return mount(ExceptionOrdersView, { global: { plugins: [ElementPlus], stubs: commonPhase5Stubs } }) } + +describe('ExceptionOrdersView permissions and creation', () => { + beforeEach(() => { + canPermission.mockReset().mockReturnValue(false) + vi.mocked(listExceptionOrders).mockReset().mockResolvedValue({ records: [], page: 1, size: 10, total: 0, pages: 0 }) + vi.mocked(resolveExceptionRelation).mockReset().mockResolvedValue({ inboundOrderId: '9', inboundCode: 'IN-1', lastMileOrderId: '8', lastMileOrderNo: 'LM-1', customerId: '100' }) + vi.mocked(createExceptionOrder).mockReset().mockResolvedValue({ id: '19', exceptionNo: 'EXC-1', status: 'PENDING' }) + }) + + it('hides creation without the independent permission', async () => { + const wrapper = mountView() + await flushPromises() + expect(wrapper.find('[data-testid="data-table"]').text()).not.toContain('新增异常订单') + wrapper.unmount() + }) + + it('validates the required relation before submitting', async () => { + canPermission.mockImplementation(code => code.endsWith(':create')) + const wrapper = mountView() + await flushPromises() + await wrapper.findAll('button').find(button => button.text().includes('新增异常订单'))!.trigger('click') + await wrapper.find('[data-dialog="新增异常订单"] .dialog-confirm').trigger('click') + await flushPromises() + expect(resolveExceptionRelation).not.toHaveBeenCalled() + expect(createExceptionOrder).not.toHaveBeenCalled() + wrapper.unmount() + }) + + it('resolves and submits trimmed business fields', async () => { + canPermission.mockImplementation(code => code.endsWith(':create')) + const wrapper = mountView() + await flushPromises() + await wrapper.findAll('button').find(button => button.text().includes('新增异常订单'))!.trigger('click') + const dialog = wrapper.find('[data-dialog="新增异常订单"]') + await dialog.find('input[placeholder="请输入入仓码"]').setValue(' IN-1 ') + await dialog.find('select').setValue('RETURN') + await dialog.find('textarea').setValue(' 包裹破损 ') + await dialog.find('.dialog-confirm').trigger('click') + await flushPromises() + expect(resolveExceptionRelation).toHaveBeenCalledWith({ customerId: undefined, inboundCode: 'IN-1', lastMileOrderNo: undefined }) + expect(createExceptionOrder).toHaveBeenCalledWith({ + customerId: undefined, inboundCode: 'IN-1', lastMileOrderNo: 'LM-1', + exceptionType: 'RETURN', reason: '包裹破损', packageCount: undefined, skuCode: undefined, + }) + wrapper.unmount() + }) +}) diff --git a/src/modules/overseas-warehouse/pages/phase5TestStubs.ts b/src/modules/overseas-warehouse/pages/phase5TestStubs.ts new file mode 100644 index 00000000..26e9ca6b --- /dev/null +++ b/src/modules/overseas-warehouse/pages/phase5TestStubs.ts @@ -0,0 +1,83 @@ +import { defineComponent, h, type Component, type PropType } from 'vue' + +export const DataTableStub = defineComponent({ + name: 'DataTable', + props: { data: { type: Array as PropType, default: () => [] } }, + setup(props, { slots }) { + return () => h('div', { 'data-testid': 'data-table' }, [ + slots['toolbar-actions']?.(), + ...props.data.flatMap(row => slots.actions?.({ row }) || []), + ]) + }, +}) + +export const AppDialogStub = defineComponent({ + name: 'AppDialog', + props: { title: { type: String, required: true }, confirmText: { type: String, default: '确定' } }, + emits: ['confirm'], + setup(props, { emit, slots }) { + return () => h('section', { 'data-dialog': props.title }, [ + slots.default?.(), + h('button', { class: 'dialog-confirm', onClick: () => emit('confirm') }, props.confirmText), + ]) + }, +}) + +export const ModelInputStub = defineComponent({ + name: 'ElInput', + inheritAttrs: false, + props: { modelValue: { type: [String, Number], default: '' }, type: { type: String, default: 'text' } }, + emits: ['update:modelValue', 'blur'], + setup(props, { attrs, emit }) { + return () => h(props.type === 'textarea' ? 'textarea' : 'input', { + ...attrs, + value: props.modelValue, + onInput: (event: Event) => emit('update:modelValue', (event.target as HTMLInputElement).value), + onBlur: () => emit('blur'), + }) + }, +}) + +export const ModelSelectStub = defineComponent({ + name: 'ElSelect', + props: { modelValue: { type: [String, Number], default: '' } }, + emits: ['update:modelValue'], + setup(props, { emit, slots }) { + return () => h('select', { + value: props.modelValue, + onChange: (event: Event) => emit('update:modelValue', (event.target as HTMLSelectElement).value), + }, slots.default?.()) + }, +}) + +export const OptionStub = defineComponent({ + name: 'ElOption', + props: { value: { type: [String, Number], required: true }, label: { type: String, required: true } }, + setup(props) { return () => h('option', { value: props.value }, props.label) }, +}) + +export const ModelNumberStub = defineComponent({ + name: 'ElInputNumber', + props: { modelValue: { type: Number, default: undefined } }, + emits: ['update:modelValue'], + setup(props, { emit }) { + return () => h('input', { + type: 'number', + value: props.modelValue, + onInput: (event: Event) => { + const value = (event.target as HTMLInputElement).value + emit('update:modelValue', value === '' ? undefined : Number(value)) + }, + }) + }, +}) + +export const commonPhase5Stubs: Record = { + DataTable: DataTableStub, + AppDialog: AppDialogStub, + QueryToolbar: defineComponent({ name: 'QueryToolbar', setup: () => () => h('div') }), + ElInput: ModelInputStub, + ElSelect: ModelSelectStub, + ElOption: OptionStub, + ElInputNumber: ModelNumberStub, +}