Files
2026-09-21 23:13:14 +08:00

141 lines
5.7 KiB
JavaScript

/**
* 缺省页:404(未知路径)/ 403(无权限)/ 500(渲染错误边界)/ 离线(断网覆盖层)。
* 挂载真实 App(布局、路由、store 全真,页面用桩替换),覆盖:
* 1. 未知路径渲染 404 页并保留原地址(独立布局,未登录也可达)
* 2. 无权限访问模块页由守卫跳到 /403 并在布局内渲染(保留侧栏 / 页签导航)
* 3. 直接访问 /500 在布局内渲染
* 4. 断网事件显示离线覆盖层,恢复后移除;覆盖层期间原页面不卸载
* 5. ErrorBoundary 捕获子树渲染错误时就地渲染 500 页,不出白屏
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import { nextTick } from 'vue'
vi.mock('@/views/dashboard/index.vue', () => ({
default: { name: 'Dashboard', template: '<div data-page="Dashboard">DB</div>' },
}))
vi.mock('@/views/module/module-management/index.vue', () => ({
default: { name: 'ModuleManagement', template: '<div data-page="ModuleManagement">MM</div>' },
}))
vi.mock('@/views/detail/DetailPage.vue', () => ({
default: { name: 'DetailPageStub', props: ['id'], template: '<div data-page="Detail" />' },
}))
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 ErrorBoundary from '@/views/fallback/ErrorBoundary.vue'
import router from '@/router'
import { createTestPinia } from '../helpers/app.js'
import { useAuthStore } from '@/stores/auth'
beforeEach(async () => {
localStorage.clear()
sessionStorage.clear()
createTestPinia()
// 普通账号(非 g3soft):用于验证「无权限访问模块中心 → 403」
useAuthStore().setSession({ token: 't', user: { id: 1, account: 'user1', name: 'U' } }, 'ORG')
await router.push('/dashboard')
await router.isReady()
})
afterEach(() => {
// 离线用例会把 navigator.onLine 改写为 false,恢复默认值避免串扰
Object.defineProperty(window.navigator, 'onLine', { value: true, configurable: true })
})
async function mountApp() {
const wrapper = mount(App, { global: { plugins: [router] }, attachTo: document.body })
await flushPromises()
return wrapper
}
describe('缺省页', () => {
it('未知路径:渲染 404 页并保留原地址', async () => {
const wrapper = await mountApp()
await router.push('/no-such-page')
await flushPromises()
// 不重定向:地址保持原样,用户能看到自己输错的是什么
expect(router.currentRoute.value.path).toBe('/no-such-page')
expect(wrapper.find('.fms-fallback').exists()).toBe(true)
expect(wrapper.text()).toContain('页面不存在')
// 独立布局:不挂 DefaultLayout(未知路径与登录态无关,未登录也能看)
expect(wrapper.find('.fms-topbar').exists()).toBe(false)
wrapper.unmount()
})
it('无权限访问模块中心:守卫跳 /403,并在布局内渲染(保留导航)', async () => {
const wrapper = await mountApp()
await router.push('/module/module-management')
await flushPromises()
expect(router.currentRoute.value.path).toBe('/403')
expect(wrapper.find('.fms-fallback').exists()).toBe(true)
expect(wrapper.text()).toContain('无权访问')
// 缺省页挂在 DefaultLayout 内:顶栏 / 页签栏都在,用户可直接切走
expect(wrapper.find('.fms-topbar').exists()).toBe(true)
expect(wrapper.find('.fms-tabs').exists()).toBe(true)
wrapper.unmount()
})
it('直接访问 /500:在布局内渲染', async () => {
const wrapper = await mountApp()
await router.push('/500')
await flushPromises()
expect(wrapper.find('.fms-fallback').exists()).toBe(true)
expect(wrapper.text()).toContain('页面出错了')
expect(wrapper.find('.fms-topbar').exists()).toBe(true)
wrapper.unmount()
})
it('断网:显示离线覆盖层,恢复后移除且原页面不卸载', async () => {
const wrapper = await mountApp()
expect(wrapper.find('[data-page="Dashboard"]').exists()).toBe(true)
expect(wrapper.find('.fms-app-offline').exists()).toBe(false)
Object.defineProperty(window.navigator, 'onLine', { value: false, configurable: true })
window.dispatchEvent(new Event('offline'))
await nextTick()
expect(wrapper.find('.fms-app-offline').exists()).toBe(true)
expect(wrapper.text()).toContain('网络已断开')
// 覆盖层方案:原页面仍在 DOM 中(表单状态 / 滚动位置保留)
expect(wrapper.find('[data-page="Dashboard"]').exists()).toBe(true)
Object.defineProperty(window.navigator, 'onLine', { value: true, configurable: true })
window.dispatchEvent(new Event('online'))
await nextTick()
expect(wrapper.find('.fms-app-offline').exists()).toBe(false)
wrapper.unmount()
})
it('ErrorBoundary:子树渲染错误时就地渲染 500 页,不出白屏', async () => {
// 错误是测试故意触发的:静默 Vue / 边界自身的 console.error 输出
const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
const Boom = {
name: 'Boom',
setup() {
throw new Error('boom')
},
}
const wrapper = mount(ErrorBoundary, {
global: { plugins: [router] },
slots: { default: Boom },
})
await flushPromises()
expect(wrapper.find('.fms-fallback').exists()).toBe(true)
expect(wrapper.text()).toContain('页面出错了')
wrapper.unmount()
spy.mockRestore()
})
})