import { createRequire } from 'node:module' import { mount, flushPromises, enableAutoUnmount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import AppDialog from '@/shared/components/AppDialog.vue' import { ApiResponseError, http } from '@/shared/api/http' import View from './PreloadPlanDetailView.vue' import { confirmPreloadPlan, getPreloadPlan, listDomesticPdaOperators, updateLoadingItemRemarks, updatePreloadTransport, type LoadingDetail, type PreloadSummary } from '../api/platformLoadingLists' // Use the repository's supported CJS entry for the real async form validator. const elementPlus = createRequire(import.meta.url)('element-plus') as typeof import('element-plus') const ElementPlus = elementPlus.default const { ElInput, ElSelect } = elementPlus vi.mock('vue-router', () => ({ useRoute: () => ({ params: { planId: '1900000000000000001' }, query: { warehouseId: '1900000000000000002' } }), useRouter: () => ({ replace: vi.fn(), push: vi.fn(), back: vi.fn() }), })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) vi.mock('@/core/workspace-tabs/workspaceTabs', () => ({ useWorkspaceTabsStore: () => ({ removeByIds: vi.fn() }) })) vi.mock('../api/platformLoadingLists', () => ({ confirmPreloadPlan: vi.fn(), getConfirmedLoadingList: vi.fn(), getPreloadPlan: vi.fn(), listDomesticPdaOperators: vi.fn(), updateLoadingItemRemarks: vi.fn(), updatePreloadTransport: vi.fn(), })) function fixture(containerNo: string | null = 'MSCU-OLD', version = 7, itemVersion = 3): LoadingDetail { return { summary: { planId: '1900000000000000001', containerBookingId: '2900000000000000001', preloadNo: 'PL-TEST-1', cabinetNo: 'CAB-TEST-1', containerNo, customers: [{ accountId: '11', customerName: '客户甲' }], warehouseId: '1900000000000000002', warehouseName: '国内仓', vesselName: 'SHIP', voyageNo: 'V1', containerType: '40HQ', status: 'EDITING', skuCount: '1', totalPackageCount: '10', totalVolumeCbm: 1, totalGrossWeightKg: 100, createdAt: '2026-10-19T10:00:00', version, readiness: { containerNoReady: Boolean(containerNo), preloadLinkCount: 1, confirmedPreloadCount: 0, oceanBookingId: '31', oceanBookingStatus: 'RELEASED', pdaTaskId: null, pdaTaskStatus: null, confirmationStatus: 'PENDING', canConfirm: Boolean(containerNo), readyForPda: false, blockingReasons: [] }, }, items: [{ itemId: '41', itemVersion, accountId: '11', customerName: '客户甲', customsTaxMode: 'TAX_REFUND', taxRefundFlag: true, orderId: '51', orderNo: 'ORD-TEST-1', inboundId: '61', inboundDetailId: '62', inboundCode: 'IN-TEST-1', containerNo, skuCode: null, destinationWarehouse: 'LAX9', chineseName: '测试货物', englishName: 'Goods', goodsImageUrl: null, unitType: 'PACKAGE', palletId: null, palletNo: null, indivisible: false, packageCount: 10, palletCount: 0, grossWeightKg: 100, netWeightKg: 90, volumeCbm: 1, dimensionalWeightKg: 167, productQuantity: '10', unitsPerPackage: 1, fbaTrackingNo: null, shipmentNo: null, shippingMark: null, internalRemark: '原内部备注', customerRemark: '客户内容\n第二行', warehouseRemark: '原仓库备注', customerServiceRemark: '原客服备注', loadSequence: 1, frozenAt: null, }], histories: [], } } async function render(initial = fixture()) { vi.mocked(getPreloadPlan).mockResolvedValue(initial) const wrapper = mount(View, { global: { plugins: [ElementPlus], stubs: { AdminPage: { template: '
' }, OperationErrorToast: true, LoadingListDetailDrawer: { name: 'LoadingListDetailDrawer', template: '
' }, } } }) await flushPromises() return wrapper } function dialog(wrapper: Awaited>, prefix: string) { return wrapper.findAllComponents(AppDialog).find(item => item.props('title').startsWith(prefix))! } enableAutoUnmount(afterEach) beforeEach(() => { vi.resetAllMocks() vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }) vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'success', data: { planId: '1900000000000000001', detailResource: 'PRELOAD', planStatus: 'EDITING' } }) vi.mocked(listDomesticPdaOperators).mockResolvedValue([{ userId: '71', displayName: '操作员甲' }]) }) afterEach(() => { vi.restoreAllMocks(); document.body.innerHTML = '' }) describe('预装清单统一弹窗', () => { it.each([null, 'MSCU-OLD'])('运输表保留规范化和纠错原因校验,原柜号=%s', async (original) => { const wrapper = await render(fixture(original)) vi.mocked(updatePreloadTransport).mockResolvedValue(fixture('MSCU-NEW', 8)) wrapper.getComponent({ name: 'LoadingListDetailDrawer' }).vm.$emit('edit') await flushPromises() const popup = dialog(wrapper, original ? '纠正' : '补录') expect(popup.props('title')).toContain('PL-TEST-1') const number = popup.findAllComponents(ElInput).find(input => input.props('placeholder') === '请输入提柜后取得的真实集装箱号')! number.vm.$emit('update:modelValue', ' mscu-new ') number.vm.$emit('blur', new FocusEvent('blur')) popup.findAllComponents(ElInput).find(input => input.props('placeholder') === '请输入航次')!.vm.$emit('update:modelValue', ' V2 ') if (original) { popup.vm.$emit('confirm'); await flushPromises() expect(updatePreloadTransport).not.toHaveBeenCalled() popup.findAllComponents(ElInput).find(input => input.props('type') === 'textarea')!.vm.$emit('update:modelValue', ' 更正柜号 ') } popup.vm.$emit('confirm'); await flushPromises() expect(updatePreloadTransport).toHaveBeenCalledWith('1900000000000000001', '1900000000000000002', { containerNo: 'MSCU-NEW', voyageNo: 'V2', operationReason: original ? '更正柜号' : null, expectedVersion: 7 }) expect(popup.props('modelValue')).toBe(false) }) it('客户备注只读,全屏及版本冲突后保留输入并按最新两层版本重试', async () => { const initial = fixture() const wrapper = await render(initial) wrapper.getComponent({ name: 'LoadingListDetailDrawer' }).vm.$emit('edit-remarks', initial.items[0]) await flushPromises() const popup = dialog(wrapper, '编辑备注') expect(popup.props('title')).toBe('编辑备注 · IN-TEST-1') const customer = popup.findAllComponents({ name: 'ElFormItem' }).find(field => field.props('label') === '客户备注(只读)')! expect(customer.text()).toContain('第二行') expect(customer.findComponent(ElInput).exists()).toBe(false) const fields = popup.findAllComponents(ElInput) fields[0]!.vm.$emit('update:modelValue', ' 新内部 ') fields[1]!.vm.$emit('update:modelValue', '') fields[2]!.vm.$emit('update:modelValue', '新客服') await popup.get('[aria-label="展开全屏"]').trigger('click') expect(fields[0]!.props('modelValue')).toBe(' 新内部 ') vi.mocked(getPreloadPlan).mockResolvedValue(fixture('MSCU-OLD', 8, 4)) vi.mocked(updateLoadingItemRemarks).mockRejectedValueOnce(new ApiResponseError('版本变化', 'A0409')).mockResolvedValueOnce({ id: initial.summary.planId, version: 9, items: [{ id: '41', version: 5, internalRemark: '新内部', warehouseRemark: null, customerServiceRemark: '新客服' }] }) popup.vm.$emit('confirm'); await flushPromises() expect(popup.props('modelValue')).toBe(true) expect(fields[0]!.props('modelValue')).toBe(' 新内部 ') popup.vm.$emit('confirm'); await flushPromises() const calls = vi.mocked(updateLoadingItemRemarks).mock.calls expect(calls[0]![2]).toMatchObject({ expectedPlanVersion: 7, expectedItemVersion: 3 }) expect(calls[1]).toEqual([initial.summary.planId, '41', { internalRemark: '新内部', warehouseRemark: null, customerServiceRemark: '新客服', expectedPlanVersion: 8, expectedItemVersion: 4, operationId: calls[0]![2].operationId }]) expect(popup.props('modelValue')).toBe(false) }) it('确认资料显示已选作业员,保留必选门禁和原推送操作号', async () => { const initial = fixture() const wrapper = await render(initial) const drawer = wrapper.getComponent({ name: 'LoadingListDetailDrawer' }) drawer.vm.$emit('confirm-preload'); await flushPromises() expect(dialog(wrapper, '推送PDA').props('modelValue')).toBe(false) wrapper.getComponent(ElSelect).vm.$emit('update:modelValue', '71') await flushPromises() drawer.vm.$emit('confirm-preload'); await flushPromises() const popup = dialog(wrapper, '推送PDA') expect(popup.get('table[aria-label="确认预装资料"]').text()).toContain('操作员甲') vi.mocked(confirmPreloadPlan).mockResolvedValue({ planId: initial.summary.planId, containerBookingId: initial.summary.containerBookingId, confirmationStatus: 'CONFIRMED', version: 8, readiness: { ...initial.summary.readiness, confirmationStatus: 'CONFIRMED', confirmedPreloadCount: 1, pdaTaskId: '91', pdaTaskStatus: 'PENDING', canConfirm: false, readyForPda: true } }) popup.vm.$emit('confirm'); await flushPromises() expect(confirmPreloadPlan).toHaveBeenCalledWith(initial.summary.planId, 7, '71', expect.stringContaining('admin-preload-confirm-')) expect(popup.props('modelValue')).toBe(false) }) })