Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
at main 305 lines 9.2 kB view raw
1import { describe, test, expect } from 'bun:test' 2import { processUploadedFiles, type UploadedFile } from './tree' 3 4describe('processUploadedFiles', () => { 5 test('should preserve nested directory structure', () => { 6 const files: UploadedFile[] = [ 7 { 8 name: 'mysite/index.html', 9 content: Buffer.from('<html>'), 10 mimeType: 'text/html', 11 size: 6 12 }, 13 { 14 name: 'mysite/_astro/main.js', 15 content: Buffer.from('console.log()'), 16 mimeType: 'application/javascript', 17 size: 13 18 }, 19 { 20 name: 'mysite/_astro/styles.css', 21 content: Buffer.from('body {}'), 22 mimeType: 'text/css', 23 size: 7 24 }, 25 { 26 name: 'mysite/images/logo.png', 27 content: Buffer.from([0x89, 0x50, 0x4e, 0x47]), 28 mimeType: 'image/png', 29 size: 4 30 } 31 ] 32 33 const result = processUploadedFiles(files) 34 35 expect(result.fileCount).toBe(4) 36 expect(result.directory.entries).toHaveLength(3) // index.html, _astro/, images/ 37 38 // Check _astro directory exists 39 const astroEntry = result.directory.entries.find(e => e.name === '_astro') 40 expect(astroEntry).toBeTruthy() 41 expect('type' in astroEntry!.node && astroEntry!.node.type).toBe('directory') 42 43 if ('entries' in astroEntry!.node) { 44 const astroDir = astroEntry!.node 45 expect(astroDir.entries).toHaveLength(2) // main.js, styles.css 46 expect(astroDir.entries.find(e => e.name === 'main.js')).toBeTruthy() 47 expect(astroDir.entries.find(e => e.name === 'styles.css')).toBeTruthy() 48 } 49 50 // Check images directory exists 51 const imagesEntry = result.directory.entries.find(e => e.name === 'images') 52 expect(imagesEntry).toBeTruthy() 53 expect('type' in imagesEntry!.node && imagesEntry!.node.type).toBe('directory') 54 55 if ('entries' in imagesEntry!.node) { 56 const imagesDir = imagesEntry!.node 57 expect(imagesDir.entries).toHaveLength(1) // logo.png 58 expect(imagesDir.entries.find(e => e.name === 'logo.png')).toBeTruthy() 59 } 60 }) 61 62 test('should handle deeply nested directories', () => { 63 const files: UploadedFile[] = [ 64 { 65 name: 'site/a/b/c/d/deep.txt', 66 content: Buffer.from('deep'), 67 mimeType: 'text/plain', 68 size: 4 69 } 70 ] 71 72 const result = processUploadedFiles(files) 73 74 expect(result.fileCount).toBe(1) 75 76 // Navigate through nested structure 77 const aEntry = result.directory.entries.find(e => e.name === 'a') 78 expect(aEntry).toBeTruthy() 79 expect('type' in aEntry!.node && aEntry!.node.type).toBe('directory') 80 81 if ('entries' in aEntry!.node) { 82 const bEntry = aEntry!.node.entries.find(e => e.name === 'b') 83 expect(bEntry).toBeTruthy() 84 expect('type' in bEntry!.node && bEntry!.node.type).toBe('directory') 85 86 if ('entries' in bEntry!.node) { 87 const cEntry = bEntry!.node.entries.find(e => e.name === 'c') 88 expect(cEntry).toBeTruthy() 89 expect('type' in cEntry!.node && cEntry!.node.type).toBe('directory') 90 91 if ('entries' in cEntry!.node) { 92 const dEntry = cEntry!.node.entries.find(e => e.name === 'd') 93 expect(dEntry).toBeTruthy() 94 expect('type' in dEntry!.node && dEntry!.node.type).toBe('directory') 95 96 if ('entries' in dEntry!.node) { 97 const fileEntry = dEntry!.node.entries.find(e => e.name === 'deep.txt') 98 expect(fileEntry).toBeTruthy() 99 expect('type' in fileEntry!.node && fileEntry!.node.type).toBe('file') 100 } 101 } 102 } 103 } 104 }) 105 106 test('should handle files at root level', () => { 107 const files: UploadedFile[] = [ 108 { 109 name: 'mysite/index.html', 110 content: Buffer.from('<html>'), 111 mimeType: 'text/html', 112 size: 6 113 }, 114 { 115 name: 'mysite/robots.txt', 116 content: Buffer.from('User-agent: *'), 117 mimeType: 'text/plain', 118 size: 13 119 } 120 ] 121 122 const result = processUploadedFiles(files) 123 124 expect(result.fileCount).toBe(2) 125 expect(result.directory.entries).toHaveLength(2) 126 expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy() 127 expect(result.directory.entries.find(e => e.name === 'robots.txt')).toBeTruthy() 128 }) 129 130 test('should skip .git directories', () => { 131 const files: UploadedFile[] = [ 132 { 133 name: 'mysite/index.html', 134 content: Buffer.from('<html>'), 135 mimeType: 'text/html', 136 size: 6 137 }, 138 { 139 name: 'mysite/.git/config', 140 content: Buffer.from('[core]'), 141 mimeType: 'text/plain', 142 size: 6 143 }, 144 { 145 name: 'mysite/.gitignore', 146 content: Buffer.from('node_modules'), 147 mimeType: 'text/plain', 148 size: 12 149 } 150 ] 151 152 const result = processUploadedFiles(files) 153 154 expect(result.fileCount).toBe(2) // Only index.html and .gitignore 155 expect(result.directory.entries).toHaveLength(2) 156 expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy() 157 expect(result.directory.entries.find(e => e.name === '.gitignore')).toBeTruthy() 158 expect(result.directory.entries.find(e => e.name === '.git')).toBeFalsy() 159 }) 160 161 test('should handle mixed root and nested files', () => { 162 const files: UploadedFile[] = [ 163 { 164 name: 'mysite/index.html', 165 content: Buffer.from('<html>'), 166 mimeType: 'text/html', 167 size: 6 168 }, 169 { 170 name: 'mysite/about/index.html', 171 content: Buffer.from('<html>'), 172 mimeType: 'text/html', 173 size: 6 174 }, 175 { 176 name: 'mysite/about/team.html', 177 content: Buffer.from('<html>'), 178 mimeType: 'text/html', 179 size: 6 180 }, 181 { 182 name: 'mysite/robots.txt', 183 content: Buffer.from('User-agent: *'), 184 mimeType: 'text/plain', 185 size: 13 186 } 187 ] 188 189 const result = processUploadedFiles(files) 190 191 expect(result.fileCount).toBe(4) 192 expect(result.directory.entries).toHaveLength(3) // index.html, about/, robots.txt 193 194 const aboutEntry = result.directory.entries.find(e => e.name === 'about') 195 expect(aboutEntry).toBeTruthy() 196 expect('type' in aboutEntry!.node && aboutEntry!.node.type).toBe('directory') 197 198 if ('entries' in aboutEntry!.node) { 199 const aboutDir = aboutEntry!.node 200 expect(aboutDir.entries).toHaveLength(2) // index.html, team.html 201 expect(aboutDir.entries.find(e => e.name === 'index.html')).toBeTruthy() 202 expect(aboutDir.entries.find(e => e.name === 'team.html')).toBeTruthy() 203 } 204 }) 205 206 test('should handle empty file array', () => { 207 const files: UploadedFile[] = [] 208 209 const result = processUploadedFiles(files) 210 211 expect(result.fileCount).toBe(0) 212 expect(result.directory.entries).toHaveLength(0) 213 }) 214 215 test('should strip base folder name from paths', () => { 216 // This tests the behavior where file.name includes the base folder 217 // e.g., "mysite/index.html" should become "index.html" at root 218 const files: UploadedFile[] = [ 219 { 220 name: 'build-output/index.html', 221 content: Buffer.from('<html>'), 222 mimeType: 'text/html', 223 size: 6 224 }, 225 { 226 name: 'build-output/assets/main.js', 227 content: Buffer.from('console.log()'), 228 mimeType: 'application/javascript', 229 size: 13 230 } 231 ] 232 233 const result = processUploadedFiles(files) 234 235 expect(result.fileCount).toBe(2) 236 237 // Should have index.html at root and assets/ directory 238 expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy() 239 expect(result.directory.entries.find(e => e.name === 'assets')).toBeTruthy() 240 241 // Should NOT have 'build-output' directory 242 expect(result.directory.entries.find(e => e.name === 'build-output')).toBeFalsy() 243 }) 244 245 test('should preserve full paths with skipNormalization option (CLI use case)', () => { 246 // CLI passes paths already relative to site directory, without a folder prefix 247 const files: UploadedFile[] = [ 248 { 249 name: 'index.html', 250 content: Buffer.from('<html>'), 251 mimeType: 'text/html', 252 size: 6 253 }, 254 { 255 name: 'assets/readme.txt', 256 content: Buffer.from('readme'), 257 mimeType: 'text/plain', 258 size: 6 259 }, 260 { 261 name: 'assets/images/logo.txt', 262 content: Buffer.from('logo'), 263 mimeType: 'text/plain', 264 size: 4 265 }, 266 { 267 name: 'assets/scripts/main.js', 268 content: Buffer.from('console.log()'), 269 mimeType: 'application/javascript', 270 size: 13 271 } 272 ] 273 274 const result = processUploadedFiles(files, { skipNormalization: true }) 275 276 expect(result.fileCount).toBe(4) 277 278 // index.html at root 279 expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy() 280 281 // assets directory should exist (NOT be stripped) 282 const assetsEntry = result.directory.entries.find(e => e.name === 'assets') 283 expect(assetsEntry).toBeTruthy() 284 expect('type' in assetsEntry!.node && assetsEntry!.node.type).toBe('directory') 285 286 if ('entries' in assetsEntry!.node) { 287 const assetsDir = assetsEntry!.node 288 // Should have readme.txt, images/, scripts/ 289 expect(assetsDir.entries.find(e => e.name === 'readme.txt')).toBeTruthy() 290 expect(assetsDir.entries.find(e => e.name === 'images')).toBeTruthy() 291 expect(assetsDir.entries.find(e => e.name === 'scripts')).toBeTruthy() 292 293 // Check nested directories have files 294 const imagesEntry = assetsDir.entries.find(e => e.name === 'images') 295 if (imagesEntry && 'entries' in imagesEntry.node) { 296 expect(imagesEntry.node.entries.find(e => e.name === 'logo.txt')).toBeTruthy() 297 } 298 299 const scriptsEntry = assetsDir.entries.find(e => e.name === 'scripts') 300 if (scriptsEntry && 'entries' in scriptsEntry.node) { 301 expect(scriptsEntry.node.entries.find(e => e.name === 'main.js')).toBeTruthy() 302 } 303 } 304 }) 305})