/** * 其他数据页:左侧模块树的子树裁剪。 * * 本页只显示挂在根模块 `base_otherdata` 之下的子孙,不显示 s_module 里的其他模块。 * 这段裁剪原先按 b_path 前缀匹配实现,现已统一到 @/utils/tree 的 collectDescendantIds * (按 b_parent_id 展开)—— 与同一函数里向上回溯祖先所用的权威字段保持一致。 * 用例按「行为」而非「实现」编写,因此两种实现都应通过。 */ import { describe, it, expect, beforeEach, vi } from 'vitest' import { flushPromises } from '@vue/test-utils' import Otherdata from '@/views/base/otherdata/index.vue' import { mountPage } from '../helpers/app.js' vi.mock('@/services/api', () => ({ loadDataApi: vi.fn(), pageDataApi: vi.fn(() => Promise.resolve({ data: { rows: [], total: 0 } })), saveObjectApi: vi.fn(() => Promise.resolve({ code: 0, success: true })), nextIdApi: vi.fn(() => Promise.resolve({ data: 9001, code: 0, success: true })), describeApi: vi.fn(() => Promise.resolve({ data: [], code: 0, success: true })), })) // 权限:分类维护权限开启 → 不做可见性裁剪,只验证子树范围本身 vi.mock('@/stores/permissions', () => ({ usePermissionStore: () => ({ load: () => Promise.resolve(), canAccess: () => true, canPower: () => true, }), })) import { loadDataApi } from '@/services/api' /** 造一条 s_module 行;默认启用、非 virtual */ function makeModule(over = {}) { return { b_id: 'x', b_parent_id: null, b_depth: 0, b_path: '/x/', b_name: 'X', b_module_type: 'data', b_xh: 10, b_canuse: 1, ...over, } } /** * 固定夹具: * base_otherdata(根) * ├─ odc_1(分类) * │ └─ od_1(数据模块) * └─ od_2(数据模块) * other_root(完全无关的模块,不应出现在本页) */ const FIXTURE = [ makeModule({ b_id: 'base_otherdata', b_parent_id: null, b_depth: 0, b_path: '/base_otherdata/', b_module_type: 'module', }), makeModule({ b_id: 'odc_1', b_parent_id: 'base_otherdata', b_depth: 1, b_path: '/base_otherdata/odc_1/', b_module_type: 'module', }), makeModule({ b_id: 'od_1', b_parent_id: 'odc_1', b_depth: 2, b_path: '/base_otherdata/odc_1/od_1/', b_module_type: 'data', }), makeModule({ b_id: 'od_2', b_parent_id: 'base_otherdata', b_depth: 1, b_path: '/base_otherdata/od_2/', b_module_type: 'data', }), makeModule({ b_id: 'other_root', b_parent_id: null, b_depth: 0, b_path: '/other_root/', b_module_type: 'module', }), ] beforeEach(() => { localStorage.clear() sessionStorage.clear() loadDataApi.mockReset() loadDataApi.mockImplementation(() => Promise.resolve({ data: FIXTURE.map((row) => ({ ...row })) }), ) }) async function mountOtherdata() { const { wrapper } = await mountPage(Otherdata) await flushPromises() return wrapper } describe('其他数据:只显示根模块之下的子孙', () => { it('包含根的各层级子孙,排除根自身', async () => { const wrapper = await mountOtherdata() const ids = wrapper.vm.modules.map((item) => String(item.b_id)).toSorted() // 根自身不进树(它是本页入口,不是可导航节点) expect(ids).toEqual(['od_1', 'od_2', 'odc_1']) }) it('排除与根无关的其他模块', async () => { const wrapper = await mountOtherdata() const ids = wrapper.vm.modules.map((item) => String(item.b_id)) expect(ids).not.toContain('other_root') }) it('virtual 类型即使挂在根下也不进树', async () => { loadDataApi.mockImplementation(() => Promise.resolve({ data: [ ...FIXTURE.map((row) => ({ ...row })), makeModule({ b_id: 'od_virtual', b_parent_id: 'base_otherdata', b_depth: 1, b_path: '/base_otherdata/od_virtual/', b_module_type: 'virtual', }), ], }), ) const wrapper = await mountOtherdata() const ids = wrapper.vm.modules.map((item) => String(item.b_id)) expect(ids).not.toContain('od_virtual') }) it('b_path 与 b_parent_id 不一致时以 b_parent_id 为准(b_path 只是派生字段)', async () => { // 回归用例:裁剪原先按 b_path 前缀匹配,而同一函数里向上回溯祖先用 b_parent_id。 // 这条数据的 b_path 是过期的(写着挂在别的根下),但 b_parent_id 正确指向本页根, // 应当被算作根的子孙 —— 否则会和祖先回溯算出矛盾结果。 loadDataApi.mockImplementation(() => Promise.resolve({ data: [ ...FIXTURE.map((row) => ({ ...row })), makeModule({ b_id: 'od_moved', b_parent_id: 'base_otherdata', b_path: '/some_stale_path/od_moved/', }), ], }), ) const wrapper = await mountOtherdata() const ids = wrapper.vm.modules.map((item) => String(item.b_id)) expect(ids).toContain('od_moved') }) it('根不存在时树为空,不退化成显示全部模块', async () => { loadDataApi.mockImplementation(() => Promise.resolve({ data: [makeModule({ b_id: 'other_root', b_path: '/other_root/' })] }), ) const wrapper = await mountOtherdata() expect(wrapper.vm.modules).toEqual([]) }) })