141 lines
5.7 KiB
JavaScript
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()
|
|
})
|
|
})
|