Skip to content

Commit 294660b

Browse files
committed
feat(utils): add path helper utilities
Add utility functions for working with files and paths: - formatFileSize() for human-readable byte formatting - Permission formatting utilities - Path manipulation helpers - Includes tests
1 parent 46a5eae commit 294660b

File tree

2 files changed

+670
-0
lines changed

2 files changed

+670
-0
lines changed

src/utils/path-helpers.test.ts

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import ava from 'ava'
2+
import * as path from 'node:path'
3+
import {
4+
formatFileSize,
5+
humanizePermissions,
6+
formatPermissions,
7+
getFileTypeChar,
8+
isBinaryFileSync,
9+
safePath,
10+
isDescendant,
11+
getCommonAncestor,
12+
expandHome,
13+
getFileType
14+
} from './path-helpers'
15+
16+
// ============================================================================
17+
// formatFileSize tests
18+
// ============================================================================
19+
20+
ava('formatFileSize - 0 bytes', (t) => {
21+
t.is(formatFileSize(0), '0 Bytes')
22+
})
23+
24+
ava('formatFileSize - bytes', (t) => {
25+
t.is(formatFileSize(500), '500 Bytes')
26+
})
27+
28+
ava('formatFileSize - kilobytes', (t) => {
29+
t.is(formatFileSize(1024), '1 KB')
30+
t.is(formatFileSize(1536), '1.5 KB')
31+
})
32+
33+
ava('formatFileSize - megabytes', (t) => {
34+
t.is(formatFileSize(1048576), '1 MB')
35+
t.is(formatFileSize(1572864), '1.5 MB')
36+
})
37+
38+
ava('formatFileSize - gigabytes', (t) => {
39+
t.is(formatFileSize(1073741824), '1 GB')
40+
})
41+
42+
ava('formatFileSize - custom decimals', (t) => {
43+
t.is(formatFileSize(1536, 0), '2 KB')
44+
t.is(formatFileSize(1536, 3), '1.5 KB')
45+
})
46+
47+
// ============================================================================
48+
// humanizePermissions tests
49+
// ============================================================================
50+
51+
ava('humanizePermissions - rwxr-xr-x (755)', (t) => {
52+
t.is(humanizePermissions(0o755), 'rwxr-xr-x')
53+
})
54+
55+
ava('humanizePermissions - rw-r--r-- (644)', (t) => {
56+
t.is(humanizePermissions(0o644), 'rw-r--r--')
57+
})
58+
59+
ava('humanizePermissions - rwx------ (700)', (t) => {
60+
t.is(humanizePermissions(0o700), 'rwx------')
61+
})
62+
63+
ava('humanizePermissions - rw------- (600)', (t) => {
64+
t.is(humanizePermissions(0o600), 'rw-------')
65+
})
66+
67+
// ============================================================================
68+
// getFileTypeChar tests
69+
// ============================================================================
70+
71+
ava('getFileTypeChar - regular file', (t) => {
72+
t.is(getFileTypeChar(0o100644), '-')
73+
})
74+
75+
ava('getFileTypeChar - directory', (t) => {
76+
t.is(getFileTypeChar(0o040755), 'd')
77+
})
78+
79+
ava('getFileTypeChar - symbolic link', (t) => {
80+
t.is(getFileTypeChar(0o120777), 'l')
81+
})
82+
83+
// ============================================================================
84+
// formatPermissions tests
85+
// ============================================================================
86+
87+
ava('formatPermissions - regular file 644', (t) => {
88+
t.is(formatPermissions(0o100644), '-rw-r--r--')
89+
})
90+
91+
ava('formatPermissions - directory 755', (t) => {
92+
t.is(formatPermissions(0o040755), 'drwxr-xr-x')
93+
})
94+
95+
// ============================================================================
96+
// isBinaryFileSync tests
97+
// ============================================================================
98+
99+
ava('isBinaryFileSync - image extensions', (t) => {
100+
t.true(isBinaryFileSync('photo.jpg'))
101+
t.true(isBinaryFileSync('image.PNG'))
102+
t.true(isBinaryFileSync('logo.gif'))
103+
})
104+
105+
ava('isBinaryFileSync - executable extensions', (t) => {
106+
t.true(isBinaryFileSync('app.exe'))
107+
t.true(isBinaryFileSync('library.dll'))
108+
t.true(isBinaryFileSync('binary.bin'))
109+
})
110+
111+
ava('isBinaryFileSync - text extensions', (t) => {
112+
t.false(isBinaryFileSync('script.js'))
113+
t.false(isBinaryFileSync('style.css'))
114+
t.false(isBinaryFileSync('readme.md'))
115+
})
116+
117+
// ============================================================================
118+
// safePath tests
119+
// ============================================================================
120+
121+
ava('safePath - joins segments', (t) => {
122+
const result = safePath('home', 'user', 'file.txt')
123+
t.is(result, path.normalize('home/user/file.txt'))
124+
})
125+
126+
ava('safePath - filters empty segments', (t) => {
127+
const result = safePath('home', '', 'user', '', 'file.txt')
128+
t.is(result, path.normalize('home/user/file.txt'))
129+
})
130+
131+
// ============================================================================
132+
// isDescendant tests
133+
// ============================================================================
134+
135+
ava('isDescendant - direct child', (t) => {
136+
t.true(isDescendant('/home/user', '/home/user/file.txt'))
137+
})
138+
139+
ava('isDescendant - nested child', (t) => {
140+
t.true(isDescendant('/home/user', '/home/user/docs/file.txt'))
141+
})
142+
143+
ava('isDescendant - not a descendant', (t) => {
144+
t.false(isDescendant('/home/user', '/home/other/file.txt'))
145+
})
146+
147+
ava('isDescendant - parent is not descendant', (t) => {
148+
t.false(isDescendant('/home/user/docs', '/home/user'))
149+
})
150+
151+
// ============================================================================
152+
// getCommonAncestor tests
153+
// ============================================================================
154+
155+
ava('getCommonAncestor - empty array', (t) => {
156+
t.is(getCommonAncestor([]), null)
157+
})
158+
159+
ava('getCommonAncestor - single path', (t) => {
160+
const result = getCommonAncestor(['/home/user/file.txt'])
161+
t.is(result, '/home/user')
162+
})
163+
164+
ava('getCommonAncestor - sibling files', (t) => {
165+
const result = getCommonAncestor(['/home/user/a.txt', '/home/user/b.txt'])
166+
t.is(result, '/home/user')
167+
})
168+
169+
ava('getCommonAncestor - different directories', (t) => {
170+
const result = getCommonAncestor(['/home/user/docs/a.txt', '/home/user/images/b.png'])
171+
t.is(result, '/home/user')
172+
})
173+
174+
// ============================================================================
175+
// expandHome tests
176+
// ============================================================================
177+
178+
ava('expandHome - tilde at start', (t) => {
179+
const home = process.env.HOME || process.env.USERPROFILE || ''
180+
const result = expandHome('~/Documents')
181+
t.is(result, path.join(home, 'Documents'))
182+
})
183+
184+
ava('expandHome - no tilde', (t) => {
185+
const result = expandHome('/absolute/path')
186+
t.is(result, '/absolute/path')
187+
})
188+
189+
ava('expandHome - tilde in middle (not expanded)', (t) => {
190+
const result = expandHome('/path/with~/tilde')
191+
t.is(result, '/path/with~/tilde')
192+
})
193+
194+
// ============================================================================
195+
// getFileType tests
196+
// ============================================================================
197+
198+
ava('getFileType - javascript', (t) => {
199+
t.is(getFileType('script.js'), 'javascript')
200+
t.is(getFileType('component.jsx'), 'javascript')
201+
})
202+
203+
ava('getFileType - typescript', (t) => {
204+
t.is(getFileType('app.ts'), 'typescript')
205+
t.is(getFileType('component.tsx'), 'typescript')
206+
})
207+
208+
ava('getFileType - python', (t) => {
209+
t.is(getFileType('script.py'), 'python')
210+
})
211+
212+
ava('getFileType - markdown', (t) => {
213+
t.is(getFileType('README.md'), 'markdown')
214+
t.is(getFileType('docs.markdown'), 'markdown')
215+
})
216+
217+
ava('getFileType - unknown', (t) => {
218+
t.is(getFileType('file.xyz'), 'unknown')
219+
})

0 commit comments

Comments
 (0)