/** * 回归:路由过渡的页面外壳(.fms-page-holder,由 getPageWrapper 的渲染函数创建)。 * * 路由页面是异步组件(懒加载 chunk),加载中的渲染是注释节点(非元素根)—— * 无法动画化非元素根:既报 warning(Component inside * renders non-element root node that cannot be animated),out-in 下 enter 也会失败、 * 页面内容一直不显示。页面外壳是恒定元素,兜住该场景。 * * 本用例用「先渲染 null 再渲染元素」的桩模拟异步组件加载过程,断言: * 1. 全程不出现 non-element root 警告; * 2. 异步就绪后内容正常显示(过渡没有吞掉页面)。 */ import { describe, it, expect, beforeEach, vi } from 'vitest' import { flushPromises, mount } from '@vue/test-utils' vi.mock('@/views/dashboard/index.vue', async () => { const { h, onMounted, ref } = await import('vue') return { default: { name: 'Dashboard', setup() { const ready = ref(false) onMounted(() => { setTimeout(() => { ready.value = true }, 30) }) // 加载中渲染 null(Vue 渲染为注释节点),模拟异步组件解析前的状态 return () => (ready.value ? h('div', { 'data-page': 'Dashboard' }, 'DB') : null) }, }, } }) vi.mock('@/views/detail/DetailPage.vue', () => ({ default: { name: 'DetailPageStub', props: ['id'], template: '
' }, })) vi.mock('@/services/api', () => ({ loadDataApi: vi.fn(() => Promise.resolve({ data: [], code: 0, success: true })), describeApi: vi.fn(() => Promise.resolve({ data: [], code: 0, success: true })), saveObjectApi: vi.fn(() => Promise.resolve({ code: 0, success: true })), nextIdApi: vi.fn(() => Promise.resolve({ data: 1, code: 0, success: true })), })) import App from '@/App.vue' import router from '@/router' import { createTestPinia } from '../helpers/app.js' import { useAuthStore } from '@/stores/auth' beforeEach(async () => { localStorage.clear() sessionStorage.clear() createTestPinia() useAuthStore().setSession({ token: 't', user: { id: 1, account: 'g3soft', name: 'U' } }, 'ORG') await router.push('/dashboard') await router.isReady() }) describe('路由过渡 · 页面外壳', () => { it('异步页面加载中不产生 non-element root 警告,完成后内容正常显示', async () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) const wrapper = mount(App, { global: { plugins: [router] }, attachTo: document.body }) await flushPromises() // 等待桩组件异步就绪(模拟 chunk 下载完成) await new Promise((resolve) => setTimeout(resolve, 60)) await flushPromises() const warnings = warnSpy.mock.calls.map((args) => String(args[0])).join('\n') expect(warnings).not.toContain('non-element root node') // 内容最终可见(过渡未吞掉页面) expect(wrapper.find('[data-page="Dashboard"]').exists()).toBe(true) warnSpy.mockRestore() wrapper.unmount() }) })