Files
MyGoNavi/frontend/src/components/sidebarV2Utils.command-search.test.ts
Syngnat 6abdb8684f 🐛 fix(sidebar): 折叠海量对象树时释放子节点
- 折叠海量数据库/对象分组时清理子树缓存和后代展开态
- 避免虚拟树在大批量函数、过程等对象折叠后残留渲染

Fixes #587
2026-06-27 11:37:55 +08:00

144 lines
4.6 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
V2_COMMAND_SEARCH_INITIAL_TREE_LIMIT,
V2_COMMAND_SEARCH_MAX_TREE_RESULTS,
buildV2CommandSearchTreeIndex,
collectSidebarSubtreeKeys,
filterV2CommandSearchTreeItems,
parseV2CommandSearchQuery,
resolveSidebarDatabaseTreePruneKeys,
shouldClearSidebarNodeChildrenOnCollapse,
type V2CommandSearchItem,
} from './sidebarV2Utils';
const buildNodeItems = (count: number): V2CommandSearchItem[] => {
return Array.from({ length: count }, (_, index) => ({
key: `node-table-${index}`,
kind: 'node' as const,
title: `fs_order_${index}`,
meta: `开发240 · front_end_sys_${index % 4}`,
icon: null,
node: {
type: index % 6 === 0 ? 'view' : 'table',
key: `table-${index}`,
title: `fs_order_${index}`,
dataRef: {
tableName: `fs_order_${index}`,
viewName: index % 6 === 0 ? `v_order_${index}` : undefined,
dbName: `front_end_sys_${index % 4}`,
name: `obj_${index}`,
config: {
host: `10.0.0.${index % 16}`,
},
},
},
}));
};
describe('sidebarV2 command search performance helpers', () => {
it('keeps the initial tree result limit when the query is empty', () => {
const items = buildNodeItems(V2_COMMAND_SEARCH_INITIAL_TREE_LIMIT + 80);
expect(
filterV2CommandSearchTreeItems(items, parseV2CommandSearchQuery('')),
).toHaveLength(V2_COMMAND_SEARCH_INITIAL_TREE_LIMIT);
});
it('caps broad keyword matches to avoid rendering the full loaded tree', () => {
const items = buildNodeItems(V2_COMMAND_SEARCH_MAX_TREE_RESULTS + 160);
const result = filterV2CommandSearchTreeItems(
items,
parseV2CommandSearchQuery('fs_order'),
);
expect(result).toHaveLength(V2_COMMAND_SEARCH_MAX_TREE_RESULTS);
expect(result[0]?.key).toBe('node-table-0');
expect(result[result.length - 1]?.key).toBe(`node-table-${V2_COMMAND_SEARCH_MAX_TREE_RESULTS - 1}`);
});
it('returns the same matches when filtering with a prebuilt search index', () => {
const items = buildNodeItems(200);
const index = buildV2CommandSearchTreeIndex(items);
const query = parseV2CommandSearchQuery('@fs_order_1');
expect(filterV2CommandSearchTreeItems(index, query)).toEqual(
filterV2CommandSearchTreeItems(items, query),
);
});
it('prunes only cold collapsed database trees when too many object trees stay loaded', () => {
expect(resolveSidebarDatabaseTreePruneKeys({
treeData: [
{
key: 'conn-1',
title: 'conn-1',
type: 'connection',
children: [
{
key: 'conn-1-db-a',
title: 'db-a',
type: 'database',
children: [{ key: 'a-tables', title: '表', type: 'object-group' }],
},
{
key: 'conn-1-db-b',
title: 'db-b',
type: 'database',
children: [{ key: 'b-tables', title: '表', type: 'object-group' }],
},
{
key: 'conn-1-db-c',
title: 'db-c',
type: 'database',
children: [{ key: 'c-tables', title: '表', type: 'object-group' }],
},
{
key: 'conn-1-db-d',
title: 'db-d',
type: 'database',
children: [{ key: 'd-tables', title: '表', type: 'object-group' }],
},
],
},
],
expandedKeys: ['conn-1-db-c'],
selectedKeys: [],
activeDatabaseKey: 'conn-1-db-d',
touchedAtByDatabaseKey: {
'conn-1-db-a': 10,
'conn-1-db-b': 20,
'conn-1-db-c': 30,
'conn-1-db-d': 40,
},
maxLoadedDatabases: 2,
})).toEqual(['conn-1-db-a', 'conn-1-db-b']);
});
it('clears only large loaded sidebar subtrees on collapse', () => {
const routineChildren = Array.from({ length: 180 }, (_, index) => ({
key: `routine-${index}`,
title: `routine_${index}`,
type: 'routine' as const,
}));
const largeRoutineGroup = {
key: 'conn-1-db-a-routines',
title: '函数',
type: 'object-group' as const,
children: routineChildren,
};
expect(collectSidebarSubtreeKeys(largeRoutineGroup)).toHaveLength(180);
expect(shouldClearSidebarNodeChildrenOnCollapse(largeRoutineGroup)).toBe(true);
expect(shouldClearSidebarNodeChildrenOnCollapse({
type: 'object-group',
children: routineChildren.slice(0, 8),
})).toBe(false);
expect(shouldClearSidebarNodeChildrenOnCollapse({
type: 'table',
children: routineChildren,
})).toBe(false);
});
});