agent-ecosystem/test/renderer/utils/fileTreeBuilder.test.ts
iliya 5b0c7d13fc feat: add project editor with drag & drop file management
- Backend: ProjectFileService with file CRUD, search, git status, file watcher
- IPC: 12 editor channels with security validation and path containment
- Store: editorSlice with multi-tab management, draft persistence, conflict detection
- UI: CodeMirror 6 editor, file tree with DnD, search-in-files, context menus
- Move: fs.rename with EXDEV fallback, full path remapping across all caches
- Tests: comprehensive coverage for services, IPC handlers, store, and utilities
2026-02-28 23:40:41 +02:00

184 lines
6 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { buildTree, sortTreeNodes } from '@renderer/utils/fileTreeBuilder';
import type { TreeNode } from '@renderer/utils/fileTreeBuilder';
interface TestItem {
path: string;
size: number;
}
const getPath = (item: TestItem) => item.path;
describe('buildTree', () => {
it('builds a flat list of files into a tree', () => {
const items: TestItem[] = [
{ path: 'src/main.ts', size: 100 },
{ path: 'src/utils.ts', size: 50 },
{ path: 'README.md', size: 30 },
];
const tree = buildTree(items, getPath);
expect(tree).toHaveLength(2);
const src = tree.find((n) => n.name === 'src');
expect(src).toBeDefined();
expect(src!.isFile).toBe(false);
expect(src!.children).toHaveLength(2);
const readme = tree.find((n) => n.name === 'README.md');
expect(readme).toBeDefined();
expect(readme!.isFile).toBe(true);
expect(readme!.data).toEqual({ path: 'README.md', size: 30 });
});
it('collapses single-child intermediate directories by default', () => {
const items: TestItem[] = [{ path: 'a/b/c/file.ts', size: 10 }];
const tree = buildTree(items, getPath);
// a/b/c collapsed into one node
expect(tree).toHaveLength(1);
expect(tree[0].name).toBe('a/b/c');
expect(tree[0].isFile).toBe(false);
expect(tree[0].children).toHaveLength(1);
expect(tree[0].children[0].name).toBe('file.ts');
expect(tree[0].children[0].isFile).toBe(true);
});
it('does not collapse when collapse=false', () => {
const items: TestItem[] = [{ path: 'a/b/file.ts', size: 10 }];
const tree = buildTree(items, getPath, { collapse: false });
expect(tree).toHaveLength(1);
expect(tree[0].name).toBe('a');
expect(tree[0].children).toHaveLength(1);
expect(tree[0].children[0].name).toBe('b');
expect(tree[0].children[0].children).toHaveLength(1);
expect(tree[0].children[0].children[0].name).toBe('file.ts');
});
it('does not collapse directories with multiple children', () => {
const items: TestItem[] = [
{ path: 'src/a/file1.ts', size: 10 },
{ path: 'src/b/file2.ts', size: 20 },
];
const tree = buildTree(items, getPath);
expect(tree).toHaveLength(1);
expect(tree[0].name).toBe('src');
expect(tree[0].children).toHaveLength(2);
// Each child is collapsed: a/ → file1.ts, b/ → file2.ts
expect(tree[0].children.map((c) => c.name).sort()).toEqual(['a', 'b']);
});
it('preserves data only on leaf nodes', () => {
const items: TestItem[] = [
{ path: 'src/index.ts', size: 100 },
{ path: 'src/utils/helper.ts', size: 50 },
];
const tree = buildTree(items, getPath);
const src = tree[0];
expect(src.data).toBeUndefined();
const indexFile = src.children.find((c) => c.name === 'index.ts');
expect(indexFile!.data).toEqual({ path: 'src/index.ts', size: 100 });
});
it('handles empty input', () => {
const tree = buildTree([], getPath);
expect(tree).toEqual([]);
});
it('handles single file at root level', () => {
const items: TestItem[] = [{ path: 'file.ts', size: 10 }];
const tree = buildTree(items, getPath);
expect(tree).toHaveLength(1);
expect(tree[0].name).toBe('file.ts');
expect(tree[0].isFile).toBe(true);
expect(tree[0].children).toHaveLength(0);
});
it('handles deeply nested paths', () => {
const items: TestItem[] = [{ path: 'a/b/c/d/e/f.ts', size: 1 }];
const tree = buildTree(items, getPath);
// Collapsed: a/b/c/d/e → f.ts
expect(tree).toHaveLength(1);
expect(tree[0].name).toBe('a/b/c/d/e');
expect(tree[0].children[0].name).toBe('f.ts');
});
it('sets correct fullPath for all nodes', () => {
const items: TestItem[] = [
{ path: 'src/components/Button.tsx', size: 100 },
{ path: 'src/components/Input.tsx', size: 80 },
];
const tree = buildTree(items, getPath, { collapse: false });
const src = tree[0];
expect(src.fullPath).toBe('src');
const components = src.children[0];
expect(components.fullPath).toBe('src/components');
const button = components.children.find((c) => c.name === 'Button.tsx');
expect(button!.fullPath).toBe('src/components/Button.tsx');
});
});
describe('sortTreeNodes', () => {
it('sorts directories before files', () => {
const nodes: TreeNode<TestItem>[] = [
{ name: 'beta.ts', fullPath: 'beta.ts', isFile: true, children: [] },
{ name: 'src', fullPath: 'src', isFile: false, children: [] },
{ name: 'alpha.ts', fullPath: 'alpha.ts', isFile: true, children: [] },
{ name: 'lib', fullPath: 'lib', isFile: false, children: [] },
];
const sorted = sortTreeNodes(nodes);
const dirs = sorted.filter((n) => !n.isFile);
const files = sorted.filter((n) => n.isFile);
// Directories come first
expect(dirs.map((n) => n.name)).toEqual(['lib', 'src']);
// Files come after
expect(files.map((n) => n.name)).toEqual(['alpha.ts', 'beta.ts']);
// Combined order
expect(sorted.slice(0, 2).every((n) => !n.isFile)).toBe(true);
expect(sorted.slice(2).every((n) => n.isFile)).toBe(true);
});
it('sorts alphabetically within same type', () => {
const nodes: TreeNode<TestItem>[] = [
{ name: 'zebra.ts', fullPath: 'zebra.ts', isFile: true, children: [] },
{ name: 'alpha.ts', fullPath: 'alpha.ts', isFile: true, children: [] },
{ name: 'mid.ts', fullPath: 'mid.ts', isFile: true, children: [] },
];
const sorted = sortTreeNodes(nodes);
expect(sorted.map((n) => n.name)).toEqual(['alpha.ts', 'mid.ts', 'zebra.ts']);
});
it('does not mutate the original array', () => {
const nodes: TreeNode<TestItem>[] = [
{ name: 'b.ts', fullPath: 'b.ts', isFile: true, children: [] },
{ name: 'a.ts', fullPath: 'a.ts', isFile: true, children: [] },
];
const sorted = sortTreeNodes(nodes);
expect(sorted).not.toBe(nodes);
expect(nodes[0].name).toBe('b.ts');
});
it('handles empty array', () => {
expect(sortTreeNodes([])).toEqual([]);
});
});