Files
wip-reference/site/tests/worldspace-data.test.mjs

164 lines
9.5 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 { createMarkdownProcessor } from '@astrojs/markdown-remark';
import { load } from 'cheerio';
import documentLinks, { contentsRoot } from '../plugins/document-links.mjs';
import { publicTree } from '../src/lib/landing-tree.ts';
import { createWorldspaceData } from '../src/lib/worldspace-data.ts';
const pages = {
'/': { title: 'Worldspace Interface Protocol', href: '/', html: '<h1>Home</h1>' },
'/book': { title: 'Book', href: '/book/', html: '<h1>Book</h1>' },
'/sources': { title: 'Sources', href: '/sources/', html: '<h1>Sources</h1>' },
'/packages': { title: 'Packages', href: '/packages/', html: '<h1>Packages</h1>' },
'/playground': { title: 'Playground', href: '/playground/', html: '<h1>Playground</h1>' },
};
const source = (id) => ({ id, title: id, href: `/book/${id}/`, html: `<h1>${id}</h1>` });
async function sourceFiles(directory, prefix = '') {
return (await Promise.all((await readdir(directory, { withFileTypes: true })).map(async (entry) => {
const relative = prefix + entry.name;
return entry.isDirectory() ? sourceFiles(path.join(directory, entry.name), relative + '/')
: entry.name.endsWith('.md') ? [relative] : [];
}))).flat();
}
const files = await sourceFiles(contentsRoot);
const publicFiles = files.filter((file) => !file.startsWith('draft/'));
const processor = await createMarkdownProcessor({ remarkPlugins: [[documentLinks, { base: '/' }]], smartypants: false });
const documents = await Promise.all(publicFiles.map(async (file) => {
const body = await readFile(path.join(contentsRoot, file), 'utf8');
const result = await processor.render(body, { fileURL: new URL(`file://${path.join(contentsRoot, file)}`) });
return { ...source(file.slice(0, -3)), title: body.match(/^#\s+(.+)$/m)?.[1] ?? file, html: result.code };
}));
function checkIdentity(data) {
for (const field of ['path', 'displayPath', 'href']) {
assert.equal(new Set(data.nodes.map((node) => node[field])).size, data.nodes.length);
}
const root = data.nodes.find((node) => node.path === '/');
assert.equal(root.name, '');
assert.equal(root.parent, undefined);
for (const node of data.nodes.filter((node) => node.path !== '/')) {
assert.ok(data.nodes.some((parent) => parent.path === node.parent));
assert.equal(node.path, `${node.parent === '/' ? '' : node.parent}/${node.name}`);
assert.ok(node.name.length > 0 && !node.name.includes('/'));
assert.ok(!node.path.split('/').includes('draft'));
assert.ok(!node.displayPath.split('/').includes('draft'));
}
}
test('actual public documents keep Web and display identities but acquire real parent paths', () => {
const data = createWorldspaceData(documents, pages);
checkIdentity(data);
const display = publicTree(documents);
assert.equal(data.nodes.length, documents.length + 5);
assert.deepEqual(data.nodes.map((node) => node.displayPath), display.map((node) => node.path));
for (const node of data.nodes) {
const legacy = display.find((item) => item.path === node.displayPath);
assert.equal(node.parent, legacy.parent === undefined ? undefined : data.nodes.find((item) => item.displayPath === legacy.parent).path);
if (legacy.href) {
assert.equal(node.href, legacy.href);
assert.equal(node.html, (documents.find((entry) => `/${entry.id}.md` === node.displayPath) ?? pages[node.displayPath]).html);
assert.match(node.html, /<h1/);
}
}
const section = data.nodes.find((node) => node.displayPath === '/1.1-interaction-abstraction.md');
assert.equal(section.path, '/book/1-concept.md/1.1-interaction-abstraction.md');
assert.equal(section.parent, '/book/1-concept.md');
assert.equal(section.href, '/book/1.1-interaction-abstraction/');
assert.equal(data.nodes[1].name, 'book');
assert.equal(display[1].name, 'book');
});
test('catalog and playground objects retain root identity, standalone URLs and no static descendants', () => {
const data = createWorldspaceData(documents, pages);
assert.deepEqual(data.nodes.filter((node) => node.parent === '/').map((node) => node.path), ['/book', '/sources', '/packages', '/playground']);
for (const path of ['/sources', '/packages', '/playground']) {
const leaf = data.nodes.find((node) => node.path === path);
assert.equal(leaf.displayPath, path);
assert.equal(leaf.href, `${path}/`);
assert.equal(leaf.html, pages[path].html);
assert.equal(data.nodes.filter((node) => node.parent === path).length, 0);
assert.ok(!data.nodes.some((node) => node.path.startsWith(`${path}/`)));
}
});
test('drafts are removed before hierarchy, body and URL generation', () => {
const marker = 'PRIVATE-DRAFT-CONTENT-MUST-NOT-SERIALIZE';
const drafts = files.filter((file) => file.startsWith('draft/')).map((file) => ({ ...source(file.slice(0, -3)), html: marker }));
assert.ok(drafts.length > 0);
const publicOnly = createWorldspaceData(documents, pages);
const mixed = createWorldspaceData([...documents, ...drafts], pages);
assert.deepEqual(mixed, publicOnly);
assert.ok(!JSON.stringify(mixed).includes(marker));
assert.ok(!mixed.nodes.some((node) => node.href.includes('/draft/')));
});
test('nested numbering, real directories and site base prefixes remain unambiguous', () => {
const docs = ['1-parent', '1.2-child', '1.2.3-leaf', 'other/1-parent', 'other/1.1-child']
.map((id) => ({ ...source(id), href: `/reference/book/${id}/` }));
const data = createWorldspaceData(docs, {
'/': { ...pages['/'], href: '/reference/' },
'/book': { ...pages['/book'], href: '/reference/book/' },
'/sources': { ...pages['/sources'], href: '/reference/sources/' },
'/packages': { ...pages['/packages'], href: '/reference/packages/' },
'/playground': { ...pages['/playground'], href: '/reference/playground/' },
'/other': { title: 'other/', href: '/reference/book/other/', html: '<h1>other/</h1>' },
});
checkIdentity(data);
assert.equal(data.nodes.find((node) => node.displayPath === '/1.2.3-leaf.md').path,
'/book/1-parent.md/1.2-child.md/1.2.3-leaf.md');
assert.equal(data.nodes.find((node) => node.displayPath === '/other/1.1-child.md').path,
'/book/other/1-parent.md/1.1-child.md');
assert.ok(data.nodes.every((node) => node.href.startsWith('/reference/')));
});
test('invalid, duplicate and incomplete generated inputs fail rather than lose documents', () => {
assert.throws(() => createWorldspaceData([source('a'), source('a')], pages), /Duplicate document/);
for (const id of ['', '/absolute', 'a//b', 'a/../b', 'a/./b', 'a/draft/b']) {
assert.throws(() => createWorldspaceData([source(id)], pages), /Invalid public document id/);
}
assert.throws(() => createWorldspaceData([source('a')], {}), /Missing rendered page/);
assert.throws(() => createWorldspaceData([{ ...source('a'), href: '/book/' }], pages), /Duplicate Web URL/);
assert.throws(() => createWorldspaceData([{ ...source('a'), href: '//external.example/' }], pages), /Invalid page metadata/);
assert.throws(() => createWorldspaceData([{ ...source('a'), title: '' }], pages), /Invalid page metadata/);
assert.throws(() => createWorldspaceData([source('dir/a')], pages), /Missing rendered page: \/dir/);
});
test('root, book, catalog and playground content is supplied by their shared page renderers', () => {
const data = createWorldspaceData([], pages);
assert.deepEqual(data.nodes, [
{ path: '/', name: '', displayPath: '/', ...pages['/'] },
{ path: '/book', name: 'book', displayPath: '/book', parent: '/', ...pages['/book'] },
{ path: '/sources', name: 'sources', displayPath: '/sources', parent: '/', ...pages['/sources'] },
{ path: '/packages', name: 'packages', displayPath: '/packages', parent: '/', ...pages['/packages'] },
{ path: '/playground', name: 'playground', displayPath: '/playground', parent: '/', ...pages['/playground'] },
]);
});
// Explicit post-build validation, usable for either root or prefixed deployments.
// WIP_TEST_GENERATED=1 node --test tests/worldspace-data.test.mjs
test('built artifact contains exactly public routes and the same rendered article bodies',
{ skip: process.env.WIP_TEST_GENERATED !== '1' }, async () => {
const data = JSON.parse(await readFile(new URL('../dist/worldspace.json', import.meta.url), 'utf8'));
checkIdentity(data);
assert.equal(data.nodes.length, documents.length + 5);
assert.deepEqual(new Set(data.nodes.filter((node) => node.displayPath.endsWith('.md')).map((node) => node.displayPath.slice(1))), new Set(publicFiles));
for (const node of data.nodes) {
const relative = node.displayPath === '/' ? 'index.html' : node.displayPath === '/book' ? 'book/index.html'
: ['/sources', '/packages', '/playground'].includes(node.displayPath) ? `${node.displayPath.slice(1)}/index.html`
: `book/${node.displayPath.slice(1, -3)}/index.html`;
const page = load(await readFile(new URL(`../dist/${relative}`, import.meta.url), 'utf8'));
// Astro adds hydration/render markers outside the actual article fragment.
// Compare normalized markup, not generated comment/whitespace placement.
const fragment = load(node.html, {}, false);
const article = page('[data-selected-content]');
assert.equal(article.length, 1, `${relative} has a document body`);
assert.equal(fragment.text().replace(/\s+/g, ' ').trim(), article.text().replace(/\s+/g, ' ').trim(), node.path);
assert.deepEqual(fragment('a[href]').map((_, item) => fragment(item).attr('href')).get(),
article.find('a[href]').map((_, item) => page(item).attr('href')).get(), node.path);
}
});