Files
workspace/code/fms/fms-vue/tests/views/nav-active-sequence.spec.js
T
2026-09-21 23:13:14 +08:00

133 lines
4.8 KiB
JavaScript

/**
* 诊断:侧栏菜单与页签的选中态(is-active)翻转序列。
*
* 背景:在"其他数据 / 联系人"之间来回切换时,旧项的选中背景会「再亮一次」再灭。
* 本用例用 MutationObserver 记录完整的类变化序列,用于二分定位:
* - 状态层问题(类被加回):序列出现多余的翻转 → 需要修导航/渲染时序
* - 序列干净(每项只按导航翻转):闪烁来自 CSS 过渡的视觉 → 需要调过渡
*/
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
// 顶级菜单(不经目录):目录的展开状态只在 NavMenuItem 初始化时计算,
// 初始路由 /dashboard 不在目录下 → 目录收起 → 子项不渲染,这里用顶级项保证可见
const MENU_ROWS = [
{
b_id: 'm1',
b_name: '其他数据',
b_menu_type: 'page',
b_canuse: 1,
b_xh: 1,
b_route: '/base/otherdata',
b_parent_id: '',
},
{
b_id: 'm2',
b_name: '联系人',
b_menu_type: 'page',
b_canuse: 1,
b_xh: 2,
b_route: '/base/contact',
b_parent_id: '',
},
]
vi.mock('@/views/dashboard/index.vue', () => ({
default: { name: 'Dashboard', template: '<div data-page="Dashboard" />' },
}))
vi.mock('@/views/base/otherdata/index.vue', () => ({
default: { name: 'BaseOtherdata', template: '<div data-page="Otherdata" />' },
}))
vi.mock('@/views/base/contact/index.vue', () => ({
default: { name: 'BaseContact', template: '<div data-page="Contact" />' },
}))
vi.mock('@/views/module/module-management/index.vue', () => ({
default: { name: 'ModuleManagement', template: '<div data-page="MM" />' },
}))
vi.mock('@/views/module/menu-management/index.vue', () => ({
default: { name: 'MenuManagement', template: '<div data-page="MenuM" />' },
}))
vi.mock('@/views/detail/DetailPage.vue', () => ({
default: { name: 'DetailPageStub', props: ['id'], template: '<div />' },
}))
vi.mock('@/services/api', () => ({
loadDataApi: vi.fn((table) =>
Promise.resolve({ data: table === 's_menu' ? MENU_ROWS : [], 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()
})
async function mountApp() {
const wrapper = mount(App, { global: { plugins: [router] }, attachTo: document.body })
await flushPromises()
return wrapper
}
describe('选中态翻转序列(闪烁诊断)', () => {
it('其他数据/联系人来回切换:is-active 序列无多余翻转', async () => {
const wrapper = await mountApp()
await router.push('/base/otherdata')
await flushPromises()
const navItem = (name) =>
Array.from(document.querySelectorAll('.fms-nav-link')).find((el) => el.textContent.includes(name))
expect(navItem('其他数据')).toBeTruthy()
const read = () => ({
other: navItem('其他数据')?.classList.contains('is-active') ? 1 : 0,
contact: navItem('联系人')?.classList.contains('is-active') ? 1 : 0,
})
const flips = []
let last = JSON.stringify(read())
const mo = new MutationObserver(() => {
const now = JSON.stringify(read())
if (now !== last) {
flips.push(now)
last = now
}
})
mo.observe(document.body, { subtree: true, attributes: true, attributeFilter: ['class'] })
// 场景 1:慢速来回(每次等导航完成)—— 期望恰好 2 次翻转
await router.push('/base/contact')
await flushPromises()
await router.push('/base/otherdata')
await flushPromises()
// 场景 2:快速来回(不等前一个导航完成)—— 模拟用户快速连点
router.push('/base/contact')
router.push('/base/otherdata')
router.push('/base/contact')
await flushPromises()
mo.disconnect()
console.log('[诊断] is-active 翻转序列:', flips.join(' -> '))
// 最终态:最后 push 的是联系人
const final = read()
expect(final.contact).toBe(1)
expect(final.other).toBe(0)
// 场景 1 恰好 2 次翻转;场景 2 的快速连点可能合并为至多 3 次 → 上限 5。
// 若状态层存在「旧项再亮一次」(类被加回),翻转次数会超过此上限并在这里变红
expect(flips.length).toBeLessThanOrEqual(5)
wrapper.unmount()
})
})