2549704bc1
* docs: expand public user and contributor guides * docs: address public guide review feedback * docs: align web e2e coverage guidance * test: cover collapsed docs references * fix: validate shortcut docs references * fix: parse balanced docs links * fix: handle indented docs code blocks
444 lines
10 KiB
JavaScript
444 lines
10 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import test, { after } from "node:test";
|
|
import { validatePublicDocs } from "./validate-public-docs.mjs";
|
|
|
|
const tempDirs = [];
|
|
|
|
/**
|
|
* Create an isolated published-docs fixture.
|
|
*
|
|
* @param {Record<string, string>} files Fixture files keyed by relative path.
|
|
* @param {{pages: string[]}} meta Navigation metadata.
|
|
* @returns {Promise<string>} Temporary fixture directory.
|
|
*/
|
|
async function createDocs(files, meta) {
|
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "kandev-public-docs-"));
|
|
tempDirs.push(dir);
|
|
await Promise.all(
|
|
Object.entries(files).map(async ([file, content]) => {
|
|
const target = path.join(dir, file);
|
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
await fs.writeFile(target, content);
|
|
}),
|
|
);
|
|
await fs.writeFile(path.join(dir, "meta.json"), JSON.stringify(meta));
|
|
return dir;
|
|
}
|
|
|
|
after(async () => {
|
|
await Promise.all(
|
|
tempDirs.map((dir) => fs.rm(dir, { recursive: true, force: true })),
|
|
);
|
|
});
|
|
|
|
const validPage = `---
|
|
title: "Overview"
|
|
description: "Start using Kandev."
|
|
---
|
|
|
|
# Kandev
|
|
|
|
Page body.
|
|
`;
|
|
|
|
test("accepts explicitly ordered pages with required frontmatter", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"README.md": "# Contributing",
|
|
"index.md": validPage,
|
|
"cli.md": validPage,
|
|
},
|
|
{ title: "Kandev Docs", pages: ["---Start---", "index", "cli"] },
|
|
);
|
|
|
|
await assert.doesNotReject(validatePublicDocs(dir));
|
|
});
|
|
|
|
test("rejects published pages omitted from meta.json", async () => {
|
|
const dir = await createDocs(
|
|
{ "index.md": validPage, "cli.md": validPage },
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.rejects(
|
|
validatePublicDocs(dir),
|
|
/meta.json is missing published page: cli/,
|
|
);
|
|
});
|
|
|
|
test("rejects meta.json entries without a matching file", async () => {
|
|
const dir = await createDocs(
|
|
{ "index.md": validPage },
|
|
{ pages: ["index", "nonexistent"] },
|
|
);
|
|
|
|
await assert.rejects(
|
|
validatePublicDocs(dir),
|
|
/meta.json references unknown page: nonexistent/,
|
|
);
|
|
});
|
|
|
|
test("rejects unsupported link decorations as unknown pages", async () => {
|
|
const dir = await createDocs(
|
|
{ "index.md": validPage },
|
|
{ pages: ["index", "external:[Support](https://example.com)"] },
|
|
);
|
|
|
|
await assert.rejects(
|
|
validatePublicDocs(dir),
|
|
/meta.json references unknown page: external:\[Support\]\(https:\/\/example.com\)/,
|
|
);
|
|
});
|
|
|
|
test("rejects duplicate entries in meta.json", async () => {
|
|
const dir = await createDocs(
|
|
{ "index.md": validPage },
|
|
{ pages: ["index", "index"] },
|
|
);
|
|
|
|
await assert.rejects(
|
|
validatePublicDocs(dir),
|
|
/meta.json lists page more than once: index/,
|
|
);
|
|
});
|
|
|
|
test("rejects files that resolve to the same published slug", async () => {
|
|
const dir = await createDocs(
|
|
{ "foo.md": validPage, "foo/index.md": validPage },
|
|
{ pages: ["foo"] },
|
|
);
|
|
|
|
await assert.rejects(
|
|
validatePublicDocs(dir),
|
|
/multiple published files resolve to slug foo: foo.md, foo\/index.md/,
|
|
);
|
|
});
|
|
|
|
test("accepts single-character frontmatter values", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": `---
|
|
title: x
|
|
description: y
|
|
---
|
|
|
|
# X
|
|
`,
|
|
},
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.doesNotReject(validatePublicDocs(dir));
|
|
});
|
|
|
|
test("rejects published pages without title and description frontmatter", async () => {
|
|
const dir = await createDocs(
|
|
{ "index.md": "# Kandev\n" },
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.rejects(
|
|
validatePublicDocs(dir),
|
|
/index.md must start with YAML frontmatter containing title and description/,
|
|
);
|
|
});
|
|
|
|
test("accepts existing relative page and asset links", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace(
|
|
"Page body.",
|
|
"[Guide](guide.md)\n\n",
|
|
),
|
|
"guide.md": validPage,
|
|
"assets/diagram.png": "not-a-real-png",
|
|
},
|
|
{ pages: ["index", "guide"] },
|
|
);
|
|
|
|
await assert.doesNotReject(validatePublicDocs(dir));
|
|
});
|
|
|
|
test("rejects a broken local page link", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace("Page body.", "[Missing](missing.md)"),
|
|
},
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.rejects(
|
|
validatePublicDocs(dir),
|
|
/index.md links to missing local target: missing.md/,
|
|
);
|
|
});
|
|
|
|
test("rejects a broken local image", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace(
|
|
"Page body.",
|
|
"",
|
|
),
|
|
},
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.rejects(
|
|
validatePublicDocs(dir),
|
|
/index.md links to missing local target: assets\/missing.png/,
|
|
);
|
|
});
|
|
|
|
test("rejects a broken local image nested inside a link", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace(
|
|
"Page body.",
|
|
"[](guide.md)",
|
|
),
|
|
"guide.md": validPage,
|
|
},
|
|
{ pages: ["index", "guide"] },
|
|
);
|
|
|
|
await assert.rejects(
|
|
validatePublicDocs(dir),
|
|
/index.md links to missing local target: assets\/missing.png/,
|
|
);
|
|
});
|
|
|
|
test("ignores external, anchor-only, and fenced-code links", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace(
|
|
"Page body.",
|
|
`[External](https://example.com/docs)\n\n[Section](#section)\n\n\`\`\`md
|
|
[Example only](does-not-exist.md)
|
|
\`\`\``,
|
|
),
|
|
},
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.doesNotReject(validatePublicDocs(dir));
|
|
});
|
|
|
|
test("ignores links inside inline code and nested shorter fences", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace(
|
|
"Page body.",
|
|
`Use \`[example](inline-missing.md)\` in prose.
|
|
|
|
\`\`\`\`md
|
|
\`\`\`md
|
|
[Example only](fenced-missing.md)
|
|
\`\`\`
|
|
\`\`\`\``,
|
|
),
|
|
},
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.doesNotReject(validatePublicDocs(dir));
|
|
});
|
|
|
|
test("ignores links inside indented code blocks", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace(
|
|
"Page body.",
|
|
`Example only:
|
|
|
|
[Missing](indented-missing.md)`,
|
|
),
|
|
},
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.doesNotReject(validatePublicDocs(dir));
|
|
});
|
|
|
|
test("ignores indented code blocks after a heading", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace(
|
|
"Page body.",
|
|
`## Example
|
|
[Missing](heading-code-missing.md)`,
|
|
),
|
|
},
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.doesNotReject(validatePublicDocs(dir));
|
|
});
|
|
|
|
test("ignores indented code blocks nested in list items", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace(
|
|
"Page body.",
|
|
`- Example only:
|
|
|
|
[Missing](list-code-missing.md)`,
|
|
),
|
|
},
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.doesNotReject(validatePublicDocs(dir));
|
|
});
|
|
|
|
test("validates links in indented list paragraphs", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace(
|
|
"Page body.",
|
|
`- Related material:
|
|
|
|
[Missing](list-paragraph-missing.md)`,
|
|
),
|
|
},
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.rejects(
|
|
validatePublicDocs(dir),
|
|
/index.md links to missing local target: list-paragraph-missing.md/,
|
|
);
|
|
});
|
|
|
|
test("accepts escaped parentheses in local link destinations", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace("Page body.", "[Guide](guide\\(1\\).md)"),
|
|
"guide(1).md": validPage,
|
|
},
|
|
{ pages: ["index", "guide(1)"] },
|
|
);
|
|
|
|
await assert.doesNotReject(validatePublicDocs(dir));
|
|
});
|
|
|
|
test("accepts balanced parentheses in local link destinations", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace("Page body.", "[Guide](guide(1).md)"),
|
|
"guide(1).md": validPage,
|
|
},
|
|
{ pages: ["index", "guide(1)"] },
|
|
);
|
|
|
|
await assert.doesNotReject(validatePublicDocs(dir));
|
|
});
|
|
|
|
test("validates reference-style link definitions", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace(
|
|
"Page body.",
|
|
"[Guide][user guide]\n\n[user guide]: missing.md",
|
|
),
|
|
},
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.rejects(
|
|
validatePublicDocs(dir),
|
|
/index.md links to missing local target: missing.md/,
|
|
);
|
|
});
|
|
|
|
test("rejects undefined full reference-style links", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace("Page body.", "[Guide][missing guide]"),
|
|
},
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.rejects(
|
|
validatePublicDocs(dir),
|
|
/index.md uses undefined Markdown reference: missing guide/,
|
|
);
|
|
});
|
|
|
|
test("validates collapsed reference-style links", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace(
|
|
"Page body.",
|
|
"[Guide][]\n\n[guide]: missing.md",
|
|
),
|
|
},
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.rejects(
|
|
validatePublicDocs(dir),
|
|
/index.md links to missing local target: missing.md/,
|
|
);
|
|
});
|
|
|
|
test("rejects undefined shortcut reference-style links", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace("Page body.", "Read the [Guide]."),
|
|
},
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.rejects(
|
|
validatePublicDocs(dir),
|
|
/index.md uses undefined Markdown reference: guide/,
|
|
);
|
|
});
|
|
|
|
test("ignores non-reference bracket syntax", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace(
|
|
"Page body.",
|
|
"> [!WARNING]\n\n- [x] Done\n\n[^note]\n\n\\[Literal]",
|
|
),
|
|
},
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.doesNotReject(validatePublicDocs(dir));
|
|
});
|
|
|
|
test("checks local links in README files", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"README.md": "# Contributing\n\n[Missing](missing.md)",
|
|
"index.md": validPage,
|
|
},
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.rejects(
|
|
validatePublicDocs(dir),
|
|
/README.md links to missing local target: missing.md/,
|
|
);
|
|
});
|
|
|
|
test("rejects site-root links because public docs use relative sources", async () => {
|
|
const dir = await createDocs(
|
|
{
|
|
"index.md": validPage.replace("Page body.", "[Guide](/docs/guide)"),
|
|
},
|
|
{ pages: ["index"] },
|
|
);
|
|
|
|
await assert.rejects(
|
|
validatePublicDocs(dir),
|
|
/index.md uses a site-root link instead of a relative source link: \/docs\/guide/,
|
|
);
|
|
});
|