import { flushPromises, shallowMount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' import { ElMessage } from 'element-plus' import { ApiResponseError, http } from '@/shared/api/http' import { getConfirmedLoadingList, getPreloadPlan } from '../api/platformLoadingLists' import PreloadPlanDetailView from './PreloadPlanDetailView.vue' const routerReplace = vi.hoisted(() => vi.fn()) const removeWorkspaceTabs = vi.hoisted(() => vi.fn()) vi.mock('vue-router', () => ({ useRoute: () => ({ params: { planId: '1900000000000000001' }, query: { warehouseId: '1900000000000000002' } }), useRouter: () => ({ replace: routerReplace, push: vi.fn(), back: vi.fn() }), })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) vi.mock('@/core/workspace-tabs/workspaceTabs', () => ({ useWorkspaceTabsStore: () => ({ removeByIds: removeWorkspaceTabs }) })) vi.mock('../api/platformLoadingLists', () => ({ confirmPreloadPlan: vi.fn(), getConfirmedLoadingList: vi.fn(), getPreloadPlan: vi.fn(), updateLoadingItemRemarks: vi.fn(), updatePreloadTransport: vi.fn(), })) async function mountView() { const wrapper = shallowMount(PreloadPlanDetailView, { global: { stubs: { AdminPage: { template: '
' }, LoadingListDetailDrawer: { name: 'LoadingListDetailDrawer', props: ['error', 'loading'], template: '
' }, OperationErrorToast: true, AppDialog: true, }, }, }) await flushPromises() return wrapper } function drawerError(wrapper: Awaited>) { return wrapper.findComponent({ name: 'LoadingListDetailDrawer' }).props('error') } function lifecycle(detailResource: 'PRELOAD' | 'LOADING_LIST', planStatus: string) { return { code: '200', message: 'success', data: { planId: '1900000000000000001', detailResource, planStatus } } } describe('PreloadPlanDetailView lifecycle handoff', () => { beforeEach(() => { vi.restoreAllMocks() vi.clearAllMocks() routerReplace.mockResolvedValue(undefined) }) it('replaces the current tab after authoritative lifecycle resolution', async () => { vi.spyOn(http, 'get').mockResolvedValue(lifecycle('LOADING_LIST', 'COMPLETED')) vi.mocked(getConfirmedLoadingList).mockResolvedValue({ summary: { containerBookingId: '2900000000000000001' }, items: [], histories: [] } as never) const message = vi.spyOn(ElMessage, 'success').mockImplementation(() => undefined as never) const wrapper = await mountView() expect(http.get).toHaveBeenCalledWith( '/platform/v1/first-leg/loading-plans/1900000000000000001/lifecycle', { params: { warehouseId: '1900000000000000002' } }, ) expect(getPreloadPlan).not.toHaveBeenCalled() expect(routerReplace).toHaveBeenCalledTimes(1) expect(routerReplace).toHaveBeenCalledWith({ name: 'domestic-container-detail', params: { containerId: '2900000000000000001' }, query: { tab: 'loading-list' }, }) expect(removeWorkspaceTabs).toHaveBeenCalledWith(['preload-plan-detail']) expect(routerReplace.mock.invocationCallOrder[0]).toBeLessThan(removeWorkspaceTabs.mock.invocationCallOrder[0]!) expect(message).toHaveBeenCalledWith('装柜已完成,已切换至装柜清单详情') expect(drawerError(wrapper)).toBeNull() expect(wrapper.findComponent({ name: 'LoadingListDetailDrawer' }).props('loading')).toBe(false) }) it('loads preload detail only while the lifecycle remains PRELOAD', async () => { vi.spyOn(http, 'get').mockResolvedValue(lifecycle('PRELOAD', 'LOADING')) vi.mocked(getPreloadPlan).mockResolvedValue({ summary: { status: 'LOADING', containerNo: null, readiness: { pdaTaskStatus: null }, }, items: [], histories: [], } as never) const wrapper = await mountView() expect(getPreloadPlan).toHaveBeenCalledWith('1900000000000000001', '1900000000000000002') expect(routerReplace).not.toHaveBeenCalled() expect(removeWorkspaceTabs).not.toHaveBeenCalled() expect(drawerError(wrapper)).toBeNull() }) it('shows A0404 only when lifecycle resolution confirms the plan is absent', async () => { vi.spyOn(http, 'get').mockRejectedValue(new ApiResponseError('装柜计划不存在', 'A0404', 404, undefined, 'request-lifecycle', 'trace-lifecycle')) const wrapper = await mountView() expect(getPreloadPlan).not.toHaveBeenCalled() expect(routerReplace).not.toHaveBeenCalled() expect(drawerError(wrapper)).toMatchObject({ code: 'A0404', requestId: 'request-lifecycle', traceId: 'trace-lifecycle', message: '装柜计划不存在' }) }) it.each([ ['A0403', 403, 'request-forbidden', 'trace-forbidden'], ['S0503', 503, 'request-unavailable', 'trace-unavailable'], ])('does not swallow a lifecycle %s error', async (code, status, requestId, traceId) => { vi.spyOn(http, 'get').mockRejectedValue(new ApiResponseError('装柜计划解析失败', code, status, undefined, requestId, traceId)) const wrapper = await mountView() expect(getPreloadPlan).not.toHaveBeenCalled() expect(routerReplace).not.toHaveBeenCalled() expect(drawerError(wrapper)).toMatchObject({ code, requestId, traceId }) }) it('surfaces a network failure without guessing from another detail endpoint', async () => { vi.spyOn(http, 'get').mockRejectedValue(new Error('Network Error')) const wrapper = await mountView() expect(getPreloadPlan).not.toHaveBeenCalled() expect(routerReplace).not.toHaveBeenCalled() expect(drawerError(wrapper)).toMatchObject({ message: '预装清单详情加载失败,请稍后重试' }) }) it('does not redirect for an internally inconsistent lifecycle payload', async () => { vi.spyOn(http, 'get').mockResolvedValue(lifecycle('LOADING_LIST', 'LOADING')) const wrapper = await mountView() expect(routerReplace).not.toHaveBeenCalled() expect(drawerError(wrapper)).toMatchObject({ code: 'MODE_STATUS_MISMATCH' }) }) })