Files
docmd-io--docmd/tests/feature-integration.test.js
wehub-resource-sync 6db8fca185
docmd CI verification / verify (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:31:55 +08:00

961 lines
37 KiB
JavaScript

#!/usr/bin/env node
/**
* docmd Comprehensive Feature Test Suite
* =======================================
* Brute tests every major docmd feature in isolation and combination.
* Run: node tests/feature-integration.test.js
*
* Tests:
* 1. Zero-config (no config file)
* 2. Zero-config with nested docs
* 3. i18n standalone (3 locales, non-default as default)
* 4. Versioning standalone (2 versions)
* 5. i18n + versioning combined
* 6. Old version with partial translations
* 7. Missing locale dir (graceful skip)
* 8. Navigation resolution (locale > version > config)
* 9. Frontmatter parsing (title, description, layout)
* 10. Containers (callout, tabs, steps, cards, hero)
* 11. Code blocks (syntax highlighting, line numbers)
* 12. Custom CSS/JS injection
* 13. Edit links
* 14. No-style pages
* 15. Search index generation
* 16. Sitemap generation
* 17. EJS content pages
* 18. README.md as index fallback
* 19. Markdown file extensions (.md and .markdown)
* 20. Deep nested directory structure
* 21. Zero-config auto-nav accuracy
* 22. Title tag auto-append
* 23. OG meta tags
* 24. Redirects
* 25. Per-page layout override
*/
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
const DOCMD = path.resolve(import.meta.dirname, '../packages/core/dist/bin/docmd.js');
const TEST_ROOT = '/tmp/docmd-brute-tests';
const PASS = '✅';
const FAIL = '❌';
let passed = 0;
let failed = 0;
const failures = [];
function setup(name) {
const dir = path.join(TEST_ROOT, name);
fs.rmSync(dir, { recursive: true, force: true });
fs.mkdirSync(dir, { recursive: true });
return dir;
}
function build(dir, expectFail = false) {
try {
const out = execSync(`node ${DOCMD} build`, { cwd: dir, stdio: 'pipe', encoding: 'utf8' });
if (expectFail) return { ok: false, output: out };
return { ok: true, output: out };
} catch (e) {
if (expectFail) return { ok: true, output: e.stderr || e.stdout || '' };
return { ok: false, output: e.stderr || e.stdout || '' };
}
}
// Run a command, return its numeric exit
// code. 0 on success, 1+ on documented failure paths. -1 if the
// process was killed by a signal (rare; mostly for diagnostics).
function exitCodeOf(cmd, cwd) {
try {
execSync(cmd, { cwd, stdio: 'pipe' });
return 0;
} catch (e) {
return e.status == null ? -1 : e.status;
}
}
function writeFile(dir, filePath, content) {
const full = path.join(dir, filePath);
fs.mkdirSync(path.dirname(full), { recursive: true });
fs.writeFileSync(full, content);
}
function readSite(dir, filePath) {
const full = path.join(dir, 'site', filePath);
if (!fs.existsSync(full)) return null;
return fs.readFileSync(full, 'utf8');
}
function siteExists(dir, filePath) {
return fs.existsSync(path.join(dir, 'site', filePath));
}
function countPages(dir) {
const siteDir = path.join(dir, 'site');
if (!fs.existsSync(siteDir)) return 0;
let count = 0;
function walk(d) {
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
if (f.isDirectory()) walk(path.join(d, f.name));
else if (f.name === 'index.html') count++;
}
}
walk(siteDir);
return count;
}
function assert(testName, condition, detail = '') {
if (condition) {
console.log(`│ ${PASS} ${testName}`);
passed++;
} else {
console.log(`│ ${FAIL} ${testName}${detail ? ': ' + detail : ''}`);
failed++;
failures.push(testName);
}
}
// ─── TEST 1: Zero-config (no config file) ───
console.log('\n📦 Test 1: Zero-config (no config file)');
{
const dir = setup('01-zero-config');
writeFile(dir, 'docs/index.md', '---\ntitle: Home\n---\n# Hello\nWelcome.');
writeFile(dir, 'docs/about.md', '---\ntitle: About\n---\n# About\nAbout page.');
const r = build(dir);
assert('builds without config', r.ok);
assert('generates 2 pages', countPages(dir) === 2);
assert('index page exists', siteExists(dir, 'index.html'));
assert('about page exists', siteExists(dir, 'about/index.html'));
const idx = readSite(dir, 'index.html');
assert('contains page content', idx?.includes('Welcome'));
}
// ─── TEST 2: Zero-config with nested docs ───
console.log('\n📦 Test 2: Zero-config with nested docs');
{
const dir = setup('02-zero-nested');
writeFile(dir, 'docs/index.md', '# Home');
writeFile(dir, 'docs/guide/intro.md', '# Intro\nIntro content.');
writeFile(dir, 'docs/guide/advanced/deep.md', '# Deep\nDeep content.');
const r = build(dir);
assert('builds with nested dirs', r.ok);
assert('generates 3 pages', countPages(dir) === 3);
assert('nested page exists', siteExists(dir, 'guide/intro/index.html'));
assert('deep nested page exists', siteExists(dir, 'guide/advanced/deep/index.html'));
}
// ─── TEST 3: i18n standalone (non-English default) ───
console.log('\n🌍 Test 3: i18n standalone (Hindi default)');
{
const dir = setup('03-i18n-hindi-default');
writeFile(dir, 'docmd.config.js', `module.exports = {
title: 'Hindi Default',
src: 'docs',
i18n: { default: 'hi', locales: [
{ id: 'hi', label: 'हिन्दी' },
{ id: 'en', label: 'English' }
]}
};`);
writeFile(dir, 'docs/hi/index.md', '---\ntitle: मुखपृष्ठ\n---\n# स्वागत');
writeFile(dir, 'docs/hi/guide.md', '---\ntitle: गाइड\n---\n# गाइड');
writeFile(dir, 'docs/hi/navigation.json', '[{"title":"मुखपृष्ठ","path":"/"},{"title":"गाइड","path":"/guide"}]');
writeFile(dir, 'docs/en/index.md', '---\ntitle: Home\n---\n# Welcome');
writeFile(dir, 'docs/en/navigation.json', '[{"title":"Home","path":"/"}]');
const r = build(dir);
assert('builds with Hindi default', r.ok);
const root = readSite(dir, 'index.html');
assert('root is Hindi content', root?.includes('स्वागत'));
const enPage = readSite(dir, 'en/index.html');
assert('English at /en/', enPage?.includes('Welcome'));
assert('Hindi guide exists', siteExists(dir, 'guide/index.html'));
assert('English guide falls back from Hindi', siteExists(dir, 'en/guide/index.html'));
}
// ─── TEST 3.5 (M-5): i18n cross-locale link to default locale prefix ───
console.log('\n🌍 Test 3.5: i18n cross-locale link to default locale (M-5)');
{
// M-5: when the default locale lives at root and a non-default-locale
// page writes `[link](/<defaultLocale>/foo)`, the build used to emit
// `<a href="/en/foo">` which 404s because the default-locale page is
// at `/foo`, not `/en/foo`. The fix strips the default-locale prefix
// from absolute hrefs in the rendered HTML so the link points to the
// actual location of the default-locale page.
const dir = setup('03.5-m5-cross-locale-link');
writeFile(dir, 'docmd.config.js', `module.exports = {
title: 'M5 test',
src: 'docs',
i18n: { default: 'en', locales: [
{ id: 'en', label: 'English' },
{ id: 'fr', label: 'Français' }
]}
};`);
writeFile(dir, 'docs/en/index.md', '# Welcome');
writeFile(dir, 'docs/en/setup.md', '# Setup page');
writeFile(dir, 'docs/fr/index.md', 'Voir la [version anglaise](/en/) et le [setup anglais](/en/setup).');
const r = build(dir);
assert('builds M-5 scenario', r.ok);
const fr = readSite(dir, 'fr/index.html');
assert('fr page does NOT link to /en/ (would 404)', fr && !/href="\/en\/"/.test(fr));
assert('fr page does NOT link to /en/setup (would 404)', fr && !/href="\/en\/setup"/.test(fr));
// The rewritten links should still resolve. Default-locale pages live
// at root, so `/en/` → `/` and `/en/setup` → `/setup` (or
// `/setup/`). Both should be present.
assert('default-locale index exists at root', siteExists(dir, 'index.html'));
assert('default-locale setup page exists', siteExists(dir, 'setup/index.html'));
// Online build keeps clean URLs — verify the link does NOT carry
// `/index.html` suffix (that would be the offline build behaviour).
assert('online build keeps clean URLs', fr && !/href="[^"]*index\.html"/.test(fr));
}
// ─── TEST 4: Versioning standalone ───
console.log('\n📚 Test 4: Versioning standalone (no i18n)');
{
const dir = setup('04-versioning');
writeFile(dir, 'docmd.config.js', `module.exports = {
title: 'Versioned',
src: 'docs',
versions: { current: 'v2', all: [
{ id: 'v2', dir: 'docs', label: 'v2.0' },
{ id: 'v1', dir: 'docs-v1', label: 'v1.0' }
]}
};`);
writeFile(dir, 'docs/index.md', '# v2 Home');
writeFile(dir, 'docs/guide.md', '# v2 Guide');
writeFile(dir, 'docs/navigation.json', '[{"title":"Home","path":"/"},{"title":"Guide","path":"/guide"}]');
writeFile(dir, 'docs-v1/index.md', '# v1 Home');
writeFile(dir, 'docs-v1/navigation.json', '[{"title":"Home","path":"/"}]');
const r = build(dir);
assert('builds with versioning', r.ok);
assert('v2 at root', readSite(dir, 'index.html')?.includes('v2 Home'));
assert('v1 at /v1/', readSite(dir, 'v1/index.html')?.includes('v1 Home'));
assert('v2 guide exists', siteExists(dir, 'guide/index.html'));
assert('no v1 guide', !siteExists(dir, 'v1/guide/index.html'));
}
// ─── TEST 5: i18n + versioning combined ───
console.log('\n🌍📚 Test 5: i18n + versioning combined');
{
const dir = setup('05-i18n-versioning');
writeFile(dir, 'docmd.config.js', `module.exports = {
title: 'Combined',
src: 'docs',
i18n: { default: 'en', locales: [
{ id: 'en', label: 'English' },
{ id: 'fr', label: 'Français' }
]},
versions: { current: 'v2', all: [
{ id: 'v2', dir: 'docs', label: 'v2' },
{ id: 'v1', dir: 'docs-v1', label: 'v1' }
]}
};`);
writeFile(dir, 'docs/en/index.md', '# EN v2');
writeFile(dir, 'docs/en/guide.md', '# EN Guide');
writeFile(dir, 'docs/en/navigation.json', '[{"title":"Home","path":"/"},{"title":"Guide","path":"/guide"}]');
writeFile(dir, 'docs/fr/index.md', '# FR v2');
writeFile(dir, 'docs/fr/navigation.json', '[{"title":"Accueil","path":"/"}]');
writeFile(dir, 'docs-v1/index.md', '# EN v1');
writeFile(dir, 'docs-v1/navigation.json', '[{"title":"Home","path":"/"}]');
const r = build(dir);
assert('builds combined i18n+versioning', r.ok);
assert('EN root', readSite(dir, 'index.html')?.includes('EN v2'));
assert('FR at /fr/', readSite(dir, 'fr/index.html')?.includes('FR v2'));
assert('EN guide fallback in FR', siteExists(dir, 'fr/guide/index.html'));
assert('v1 at /v1/ (EN only)', readSite(dir, 'v1/index.html')?.includes('EN v1'));
assert('no FR v1 (no locale dir)', !siteExists(dir, 'fr/v1/index.html'));
}
// ─── TEST 6: Old version with partial translation ───
console.log('\n🌍📚 Test 6: Old version with partial translation');
{
const dir = setup('06-partial-old-version');
writeFile(dir, 'docmd.config.js', `module.exports = {
title: 'Partial',
src: 'docs',
i18n: { default: 'en', locales: [
{ id: 'en', label: 'English' },
{ id: 'hi', label: 'Hindi' }
]},
versions: { current: 'v2', all: [
{ id: 'v2', dir: 'docs', label: 'v2' },
{ id: 'v1', dir: 'docs-v1', label: 'v1' }
]}
};`);
writeFile(dir, 'docs/en/index.md', '# EN v2');
writeFile(dir, 'docs/en/navigation.json', '[{"title":"Home","path":"/"}]');
writeFile(dir, 'docs/hi/index.md', '# HI v2');
writeFile(dir, 'docs/hi/navigation.json', '[{"title":"होम","path":"/"}]');
writeFile(dir, 'docs-v1/index.md', '# EN v1');
writeFile(dir, 'docs-v1/guide.md', '# EN v1 Guide');
writeFile(dir, 'docs-v1/navigation.json', '[{"title":"Home","path":"/"},{"title":"Guide","path":"/guide"}]');
writeFile(dir, 'docs-v1/hi/index.md', '# HI v1');
writeFile(dir, 'docs-v1/hi/navigation.json', '[{"title":"होम","path":"/"}]');
const r = build(dir);
assert('builds with partial old version translation', r.ok);
assert('HI v1 at /hi/v1/', readSite(dir, 'hi/v1/index.html')?.includes('HI v1'));
assert('EN v1 guide exists', siteExists(dir, 'v1/guide/index.html'));
assert('no ghost /v1/hi/ path', !siteExists(dir, 'v1/hi/index.html'));
}
// ─── TEST 7: Missing locale dir (graceful skip) ───
console.log('\n🌍 Test 7: Missing locale dir (graceful skip)');
{
const dir = setup('07-missing-locale');
writeFile(dir, 'docmd.config.js', `module.exports = {
title: 'Missing',
src: 'docs',
i18n: { default: 'en', locales: [
{ id: 'en', label: 'English' },
{ id: 'ja', label: 'Japanese' }
]}
};`);
writeFile(dir, 'docs/en/index.md', '# English');
writeFile(dir, 'docs/en/navigation.json', '[{"title":"Home","path":"/"}]');
// No docs/ja/ directory at all
const r = build(dir);
assert('builds without crashing', r.ok);
assert('EN pages built', siteExists(dir, 'index.html'));
assert('no JA dir in output', !siteExists(dir, 'ja/index.html'));
}
// ─── TEST 8: Navigation resolution priority ───
console.log('\n🧭 Test 8: Navigation resolution priority');
{
const dir = setup('08-nav-resolution');
writeFile(dir, 'docmd.config.js', `module.exports = {
title: 'Nav Test',
src: 'docs',
navigation: [{ title: 'Config Nav', path: '/' }],
i18n: { default: 'en', locales: [
{ id: 'en', label: 'English' },
{ id: 'fr', label: 'French' }
]}
};`);
writeFile(dir, 'docs/en/index.md', '# Home');
writeFile(dir, 'docs/en/navigation.json', '[{"title":"Locale Nav","path":"/"}]');
writeFile(dir, 'docs/fr/index.md', '# Accueil');
// FR has NO navigation.json — should fall back to EN's
const r = build(dir);
assert('builds with nav priority', r.ok);
const enPage = readSite(dir, 'index.html');
assert('EN uses locale nav', enPage?.includes('Locale Nav'));
}
// ─── TEST 9: Frontmatter parsing ───
console.log('\n📝 Test 9: Frontmatter parsing');
{
const dir = setup('09-frontmatter');
writeFile(dir, 'docs/index.md', `---
title: "My Page Title"
description: "A test description"
---
# Content
Body text.`);
const r = build(dir);
assert('builds with frontmatter', r.ok);
const html = readSite(dir, 'index.html');
assert('title in output', html?.includes('My Page Title'));
assert('description meta tag', html?.includes('A test description'));
}
// ─── TEST 10: Containers ───
console.log('\n📦 Test 10: Containers');
{
const dir = setup('10-containers');
writeFile(dir, 'docs/index.md', `# Containers Test
::: callout info "Note"
This is a callout.
:::
::: callout warning "Warning"
Be careful.
:::
::: callout danger "Danger"
Very dangerous.
:::
::: callout tip "Tip"
A helpful tip.
:::
::: tabs
== tab "Tab One"
First tab content.
== tab "Tab Two"
Second tab content.
:::
::: steps
### Step 1
Do this first.
### Step 2
Then do this.
:::
::: hero
# Hero Title
Hero description.
:::
`);
const r = build(dir);
assert('builds with containers', r.ok);
const html = readSite(dir, 'index.html');
assert('callout rendered', html?.includes('callout') || html?.includes('Note'));
assert('tabs rendered', html?.includes('tabs') && html?.includes('Tab One'));
assert('steps rendered', html?.includes('Step 1') && html?.includes('Step 2'));
assert('hero rendered', html?.includes('Hero Title'));
}
// ─── TEST 11: Code blocks ───
console.log('\n💻 Test 11: Code blocks');
{
const dir = setup('11-code-blocks');
writeFile(dir, 'docs/index.md', `# Code Test
\`\`\`js
const x = 42;
console.log(x);
\`\`\`
\`\`\`python
def hello():
return "world"
\`\`\`
`);
const r = build(dir);
assert('builds with code blocks', r.ok);
const html = readSite(dir, 'index.html');
assert('JS code rendered', html?.includes('const') && html?.includes('42'));
assert('Python code rendered', html?.includes('def') && html?.includes('hello'));
}
// ─── TEST 12: Custom CSS/JS injection ───
console.log('\n🎨 Test 12: Custom CSS/JS injection');
{
const dir = setup('12-custom-assets');
writeFile(dir, 'docmd.config.js', `module.exports = {
title: 'Custom Assets',
src: 'docs',
head: [
'<link rel="stylesheet" href="/custom.css">',
'<script src="/custom.js"></script>'
]
};`);
writeFile(dir, 'docs/index.md', '# Custom Assets');
const r = build(dir);
assert('builds with custom assets', r.ok);
const html = readSite(dir, 'index.html');
assert('custom CSS or head injection works', html?.includes('custom.css') || r.ok);
assert('custom JS or head injection works', html?.includes('custom.js') || r.ok);
}
// ─── TEST 13: Edit links ───
console.log('\n✏️ Test 13: Edit links');
{
const dir = setup('13-edit-links');
writeFile(dir, 'docmd.config.js', `module.exports = {
title: 'Edit Links',
src: 'docs',
editLink: {
enabled: true,
baseUrl: 'https://github.com/test/repo/edit/main/docs',
text: 'Edit on GitHub'
}
};`);
writeFile(dir, 'docs/index.md', '# Edit Link Test');
const r = build(dir);
assert('builds with edit links', r.ok);
const html = readSite(dir, 'index.html');
assert('edit link in page', html?.includes('Edit on GitHub') || html?.includes('github.com/test/repo'));
}
// ─── TEST 14: No-style pages ───
console.log('\n🧹 Test 14: No-style pages');
{
const dir = setup('14-no-style');
writeFile(dir, 'docs/index.md', '# Normal Page');
writeFile(dir, 'docs/raw.md', '---\nlayout: no-style\n---\n# Raw Page\nNo styling.');
const r = build(dir);
assert('builds with no-style page', r.ok);
assert('no-style page exists', siteExists(dir, 'raw/index.html'));
const raw = readSite(dir, 'raw/index.html');
assert('no-style page rendered', raw?.includes('Raw Page'));
}
// ─── TEST 15: Search index ───
console.log('\n🔍 Test 15: Search index generation');
{
const dir = setup('15-search');
writeFile(dir, 'docs/index.md', '# Search Home\nSearchable content here.');
writeFile(dir, 'docs/guide.md', '# Guide\nMore searchable content.');
const r = build(dir);
assert('builds with search', r.ok);
assert('search-index.json exists', siteExists(dir, 'search-index.json'));
const idx = readSite(dir, 'search-index.json');
assert('search index has content', idx && JSON.parse(idx).documentCount >= 2);
}
// ─── TEST 16: Sitemap ───
console.log('\n🗺️ Test 16: Sitemap generation');
{
const dir = setup('16-sitemap');
writeFile(dir, 'docmd.config.js', `module.exports = {
title: 'Sitemap Test',
src: 'docs',
url: 'https://example.com'
};`);
writeFile(dir, 'docs/index.md', '# Home');
writeFile(dir, 'docs/about.md', '# About');
const r = build(dir);
assert('builds with sitemap', r.ok);
assert('sitemap.xml exists', siteExists(dir, 'sitemap.xml'));
const sm = readSite(dir, 'sitemap.xml');
assert('sitemap has URLs', sm?.includes('example.com'));
}
// ─── TEST 17: EJS content pages ───
console.log('\n🔧 Test 17: EJS content pages');
{
const dir = setup('17-ejs');
writeFile(dir, 'docs/index.md', '# Home');
writeFile(dir, 'docs/dynamic.ejs', `---
title: Dynamic Page
---
# Generated at build time
<% const items = ['one', 'two', 'three']; %>
<% items.forEach(i => { %>
- Item: <%= i %>
<% }); %>
`);
const r = build(dir);
assert('builds with EJS pages', r.ok);
assert('EJS page rendered', siteExists(dir, 'dynamic/index.html'));
const html = readSite(dir, 'dynamic/index.html');
assert('EJS content expanded', html?.includes('Item: one') && html?.includes('Item: three'));
}
// ─── TEST 18: README.md as index fallback ───
console.log('\n📋 Test 18: README.md as index fallback');
{
const dir = setup('18-readme');
writeFile(dir, 'docs/README.md', '# Welcome\nFrom README.');
writeFile(dir, 'docs/guide/README.md', '# Guide\nFrom guide README.');
const r = build(dir);
assert('builds with README fallback', r.ok);
assert('README becomes index', siteExists(dir, 'index.html'));
const html = readSite(dir, 'index.html');
assert('README content rendered', html?.includes('From README'));
assert('nested README becomes index', siteExists(dir, 'guide/index.html'));
}
// ─── TEST 19: .markdown extension ───
console.log('\n📄 Test 19: .markdown file extension');
{
const dir = setup('19-markdown-ext');
writeFile(dir, 'docs/index.md', '# Home');
writeFile(dir, 'docs/about.markdown', '# About\nMarkdown extension.');
const r = build(dir);
assert('builds with .markdown files', r.ok);
assert('.markdown page rendered', siteExists(dir, 'about/index.html'));
const html = readSite(dir, 'about/index.html');
assert('.markdown content correct', html?.includes('Markdown extension'));
}
// ─── TEST 20: Deep nesting ───
console.log('\n🏗️ Test 20: Deep nested structure');
{
const dir = setup('20-deep-nesting');
writeFile(dir, 'docs/index.md', '# Home');
writeFile(dir, 'docs/a/b/c/d/deep.md', '# Deep Page\nVery deep.');
const r = build(dir);
assert('builds with deep nesting', r.ok);
assert('deep page exists', siteExists(dir, 'a/b/c/d/deep/index.html'));
}
// ─── TEST 21: Zero-config auto-nav accuracy ───
console.log('\n🧭 Test 21: Zero-config auto-nav accuracy');
{
const dir = setup('21-auto-nav');
writeFile(dir, 'docs/index.md', '# Home');
writeFile(dir, 'docs/getting-started.md', '# Getting Started');
writeFile(dir, 'docs/guide/intro.md', '# Intro');
writeFile(dir, 'docs/guide/advanced.md', '# Advanced');
const r = build(dir);
assert('builds with auto-nav', r.ok);
const html = readSite(dir, 'index.html');
assert('auto-nav includes Getting Started', html?.includes('Getting Started'));
assert('auto-nav includes Guide section', html?.includes('Intro') || html?.includes('Guide'));
}
// ─── TEST 22: Title tag auto-append ───
console.log('\n🏷️ Test 22: Title tag auto-append');
{
const dir = setup('22-title-tag');
writeFile(dir, 'docmd.config.js', `module.exports = { title: 'MySite', src: 'docs' };`);
writeFile(dir, 'docs/index.md', '---\ntitle: Home\n---\n# Home');
writeFile(dir, 'docs/about.md', '---\ntitle: About Us\n---\n# About');
const r = build(dir);
assert('builds with title tags', r.ok);
const about = readSite(dir, 'about/index.html');
// Title should be "About Us — MySite" or similar
const titleMatch = about?.match(/<title>(.*?)<\/title>/s);
const titleText = titleMatch?.[1]?.trim();
assert('sub-page title includes page + site', titleText?.includes('About') && titleText?.includes('MySite'));
}
// ─── TEST 23: OG meta tags ───
console.log('\n🏷️ Test 23: OG meta tags');
{
const dir = setup('23-og-tags');
writeFile(dir, 'docmd.config.js', `module.exports = { title: 'OG Test', src: 'docs', url: 'https://example.com' };`);
writeFile(dir, 'docs/index.md', '---\ntitle: OG Page\ndescription: OG description\n---\n# OG');
const r = build(dir);
assert('builds with OG tags', r.ok);
const html = readSite(dir, 'index.html');
assert('og:title present', html?.includes('og:title'));
assert('og:description present', html?.includes('og:description') || html?.includes('OG description'));
}
// ─── TEST 24: Redirects ───
console.log('\n↩️ Test 24: Redirects');
{
const dir = setup('24-redirects');
writeFile(dir, 'docmd.config.js', `module.exports = {
title: 'Redirects',
src: 'docs',
redirects: { '/old-page': '/new-page' }
};`);
writeFile(dir, 'docs/index.md', '# Home');
writeFile(dir, 'docs/new-page.md', '# New Page');
const r = build(dir);
assert('builds with redirects', r.ok);
assert('redirect page created', siteExists(dir, 'old-page/index.html'));
const redirect = readSite(dir, 'old-page/index.html');
assert('redirect has meta refresh', redirect?.includes('new-page') || redirect?.includes('redirect'));
}
// ─── TEST 25: Per-page layout override ───
console.log('\n📐 Test 25: Per-page layout override');
{
const dir = setup('25-layout-override');
writeFile(dir, 'docs/index.md', '# Normal');
writeFile(dir, 'docs/landing.md', '---\nlayout: no-style\ntitle: Landing\n---\n# Landing Page\nCustom layout.');
const r = build(dir);
assert('builds with layout override', r.ok);
const normal = readSite(dir, 'index.html');
const landing = readSite(dir, 'landing/index.html');
assert('normal page has full layout', normal?.includes('sidebar') || normal?.includes('nav'));
assert('landing page rendered', landing?.includes('Landing Page'));
}
// ─── TEST 26: URL Consistency (No Double Slashes) ───
console.log('\n🔗 Test 26: URL Consistency (No Double Slashes)');
{
const dir = setup('26-url-consistency');
writeFile(dir, 'docmd.config.js', `module.exports = {
title: 'URL Test',
url: 'https://example.com',
src: 'docs',
navigation: [
{ title: 'Home', path: '/' },
{ title: 'Guide', path: '/guide/' },
{ title: 'Nested', path: '/deep/nested/' }
]
};`);
writeFile(dir, 'docs/index.md', '# Home\n[Go to Guide](guide/)\n[Go to Nested](deep/nested/)');
writeFile(dir, 'docs/guide.md', '# Guide\n[Back Home](/)');
writeFile(dir, 'docs/deep/nested.md', '# Nested\n[Back Home](/)');
const r = build(dir);
assert('builds with navigation', r.ok);
const idx = readSite(dir, 'index.html');
const guide = readSite(dir, 'guide/index.html');
const nested = readSite(dir, 'deep/nested/index.html');
// Check for double slashes in href attributes (excluding protocol://)
const hasDoubleSlashHref = (html) => /href="[^"]*([^:])\/\//.test(html);
assert('index has no double-slash hrefs', !hasDoubleSlashHref(idx));
assert('guide has no double-slash hrefs', !hasDoubleSlashHref(guide));
assert('nested has no double-slash hrefs', !hasDoubleSlashHref(nested));
// Check sitemap URLs
const sitemap = readSite(dir, 'sitemap.xml');
const hasDoubleSlash = (html) => /([^:])\/\//.test(html);
assert('sitemap has no double slashes', !hasDoubleSlash(sitemap));
assert('sitemap has correct root URL', sitemap?.includes('https://example.com/'));
assert('sitemap has correct guide URL', sitemap?.includes('https://example.com/guide/'));
}
// ─── TEST 27: Search Index URL Format ───
console.log('\n🔍 Test 27: Search Index URL Format');
{
const dir = setup('27-search-urls');
writeFile(dir, 'docs/index.md', '---\ntitle: Home\n---\n# Welcome');
writeFile(dir, 'docs/guide/intro.md', '---\ntitle: Introduction\n---\n# Getting Started');
const r = build(dir);
assert('builds with search', r.ok);
const searchIdx = readSite(dir, 'search-index.json');
const parsed = JSON.parse(searchIdx);
const storedFields = parsed.storedFields || {};
const ids = Object.values(storedFields).map((f) => f.id);
// IDs should be clean slugs without leading slashes (except root)
assert('root page has clean ID', ids.some(id => id === '/'));
assert('nested page has clean ID', ids.some(id => id === 'guide/intro/' || id === '/guide/intro/'));
// No double slashes in any ID
assert('no double slashes in search IDs', !ids.some(id => id.includes('//')));
}
// ─── TEST 28: i18n URL Prefixes ───
console.log('\n🌍 Test 28: i18n URL Prefixes');
{
const dir = setup('28-i18n-urls');
writeFile(dir, 'docmd.config.js', `module.exports = {
title: 'i18n URLs',
url: 'https://example.com',
src: 'docs',
i18n: { default: 'en', locales: [
{ id: 'en', label: 'English' },
{ id: 'de', label: 'Deutsch' }
]}
};`);
writeFile(dir, 'docs/en/index.md', '# English Home\n[German](/de/)');
writeFile(dir, 'docs/en/guide.md', '# English Guide');
writeFile(dir, 'docs/en/navigation.json', '[{"title":"Home","path":"/"},{"title":"Guide","path":"/guide/"}]');
writeFile(dir, 'docs/de/index.md', '# German Home\n[English](/)');
writeFile(dir, 'docs/de/navigation.json', '[{"title":"Startseite","path":"/"}]');
const r = build(dir);
assert('builds with i18n', r.ok);
// Check that nav links have correct prefixes
const enIdx = readSite(dir, 'index.html');
const deIdx = readSite(dir, 'de/index.html');
// English root should have links without double slashes
const hasDoubleSlash = (html) => /href="[^"]*([^:])\/\//.test(html);
assert('EN index has no double-slash hrefs', !hasDoubleSlash(enIdx));
assert('DE index has no double-slash hrefs', !hasDoubleSlash(deIdx));
// Check sitemap has both locales
const sitemap = readSite(dir, 'sitemap.xml');
assert('sitemap has EN URLs', sitemap?.includes('example.com/'));
assert('sitemap has DE URLs', sitemap?.includes('example.com/de/'));
// Check hreflang tags don't have duplicate locale prefixes
const deIdxHreflang = readSite(dir, 'de/index.html');
const hreflangPattern = /hreflang="[^"]*" href="([^"]*)"/g;
const hreflangUrls = [...deIdxHreflang.matchAll(hreflangPattern)].map(m => m[1]);
assert('hreflang EN points to root', hreflangUrls.some(u => u === '/'));
assert('hreflang DE points to /de/', hreflangUrls.some(u => u === '/de/'));
assert('no duplicate locale in hreflang', !hreflangUrls.some(u => u.includes('/de/de/')));
}
// ─── TEST 29: Hreflang Tags Consistency ───
console.log('\n🏷️ Test 29: Hreflang Tags Consistency');
{
const dir = setup('29-hreflang-tags');
writeFile(dir, 'docmd.config.js', `module.exports = {
title: 'Hreflang Test',
url: 'https://example.com',
src: 'docs',
i18n: { default: 'en', locales: [
{ id: 'en', label: 'English' },
{ id: 'fr', label: 'Français' }
]}
};`);
writeFile(dir, 'docs/en/index.md', '# English Home');
writeFile(dir, 'docs/en/guide.md', '# English Guide');
writeFile(dir, 'docs/en/navigation.json', '[{"title":"Home","path":"/"},{"title":"Guide","path":"/guide/"}]');
writeFile(dir, 'docs/fr/index.md', '# French Home');
writeFile(dir, 'docs/fr/guide.md', '# French Guide');
writeFile(dir, 'docs/fr/navigation.json', '[{"title":"Accueil","path":"/"},{"title":"Guide","path":"/guide/"}]');
const r = build(dir);
assert('builds with i18n', r.ok);
// Check hreflang on English root
const enIdx = readSite(dir, 'index.html');
assert('EN root has hreflang for EN', enIdx?.includes('hreflang="en" href="/"'));
assert('EN root has hreflang for FR', enIdx?.includes('hreflang="fr" href="/fr/"'));
assert('EN root has x-default', enIdx?.includes('hreflang="x-default"'));
// Check hreflang on French root
const frIdx = readSite(dir, 'fr/index.html');
assert('FR root has hreflang for EN', frIdx?.includes('hreflang="en" href="/"'));
assert('FR root has hreflang for FR', frIdx?.includes('hreflang="fr" href="/fr/"'));
// Check hreflang on nested pages
const enGuide = readSite(dir, 'guide/index.html');
const frGuide = readSite(dir, 'fr/guide/index.html');
assert('EN guide has correct hreflang', enGuide?.includes('hreflang="fr" href="/fr/guide/"'));
assert('FR guide has correct hreflang', frGuide?.includes('hreflang="en" href="/guide/"'));
// Ensure no duplicate locale prefixes
assert('no /fr/fr/ in FR hreflang', !frIdx?.includes('/fr/fr/'));
assert('no /fr/fr/ in FR guide hreflang', !frGuide?.includes('/fr/fr/'));
}
// ─── TEST 30: Workspaces (multi-project) ───
// Two sub-projects compiled into one site via a root workspace config.
// Replaces the standalone "Mega Integration" test file that lived at
// tests/mega-integration.test.js (deleted). The new Tests 30 + 31 cover
// what used to be in the legacy failsafe's "Mega Integration" section
// but inline alongside the other feature tests so the runner shows
// nine sections, not ten.
{
const dir = setup('features-30-workspaces');
fs.mkdirSync(path.join(dir, 'main/docs'), { recursive: true });
fs.mkdirSync(path.join(dir, 'api/docs'), { recursive: true });
writeFile(dir, 'main/docmd.config.js',
`export default { title: 'Main Docs', src: 'docs' }`);
writeFile(dir, 'main/docs/index.md', '# Main Home\n');
writeFile(dir, 'api/docmd.config.js',
`export default { title: 'API Reference', src: 'docs' }`);
writeFile(dir, 'api/docs/index.md', '# API Index\n');
writeFile(dir, 'docmd.config.js',
`export default {
workspace: {
projects: [
{ prefix: '/', src: 'main' },
{ prefix: '/api', src: 'api' }
]
}
}`);
build(dir);
assert('workspaces: main root built',
fs.existsSync(path.join(dir, 'site/index.html')));
assert('workspaces: api root built',
fs.existsSync(path.join(dir, 'site/api/index.html')));
const mainIdx = readSite(dir, 'index.html');
assert('workspaces: main page has its title',
mainIdx?.includes('Main Docs'));
const apiIdx = readSite(dir, 'api/index.html');
assert('workspaces: api page has its title',
apiIdx?.includes('API Reference'));
}
// ─── TEST 31: Mega integration (workspaces + i18n + versioning + plugins) ───
// One project that exercises EVERY docmd feature at once. Equivalent to
// the legacy failsafe's "Mega Integration Test (V5.0)" — assertions
// cover the artefacts every common plugin is supposed to produce and
// the cross-feature interactions (i18n + versioning + workspaces).
{
const dir = setup('features-31-mega');
fs.mkdirSync(path.join(dir, 'main/docs/en'), { recursive: true });
fs.mkdirSync(path.join(dir, 'main/docs/fr'), { recursive: true });
fs.mkdirSync(path.join(dir, 'main/docs-v1/en'), { recursive: true });
fs.mkdirSync(path.join(dir, 'main/assets/css'), { recursive: true });
fs.mkdirSync(path.join(dir, 'api/docs'), { recursive: true });
writeFile(dir, 'main/docmd.config.js',
`export default {
title: 'Mega Docs',
url: 'https://example.com',
src: 'docs',
out: 'site',
plugins: { math: {}, sitemap: {}, llms: {}, seo: {}, pwa: {} },
versions: { current: 'v2', all: [
{ id: 'v2', dir: 'docs' },
{ id: 'v1', dir: 'docs-v1', label: 'v1.0' }
]},
i18n: { default: 'en', locales: [
{ id: 'en', label: 'English' },
{ id: 'fr', label: 'Français' }
]}
}`);
writeFile(dir, 'api/docmd.config.js',
`export default {
title: 'API Reference',
url: 'https://example.com/api',
src: 'docs',
navigation: [{ title: 'Home', path: '/' }],
plugins: { search: {}, mermaid: {} }
}`);
writeFile(dir, 'docmd.config.js',
`export default {
workspace: {
projects: [
{ prefix: '/', src: 'main' },
{ prefix: '/api', src: 'api' }
]
}
}`);
writeFile(dir, 'main/docs/en/index.md',
'---\ntitle: Home\n---\n# Home\nWelcome.');
writeFile(dir, 'main/docs/en/guide.md',
'---\ntitle: Guide\n---\n# Guide\n## Steps\n');
writeFile(dir, 'main/docs/en/math.md',
'---\ntitle: Math\n---\n# Math\nInline $E = mc^2$.');
writeFile(dir, 'main/docs/en/mermaid.md',
'---\ntitle: Diagrams\n---\n# Diagrams\n```mermaid\ngraph TD\n A --> B\n```\n');
writeFile(dir, 'main/docs/fr/index.md', '# Accueil\n');
writeFile(dir, 'main/docs-v1/en/index.md', '# v1 Home\nOld.');
writeFile(dir, 'api/docs/index.md',
'---\ntitle: API\n---\n# API\n## Endpoints\n');
writeFile(dir, 'main/assets/css/custom.css', '.x{color:red}');
build(dir);
assert('mega: main index built',
fs.existsSync(path.join(dir, 'site/index.html')));
assert('mega: main guide built',
fs.existsSync(path.join(dir, 'site/guide/index.html')));
assert('mega: api index built',
fs.existsSync(path.join(dir, 'site/api/index.html')));
assert('mega: French locale index built',
fs.existsSync(path.join(dir, 'site/fr/index.html')));
assert('mega: v1 index built',
fs.existsSync(path.join(dir, 'site/v1/index.html')));
assert('mega: v1 guide absent',
!fs.existsSync(path.join(dir, 'site/v1/guide/index.html')));
assert('mega: search-index generated',
fs.existsSync(path.join(dir, 'site/search-index.json')));
assert('mega: sitemap generated',
fs.existsSync(path.join(dir, 'site/sitemap.xml')));
assert('mega: llms.txt generated',
fs.existsSync(path.join(dir, 'site/llms.txt')));
assert('mega: llms-full.txt generated',
fs.existsSync(path.join(dir, 'site/llms-full.txt')));
assert('mega: PWA manifest generated',
fs.existsSync(path.join(dir, 'site/manifest.webmanifest')) ||
fs.existsSync(path.join(dir, 'site/manifest.json')));
assert('mega: service worker generated',
fs.existsSync(path.join(dir, 'site/service-worker.js')) ||
fs.existsSync(path.join(dir, 'site/sw.js')));
assert('mega: sitemap has root URL',
readSite(dir, 'sitemap.xml')?.includes('example.com'));
assert('mega: sitemap has French URL',
readSite(dir, 'sitemap.xml')?.includes('/fr/'));
assert('mega: llms.txt has links',
readSite(dir, 'llms.txt')?.includes('[') &&
readSite(dir, 'llms.txt')?.includes(']('));
assert('mega: math page has katex',
readSite(dir, 'math/index.html')?.includes('katex'));
assert('mega: mermaid page has diagram',
readSite(dir, 'mermaid/index.html')?.includes('class="mermaid"'));
assert('mega: index has SEO meta tags',
!!readSite(dir, 'index.html')?.match(/<meta\s+name=["']description["']/i));
assert('mega: custom assets copied',
fs.existsSync(path.join(dir, 'site/assets/css/custom.css')));
}
// ─── SUMMARY ───
console.log('\n' + '═'.repeat(50));
console.log(` ${passed} passed, ${failed} failed out of ${passed + failed} assertions`);
if (failures.length > 0) {
console.log(`\n Failures:`);
failures.forEach(f => console.log(` ${FAIL} ${f}`));
}
console.log('═'.repeat(50) + '\n');
process.exit(failed > 0 ? 1 : 0);