164 lines
9.5 KiB
JavaScript
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);
|
|
}
|
|
});
|