/** * 诊断:侧栏菜单与页签的选中态(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: '
' }, })) vi.mock('@/views/base/otherdata/index.vue', () => ({ default: { name: 'BaseOtherdata', template: '
' }, })) vi.mock('@/views/base/contact/index.vue', () => ({ default: { name: 'BaseContact', template: '
' }, })) vi.mock('@/views/module/module-management/index.vue', () => ({ default: { name: 'ModuleManagement', template: '
' }, })) vi.mock('@/views/module/menu-management/index.vue', () => ({ default: { name: 'MenuManagement', template: '
' }, })) vi.mock('@/views/detail/DetailPage.vue', () => ({ default: { name: 'DetailPageStub', props: ['id'], template: '
' }, })) 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() }) })