import ElementPlus from 'element-plus' import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router' 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 InboundOrdersView from './InboundOrdersView.vue' import InboundOrderDetailView from './InboundOrderDetailView.vue' import FbaAppointmentFields from '../components/FbaAppointmentFields.vue' import { createAppointmentForm, type AppointmentForm } from '../appointmentForm' import { createFbaAppointment, getFbaAppointment, listInboundOrders, updateFbaAppointment, updateInboundPlan, type FbaAppointmentDto, type InboundOrderSummaryDto } from '../api/inboundOrders' import { getInboundPlan, moveInboundPallet, type InboundPlanDetailDto } from '../api/inboundPlans' import { listReceivingOverseasWarehouses, type ReceivingOverseasWarehouseDto } from '../api/pallets' import { listOverseasLocations, type OverseasLocationDto } from '../api/locations' import { overseasWarehousePermissions as permission } from '../permissions' const can = vi.hoisted(() => vi.fn((_code: string) => true)) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) })) vi.mock('vue-router', () => ({ useRoute: () => ({ params: { orderId: 'IP-1' }, query: { customerId: 'C-1' } }), useRouter: () => ({ push: vi.fn() }), onBeforeRouteLeave: vi.fn(), onBeforeRouteUpdate: vi.fn() })) vi.mock('@/modules/appointment-calendar', () => ({ appointmentReturnTarget: () => null, AppointmentConfirmationCargo: { name: 'AppointmentConfirmationCargo', mounted() { this.$emit('ready', true) }, methods: { canLeave: async () => true }, template: '
' } })) vi.mock('../components/SmartDispatchDialog.vue', () => ({ default: { name: 'SmartDispatchDialog', template: '' } })) vi.mock('../api/inboundOrders', () => ({ createFbaAppointment: vi.fn(), getFbaAppointment: vi.fn(), listInboundOrders: vi.fn(), updateFbaAppointment: vi.fn(), updateInboundPlan: vi.fn() })) vi.mock('../api/inboundPlans', () => ({ getInboundPlan: vi.fn(), moveInboundPallet: vi.fn() })) vi.mock('../api/pallets', () => ({ listReceivingOverseasWarehouses: vi.fn() })) vi.mock('../api/locations', () => ({ listOverseasLocations: vi.fn() })) enableAutoUnmount(afterEach) const order: InboundOrderSummaryDto = { lineId: '9223372036854775001', planId: 'internal-plan', planNo: 'IP-1', customerId: 'C-1', orderId: 'internal-order', status: 'PENDING_ASSIGNMENT', overseasWarehouseId: 'WH-1', overseasWarehouseCode: 'LA1', overseasWarehouseName: '洛杉矶一仓', warehouseId: 'WH-1', warehouseCode: 'LA1', warehouseName: '洛杉矶一仓', fbaWarehouseCode: 'ONT8', appointmentNo: 'AP-1', inboundCode: 'IN-1', version: 0 } const warehouse: ReceivingOverseasWarehouseDto = { id: '9223372036854775002', customerId: '0', warehouseCode: 'LA2', warehouseName: '洛杉矶二仓', warehouseType: 'OVERSEAS', status: 'ACTIVE' } const appointment: FbaAppointmentDto = { id: 'internal-appointment', appointmentNo: 'AP-1', version: 0, originWarehouseId: 'WH-1', fbaWarehouseCode: 'ONT8', appointmentStatus: 'PENDING', selectedCount: 1, timezone: 'America/Los_Angeles', items: [] } const locations: OverseasLocationDto[] = [ { id: 'normal', warehouseId: 'WH-1', locationCode: 'N01', locationType: 'NORMAL', sharedOverflow: false, status: 'ACTIVE', version: 1 }, { id: 'overflow', warehouseId: 'WH-1', locationCode: 'A0', locationType: 'SHARED_OVERFLOW', sharedOverflow: true, status: 'ACTIVE', version: 0 }, { id: 'virtual', warehouseId: 'WH-1', locationCode: 'VIRTUAL', locationType: 'VIRTUAL', sharedOverflow: false, status: 'ACTIVE', version: 1 }, { id: 'disabled', warehouseId: 'WH-1', locationCode: 'DISABLED', locationType: 'NORMAL', sharedOverflow: false, status: 'DISABLED', version: 1 }, ] function plan(): InboundPlanDetailDto { return { id: 'internal-plan', planNo: 'IP-1', customerId: 'C-1', sourceOrderId: 'internal-order', overseasWarehouseId: 'WH-1', overseasWarehouseCode: 'LA1', overseasWarehouseName: '洛杉矶一仓', lineCount: 1, palletCount: 1, totalGrossWeightKg: 10, totalNetWeightKg: 8, totalVolumeCbm: 1, lines: [{ id: 'line', orderId: 'internal-order', inboundCode: 'IN-1', requiredPackageCount: 2, plannedPalletCount: 1 }], pallets: [{ id: 'pallet', palletNo: 'PALLET-1', sequenceNo: 1, warehouseLocationCode: 'OLD', actualPackageCount: 2, version: 0 }], allocations: [{ lineId: 'line', palletId: 'pallet', palletNo: 'PALLET-1', palletOwnerPlanNo: 'IP-1', palletOwnedByCurrentPlan: true, plannedPackageCount: 2, countedPackageCount: 2, pickedPackageCount: 2, version: 0 }], } } beforeEach(() => { vi.resetAllMocks(); can.mockReturnValue(true) vi.mocked(listInboundOrders).mockResolvedValue({ records: [{ ...order }], total: 1, page: 1, size: 10, pages: 1 }) vi.mocked(getFbaAppointment).mockResolvedValue({ ...appointment }) vi.mocked(createFbaAppointment).mockResolvedValue({ ...appointment }) vi.mocked(updateFbaAppointment).mockResolvedValue({ ...appointment }) vi.mocked(updateInboundPlan).mockResolvedValue({ ...order }) vi.mocked(listReceivingOverseasWarehouses).mockResolvedValue({ records: [warehouse], total: 1, page: 1, size: 200, pages: 1 }) vi.mocked(getInboundPlan).mockResolvedValue(plan()) vi.mocked(listOverseasLocations).mockResolvedValue({ records: locations, total: 4, page: 1, size: 200, pages: 1 }) vi.mocked(moveInboundPallet).mockResolvedValue({ commandId: 'command', status: 'COMPLETED', canMove: true }) }) async function listPage() { const wrapper = mount(InboundOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } }); await flushPromises(); return wrapper } function popup(wrapper: VueWrapper, title: string) { return wrapper.findAllComponents(AppDialog).find(dialog => dialog.props('title').startsWith(title))! } async function openEditPlan() { const wrapper = await listPage(); await wrapper.findAll('.el-table__body-wrapper button').find(button => button.text() === '编辑')!.trigger('click'); await flushPromises(); return { wrapper, dialog: popup(wrapper, '编辑入库单') } } async function openEditAppointment() { const wrapper = await listPage(); await wrapper.findAll('.el-table__body-wrapper button').find(button => button.text() === '预约')!.trigger('click'); await flushPromises(); return { wrapper, dialog: popup(wrapper, '编辑预约') } } async function openMove() { const wrapper = mount(InboundOrderDetailView, { attachTo: document.body, global: { plugins: [ElementPlus] } }); await flushPromises(); await wrapper.findAll('.el-table__body-wrapper button').find(button => button.text() === '移库')!.trigger('click'); await flushPromises(); return { wrapper, dialog: popup(wrapper, '托盘移库') } } describe('inbound appointment native dialogs', () => { it('keeps every confirmed appointment field through fullscreen and submits the exact selected origin and lines', async () => { vi.mocked(listInboundOrders).mockResolvedValue({ records: [{ ...order, appointmentNo: null }], total: 1, page: 1, size: 10, pages: 1 }) const wrapper = await listPage() const table = wrapper.findComponent(DataTable) table.vm.$emit('selection-change', table.props('data')); await flushPromises() await wrapper.findAll('button').find(button => button.text() === '预约')!.trigger('click'); await flushPromises() const dialog = popup(wrapper, '预约 ·') const fields = dialog.getComponent(FbaAppointmentFields) const model = fields.props('model') model.appointmentStatus = 'SUCCESS'; await flushPromises() dialog.vm.$emit('confirm'); await flushPromises(); expect(createFbaAppointment).not.toHaveBeenCalled() Object.assign(model, { plannedDate: '2026-11-01', plannedAt: '2026-11-01T09:00:00', plannedEndAt: '2026-11-01T11:00:00', confirmationNo: ' EXT-1 ', appointmentAt: '2026-11-01T01:00:00', appointmentEndAt: '2026-11-01T01:30:00', appointmentRemark: ' 约仓确认 ', startOffset: '-07:00', endOffset: '-08:00' }) await flushPromises(); await dialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises() expect(fields.props('continuous')).toBe(true) expect(dialog.get('[aria-label="预约来源信息"]').text()).toContain('保存后生成') expect(fields.findAll('details')).toHaveLength(1) dialog.vm.$emit('confirm'); await flushPromises() expect(createFbaAppointment).toHaveBeenCalledExactlyOnceWith({ originWarehouseId: 'WH-1', fbaWarehouseCode: 'ONT8', lineIds: [order.lineId], idempotencyKey: expect.any(String), details: { plannedDate: '2026-11-01', plannedAt: '2026-11-01T09:00:00', plannedEndAt: '2026-11-01T11:00:00', confirmationNo: 'EXT-1', appointmentAt: '2026-11-01T01:00:00', appointmentEndAt: '2026-11-01T01:30:00', appointmentStatus: 'SUCCESS', appointmentRemark: '约仓确认', startOffset: '-07:00', endOffset: '-08:00', } }) }) it('locks an uncertain edit to the original zero-version request and keeps close and route guards intact', async () => { let reject!: (reason: unknown) => void vi.mocked(updateFbaAppointment).mockReturnValueOnce(new Promise((_, no) => { reject = no })).mockResolvedValueOnce({ ...appointment }) const { dialog } = await openEditAppointment() const fields = dialog.getComponent(FbaAppointmentFields) fields.props('model').appointmentRemark = '交接备注'; await flushPromises() dialog.vm.$emit('confirm'); dialog.vm.$emit('confirm'); await flushPromises() expect(updateFbaAppointment).toHaveBeenCalledTimes(1) const original = vi.mocked(updateFbaAppointment).mock.calls[0]! expect(original[1]).toMatchObject({ version: 0, idempotencyKey: expect.any(String) }) expect(dialog.findComponent({ name: 'ElDialog' }).props()).toMatchObject({ showClose: false, closeOnPressEscape: false }) reject(new ApiResponseError('结果暂未确认', 'A0503', 503, null, 'appointment-popup-request')); await flushPromises() expect(fields.props('disabled')).toBe(true) expect(dialog.text()).toContain('appointment-popup-request') await dialog.findAll('button').find(button => button.text() === '取消')!.trigger('click'); await flushPromises() expect(dialog.props('modelValue')).toBe(true) for (const hook of [onBeforeRouteLeave, onBeforeRouteUpdate]) expect(await (vi.mocked(hook).mock.calls[0]![0] as () => Promise