Skip to content

Commit 6d5c69f

Browse files
yangshunclaude
andcommitted
Add tests for edge cases, symlinks, and special characters
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 076d4da commit 6d5c69f

2 files changed

Lines changed: 186 additions & 1 deletion

File tree

‎src/__tests__/__snapshots__/tree.test.ts.snap‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,49 @@ exports[`tree > allFiles 7`] = `
6565
└── .gitkeep"
6666
`;
6767

68+
exports[`tree > combined: allFiles + dirsOnly 1`] = `
69+
"multi-level
70+
├── beta
71+
│ ├── alpha
72+
│ │ └── beta
73+
│ └── charlie
74+
└── charlie
75+
└── beta"
76+
`;
77+
78+
exports[`tree > combined: dirsFirst + reverse 1`] = `
79+
"multi-level
80+
├── charlie
81+
├── beta
82+
├── delta.txt
83+
└── alpha.txt"
84+
`;
85+
86+
exports[`tree > combined: exclude + allFiles 1`] = `
87+
"multi-level
88+
├── alpha.txt
89+
├── beta
90+
│ ├── alpha
91+
│ │ ├── alpha.txt
92+
│ │ └── beta
93+
│ │ └── alpha.txt
94+
│ ├── beta.txt
95+
│ └── charlie
96+
│ └── alpha.txt
97+
├── charlie
98+
│ ├── alpha.txt
99+
│ ├── beta
100+
│ │ └── alpha.txt
101+
│ └── charlie.txt
102+
└── delta.txt"
103+
`;
104+
105+
exports[`tree > combined: trailingSlash + lineAscii + sizes 1`] = `
106+
"13B single-level/
107+
|-- 13B alpha.txt
108+
\`-- 0B beta/"
109+
`;
110+
68111
exports[`tree > default 1`] = `
69112
"chain
70113
└── alpha

‎src/__tests__/tree.test.ts‎

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import fs from 'fs';
22
import { isNotJunk } from 'junk';
3+
import os from 'os';
34
import path from 'path';
4-
import { describe, expect, test } from 'vitest';
5+
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
56
import tree from '../index';
67

78
const PATH_TO_TEST = path.join(__dirname, '__fixtures__');
@@ -129,4 +130,145 @@ describe('tree', () => {
129130
expect(result).toMatchSnapshot();
130131
});
131132
});
133+
134+
test('maxDepth 0 shows only root', () => {
135+
const result = tree(path.join(PATH_TO_TEST, 'multi-level'), {
136+
maxDepth: 0,
137+
});
138+
const lines = result.split('\n');
139+
expect(lines).toHaveLength(1);
140+
expect(lines[0]).toBe('multi-level');
141+
});
142+
143+
test('combined: dirsFirst + reverse', () => {
144+
const result = tree(path.join(PATH_TO_TEST, 'multi-level'), {
145+
dirsFirst: true,
146+
reverse: true,
147+
maxDepth: 1,
148+
});
149+
expect(result).toMatchSnapshot();
150+
});
151+
152+
test('combined: allFiles + dirsOnly', () => {
153+
const result = tree(path.join(PATH_TO_TEST, 'multi-level'), {
154+
allFiles: true,
155+
dirsOnly: true,
156+
});
157+
expect(result).toMatchSnapshot();
158+
});
159+
160+
test('combined: trailingSlash + lineAscii + sizes', () => {
161+
const result = tree(path.join(PATH_TO_TEST, 'single-level'), {
162+
trailingSlash: true,
163+
lineAscii: true,
164+
sizes: true,
165+
});
166+
expect(result).toMatchSnapshot();
167+
});
168+
169+
test('combined: exclude + allFiles', () => {
170+
const result = tree(path.join(PATH_TO_TEST, 'multi-level'), {
171+
allFiles: true,
172+
exclude: [/\.gitkeep/],
173+
});
174+
expect(result).toMatchSnapshot();
175+
});
176+
});
177+
178+
describe('tree error handling', () => {
179+
test('throws on non-existent path', () => {
180+
expect(() => tree('/non/existent/path')).toThrow();
181+
});
182+
183+
test('returns just the filename for a file path', () => {
184+
const result = tree(
185+
path.join(PATH_TO_TEST, 'single-file', 'alpha.txt'),
186+
);
187+
const lines = result.split('\n');
188+
expect(lines).toHaveLength(1);
189+
expect(lines[0]).toBe('alpha.txt');
190+
});
191+
});
192+
193+
describe('tree with symlinks', () => {
194+
let tmpDir: string;
195+
196+
beforeEach(() => {
197+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tree-test-'));
198+
fs.writeFileSync(path.join(tmpDir, 'real-file.txt'), 'hello');
199+
fs.mkdirSync(path.join(tmpDir, 'real-dir'));
200+
fs.writeFileSync(path.join(tmpDir, 'real-dir', 'nested.txt'), 'world');
201+
fs.symlinkSync(
202+
path.join(tmpDir, 'real-file.txt'),
203+
path.join(tmpDir, 'link-to-file.txt'),
204+
);
205+
fs.symlinkSync(
206+
path.join(tmpDir, 'real-dir'),
207+
path.join(tmpDir, 'link-to-dir'),
208+
);
209+
});
210+
211+
afterEach(() => {
212+
fs.rmSync(tmpDir, { recursive: true });
213+
});
214+
215+
test('lists symlinks but does not follow them into directories', () => {
216+
const result = tree(tmpDir);
217+
expect(result).toContain('link-to-file.txt');
218+
expect(result).toContain('link-to-dir');
219+
// The symlinked dir should appear but its contents should not,
220+
// because lstatSync treats it as a non-directory (symlink).
221+
const lines = result.split('\n');
222+
const linkDirLine = lines.find((l) => l.includes('link-to-dir'));
223+
expect(linkDirLine).toBeDefined();
224+
// nested.txt should only appear once (under real-dir, not under link-to-dir)
225+
const nestedOccurrences = lines.filter((l) => l.includes('nested.txt'));
226+
expect(nestedOccurrences).toHaveLength(1);
227+
});
228+
229+
test('symlinks are excluded by dirsOnly', () => {
230+
const result = tree(tmpDir, { dirsOnly: true });
231+
// Symlinks are treated as files by lstatSync, so they should be excluded
232+
expect(result).not.toContain('link-to-file.txt');
233+
expect(result).not.toContain('link-to-dir');
234+
expect(result).toContain('real-dir');
235+
});
236+
});
237+
238+
describe('tree with special characters in filenames', () => {
239+
let tmpDir: string;
240+
241+
beforeEach(() => {
242+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tree-test-'));
243+
fs.writeFileSync(path.join(tmpDir, 'file with spaces.txt'), '');
244+
fs.writeFileSync(path.join(tmpDir, 'file-with-dashes.txt'), '');
245+
fs.writeFileSync(path.join(tmpDir, "file'with'quotes.txt"), '');
246+
fs.writeFileSync(path.join(tmpDir, 'file(with)parens.txt'), '');
247+
fs.mkdirSync(path.join(tmpDir, 'dir with spaces'));
248+
fs.writeFileSync(
249+
path.join(tmpDir, 'dir with spaces', 'inner.txt'),
250+
'content',
251+
);
252+
});
253+
254+
afterEach(() => {
255+
fs.rmSync(tmpDir, { recursive: true });
256+
});
257+
258+
test('handles filenames with special characters', () => {
259+
const result = tree(tmpDir);
260+
expect(result).toContain('file with spaces.txt');
261+
expect(result).toContain('file-with-dashes.txt');
262+
expect(result).toContain("file'with'quotes.txt");
263+
expect(result).toContain('file(with)parens.txt');
264+
expect(result).toContain('dir with spaces');
265+
expect(result).toContain('inner.txt');
266+
});
267+
268+
test('special characters work with exclude patterns', () => {
269+
const result = tree(tmpDir, { exclude: [/spaces/] });
270+
expect(result).not.toContain('file with spaces.txt');
271+
expect(result).not.toContain('dir with spaces');
272+
expect(result).toContain('file-with-dashes.txt');
273+
});
132274
});

0 commit comments

Comments
 (0)