fasttree.test.js (4891B)
1 // Run with: node --test tests/*.test.js 2 const test = require('node:test'); 3 const assert = require('node:assert/strict'); 4 const { escapeHtml, jsonForScript, formatSize, buildIndex, listFolder, compose, folderPathFromUrl } = 5 require('../app/static/fasttree.js'); 6 7 const FILES = [ 8 ['README.md', 1200], ['.gitattributes', 66], 9 ['bsc/zeta.pdf', 2500000], ['bsc/Alpha.pdf', 999], ['bsc/sem1/n.txt', 12], ['bsc/sem1/deep/a.bin', 5], 10 ['$&/<b>.txt', 7], ['@@name@@/x.txt', 1152], 11 ]; 12 const index = buildIndex(FILES); 13 14 test('buildIndex: folders, files, totals', () => { 15 assert.deepEqual([...index.folders.keys()].sort(), ['', '$&', '@@name@@', 'bsc', 'bsc/sem1', 'bsc/sem1/deep']); 16 assert.equal(index.count, 8); 17 assert.equal(index.bytes, 1200 + 66 + 2500000 + 999 + 12 + 5 + 7 + 1152); 18 }); 19 20 test('listFolder: folders first, then case-insensitive by name, with sizes', () => { 21 const items = listFolder(index, 'bsc'); 22 assert.deepEqual(items.map((i) => [i.name, i.is_dir, i.size_str]), [ 23 ['sem1', true, '-'], ['Alpha.pdf', false, '999 B'], ['zeta.pdf', false, '2.38 MB'], 24 ]); 25 assert.equal(items[1].path, 'bsc/Alpha.pdf'); 26 assert.equal(listFolder(index, 'nope'), null); 27 }); 28 29 test('formatSize: matches Python, including round-half-even ties', () => { 30 const cases = { 0: '0 B', 1023: '1023 B', 1024: '1.00 KB', 1152: '1.12 KB', 1408: '1.38 KB', 1048576: '1.00 MB', 2500000: '2.38 MB' }; 31 for (const [n, want] of Object.entries(cases)) assert.equal(formatSize(Number(n)), want, n); 32 }); 33 34 test('escapeHtml / jsonForScript', () => { 35 assert.equal(escapeHtml(`<a href="x">'&'</a>`), '<a href="x">'&'</a>'); 36 assert.equal(jsonForScript('a</script>&\'é'), '"a\\u003c/script\\u003e\\u0026\\u0027\\u00e9"'); 37 }); 38 39 test('folderPathFromUrl: plain content paths only', () => { 40 const p = (s) => folderPathFromUrl(new URL(s, 'https://cdn.example.com')); 41 assert.equal(p('/'), ''); 42 assert.equal(p('/bsc/'), 'bsc'); 43 assert.equal(p('/a%26b/it%27s%20here'), "a&b/it's here"); 44 assert.equal(p('/bsc?x=1'), null); 45 assert.equal(p('/bsc#top'), null); 46 for (const r of ['/api/get/x', '/admin', '/mask/abc', '/pack/b1', '/static/sw.js', '/raw/x']) assert.equal(p(r), null, r); 47 assert.equal(p('/administrator'), 'administrator'); // only whole segments are reserved 48 assert.equal(p('/%E0%A4%A'), null); // malformed escape 49 }); 50 51 // A hand-made shell: every fragment is visible in the output, so substitution is easy to assert on. 52 const shell = { 53 cdn_base: 'https://cdn.example.com', 54 html: 'A<!--@root-header-->B<!--@breadcrumb-->C<!--@toolbar-->D<!--@list-->E<!--@stats-->F/*@visit*/G', 55 parts: { 56 root_header: '[RH]', count: '<c>@@n@@ @@word@@@@s@@</c>', toolbar: '[TB:@@counts@@|@@path@@]', 57 go_up: '[UP:@@parent_path@@]', row_dir: '[D:@@name@@|@@path@@]', row_file: '[F:@@name@@|@@path@@|@@size_str@@]', 58 empty: '[EMPTY]', stats: '[ST:@@n@@@@s@@|@@size_str@@]', crumb_container: '[CC:@@inner@@|@@tail@@]', 59 crumb_link: '<@@href@@:@@name@@>', crumb_collapsed: '[COL:@@menu@@]', copy_btn: '[CP:@@path@@]', 60 visit: 'V(@@path_json@@,@@cdn_url_json@@)', 61 }, 62 }; 63 const norm = (s) => s.replace(/\s+/g, ' '); 64 65 test('compose: root has the header, no breadcrumb/go-up/visit; hidden .gitattributes is not a row or a count', () => { 66 const html = norm(compose(shell, index, '')); 67 assert.match(html, /^A\[RH\]BC\[TB: <c>3 folders<\/c> & <c>1 file<\/c> \|\]D/); 68 assert.ok(!html.includes('.gitattributes')); 69 assert.ok(html.includes('[F:README.md|README.md|1.17 KB]')); 70 assert.ok(!html.includes('[UP:')); 71 assert.ok(html.endsWith('E[ST:8s|2.39 MB]FG'), html.slice(-40)); 72 }); 73 74 test('compose: a folder gets breadcrumb, go-up row, counts and a visit entry', () => { 75 const html = norm(compose(shell, index, 'bsc/sem1')); 76 assert.ok(html.includes('[CC: </:root> / </bsc:bsc> / </bsc/sem1:sem1> |[CP:bsc/sem1]]'), html); 77 assert.ok(html.includes('[UP:/bsc]')); 78 assert.ok(html.includes('<c>1 folder</c> & <c>1 file</c>')); 79 assert.ok(html.includes('V("bsc/sem1","https://cdn.example.com/bsc/sem1/")')); 80 assert.ok(!html.includes('[RH]')); 81 }); 82 83 test('compose: top-level folders go up to "/"', () => { 84 assert.ok(norm(compose(shell, index, 'bsc')).includes('[UP:/]')); 85 }); 86 87 test('compose: names are substituted literally (no $-patterns, no re-expansion of @@x@@)', () => { 88 const html = norm(compose(shell, index, '$&')); 89 assert.ok(html.includes('[F:<b>.txt|$&/<b>.txt|7 B]'), html); 90 const html2 = norm(compose(shell, index, '@@name@@')); 91 assert.ok(html2.includes('[F:x.txt|@@name@@/x.txt|1.12 KB]'), html2); 92 }); 93 94 test('compose: empty-looking folder shows the empty state and "0 items"', () => { 95 const only = buildIndex([['solo/.gitattributes', 1]]); 96 const html = norm(compose(shell, only, 'solo')); 97 assert.ok(html.includes('0 items')); 98 assert.ok(html.includes('[EMPTY]')); 99 });