Files
wip-reference/site/tests/landing-tree.test.mjs

164 lines
8.7 KiB
JavaScript

import assert from 'node:assert/strict';
import { readdir, readFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import { contentsRoot } from '../plugins/document-links.mjs';
import { documentTree, chapterTree, bookTree, publicTree, childrenOf, visibleNodes, treePrefix, serializeTree } from '../src/lib/landing-tree.ts';
async function sourceFiles(directory, prefix = '') {
const entries = await readdir(directory, { withFileTypes: true });
return (await Promise.all(entries.map(async (entry) => {
const relative = prefix + entry.name;
if (entry.isDirectory()) return sourceFiles(path.join(directory, entry.name), relative + '/');
return entry.name.endsWith('.md') ? [relative] : [];
}))).flat();
}
const files = await sourceFiles(contentsRoot);
const documents = await Promise.all(files.map(async (file) => {
const body = await readFile(path.join(contentsRoot, file), 'utf8');
return { id: file.slice(0, -3), title: body.match(/^#\s+(.+)$/m)?.[1], href: '/book/' + file.slice(0, -3) + '/', html: body };
}));
const nodes = documentTree(documents);
test('tree mirrors only real content files and their directory parents', () => {
assert.deepEqual(nodes.filter((node) => node.href).map((node) => node.path).sort(), files.map((file) => '/' + file).sort());
const directories = new Set(['/']);
for (const file of files) {
const segments = file.split('/');
for (let length = 1; length < segments.length; length++) directories.add('/' + segments.slice(0, length).join('/'));
}
assert.deepEqual(new Set(nodes.filter((node) => !node.href).map((node) => node.path)), directories);
assert.equal(new Set(nodes.map((node) => node.path)).size, nodes.length);
for (const node of nodes.slice(1)) {
assert.equal(node.name, node.path.split('/').at(-1));
assert.equal(node.depth, node.path.split('/').length);
assert.ok(childrenOf(nodes, node.parent).includes(node));
assert.ok(nodes.findIndex((parent) => parent.path === node.parent) < nodes.indexOf(node));
}
});
test('chapter display nests existing numbered sections while preserving paths and URLs', () => {
const display = chapterTree(documents);
for (const [chapter, sections] of [
['/1-concept.md', ['/1.1-interaction-abstraction.md', '/1.2-existing-abstractions.md', '/1.3-design-principles.md', '/1.4-concepts-and-scope.md']],
['/2-core-model.md', ['/2.1-object-tree-schema.md', '/2.2-operation-and-interface.md']],
['/4-client.md', ['/4.1-ai-tools.md', '/4.2-text-view.md']],
['/5-transports.md', ['/5.1-wip-over-http.md']],
]) {
assert.deepEqual(childrenOf(display, chapter).map((node) => node.path), sections);
for (const section of sections) {
const node = display.find((node) => node.path === section);
assert.equal(node.depth, 3);
assert.equal(treePrefix(node, display).length, 6);
assert.ok(!visibleNodes(display, new Set([chapter])).includes(node));
}
}
for (const node of display) {
const original = nodes.find((candidate) => candidate.path === node.path);
assert.equal(node.href, original.href);
assert.equal(node.name, original.name);
assert.equal(node.title, original.title);
}
assert.deepEqual(childrenOf(display, '/draft'), childrenOf(nodes, '/draft'));
// The directory index still lists the physical files, not the display grouping.
assert.equal(nodes.find((node) => node.path === '/1.1-interaction-abstraction.md').parent, '/');
});
test('chapter grouping supports deeper numbering and never invents or crosses directory parents', () => {
const source = ['1.2.3-leaf', '1.2-section', '1-chapter', '10-other', '10.1-section', '3.1-orphan', 'draft/1.1-independent']
.map((id) => ({ id, title: id, href: `/book/${id}/` }));
const display = chapterTree(source);
const at = (id) => display.find((node) => node.path === `/${id}.md`);
assert.equal(at('1.2-section').parent, '/1-chapter.md');
assert.equal(at('1.2.3-leaf').parent, '/1.2-section.md');
assert.equal(at('1.2.3-leaf').depth, 4);
assert.equal(at('10.1-section').parent, '/10-other.md');
assert.equal(at('3.1-orphan').parent, '/');
assert.equal(at('draft/1.1-independent').parent, '/draft');
assert.ok(display.indexOf(at('1-chapter')) < display.indexOf(at('1.2-section')));
assert.ok(display.indexOf(at('1.2-section')) < display.indexOf(at('1.2.3-leaf')));
});
test('public book container wraps the numbered chapters without changing document identity', () => {
const published = documents.filter((document) => !document.id.startsWith('draft/'));
const display = bookTree(published);
assert.deepEqual(childrenOf(display, '/').map((node) => node.path), ['/book']);
assert.equal(display[1].name, 'book');
assert.equal(display[1].depth, 2);
assert.equal(display.length, published.length + 2);
assert.ok(!display.some((node) => node.path.startsWith('/draft')));
assert.deepEqual(childrenOf(display, '/book').map((node) => node.path), ['/1-concept.md', '/2-core-model.md', '/3-protocol.md', '/4-client.md', '/5-transports.md']);
const section = display.find((node) => node.path === '/1.1-interaction-abstraction.md');
assert.equal(section.parent, '/1-concept.md');
assert.equal(section.depth, 4);
assert.equal(treePrefix(section, display), ' │ ├─ ');
assert.deepEqual(visibleNodes(display, new Set(['/book'])), display.slice(0, 2));
for (const node of display.filter((node) => node.href)) {
assert.equal(node.href, nodes.find((original) => original.path === node.path).href);
}
});
test('public root catalogs and playground are leaves alongside the unchanged book subtree', () => {
const published = documents.filter((document) => !document.id.startsWith('draft/'));
for (const base of ['/', '/reference/']) {
const display = publicTree(published, base);
assert.deepEqual(display.slice(0, -3), bookTree(published));
assert.deepEqual(childrenOf(display, '/').map((node) => node.path), ['/book', '/sources', '/packages', '/playground']);
assert.deepEqual(visibleNodes(display, new Set(['/book'])).map((node) => node.path), ['/', '/book', '/sources', '/packages', '/playground']);
assert.deepEqual(visibleNodes(display, new Set(['/'])), [display[0]]);
for (const name of ['sources', 'packages', 'playground']) {
const leaf = display.find((node) => node.path === `/${name}`);
assert.equal(leaf.name, name);
assert.equal(leaf.depth, 2);
assert.equal(leaf.href, `${base}${name}/`);
assert.ok(leaf.title);
assert.deepEqual(childrenOf(display, leaf.path), []);
assert.deepEqual(visibleNodes(display, new Set([leaf.path])), display);
}
assert.equal(treePrefix(display.find((node) => node.path === '/book'), display), '├─ ');
assert.equal(treePrefix(display.find((node) => node.path === '/1.1-interaction-abstraction.md'), display), '│ │ ├─ ');
assert.equal(treePrefix(display.find((node) => node.path === '/packages'), display), '├─ ');
assert.equal(display.at(-1).path, '/playground');
assert.equal(treePrefix(display.at(-1), display), '└─ ');
assert.ok(!display.some((node) => node.path.includes('/draft')));
}
});
test('existing titles and hrefs pass through without embedding document bodies', () => {
for (const document of documents) {
const node = nodes.find((node) => node.path === '/' + document.id + '.md');
assert.equal('html' in node, false);
assert.equal('body' in node, false);
assert.equal(node.title, document.title);
assert.equal(node.href, document.href);
assert.equal('description' in node, false);
assert.equal('interface' in node, false);
}
for (const folder of nodes.filter((node) => !node.href)) {
assert.equal(folder.html, undefined);
assert.equal(folder.title, undefined);
}
});
test('collapse hides descendants only and joins follow the visible tree', () => {
assert.deepEqual(visibleNodes(nodes, new Set()), nodes);
assert.deepEqual(visibleNodes(nodes, new Set(['/'])), [nodes[0]]);
const collapsed = visibleNodes(nodes, new Set(['/draft']));
assert.ok(collapsed.some((node) => node.path === '/draft'));
assert.ok(!collapsed.some((node) => node.path.startsWith('/draft/')));
for (const shown of [nodes, collapsed]) {
for (const node of shown) {
const prefix = treePrefix(node, nodes, shown);
assert.equal(prefix.length, (node.depth - 1) * 3);
if (node.parent) assert.ok(prefix.endsWith(childrenOf(shown, node.parent).at(-1) === node ? '└─ ' : '├─ '));
}
}
});
test('embedded metadata cannot close its JSON script and roundtrips exactly', () => {
const input = documentTree([{ id: 'source', title: '</script><script>alert(1)</script>\u2028&', href: '/book/source/' }]);
const serialized = serializeTree(input);
assert.equal(serialized.includes('<'), false);
assert.deepEqual(JSON.parse(serialized), input);
});