Files
SnapOtter/tests/unit/infra/docs-tool-names.test.ts
T

103 lines
3.5 KiB
TypeScript
Raw Normal View History

// @vitest-environment node
import { globSync, readFileSync } from "node:fs";
import path from "node:path";
import { TOOLS } from "@snapotter/shared";
import { describe, expect, it } from "vitest";
/**
* PR #520 renamed 18 tools across the app and the tool pages, but the VitePress
* sidebar kept its old labels for another two releases: 19 entries in the nav
* under names the product no longer used. One page H1 was missed too.
*
* Tool leaves in the sidebar are deliberately never translated (see
* localizeSidebar in apps/docs/.vitepress/config.mts), so checking the English
* config covers all 21 locales.
*/
const ROOT = path.resolve(__dirname, "../../..");
const NAME_BY_ID = new Map(TOOLS.map((t) => [t.id, t.name]));
/**
* Shipped tools with no docs page. Adding a page means deleting the id here.
* Adding a tool without a page means this list grows, which is the point.
*/
const UNDOCUMENTED = new Set(["remove-gif-background", "rounded-crop"]);
interface Leaf {
label: string;
id: string;
line: number;
}
function sidebarToolLeaves(): Leaf[] {
const src = readFileSync(path.join(ROOT, "apps/docs/.vitepress/config.mts"), "utf8");
const leaves: Leaf[] = [];
src.split("\n").forEach((line, i) => {
// The single top-nav entry sits at six spaces; sidebar leaves are deeper.
if (!/^\s{10,}\{ text: "/.test(line)) return;
const m = line.match(
/\{ text: "([^"]+)", link: "\/tools\/(?:image|video|audio|pdf|files)\/([a-z0-9-]+)" \}/,
);
if (m) leaves.push({ label: m[1], id: m[2], line: i + 1 });
});
return leaves;
}
const LEAVES = sidebarToolLeaves();
const PAGES = globSync("apps/docs/tools/*/*.md", { cwd: ROOT })
.filter((f) => !f.endsWith("conversion-presets.md"))
.sort();
describe("docs sidebar", () => {
it("found the tool leaves", () => {
expect(LEAVES.length).toBeGreaterThan(100);
});
it("links only to tools the catalog ships", () => {
expect(LEAVES.filter((l) => !NAME_BY_ID.has(l.id)).map((l) => l.id)).toEqual([]);
});
it("labels every leaf with the tool's catalog name", () => {
const wrong = LEAVES.filter((l) => NAME_BY_ID.get(l.id) !== l.label).map(
(l) => `config.mts:${l.line} "${l.label}" should be "${NAME_BY_ID.get(l.id)}"`,
);
expect(wrong).toEqual([]);
});
});
describe("tool pages", () => {
it("found the pages", () => {
expect(PAGES.length).toBeGreaterThan(100);
});
it("titles every page with the tool's catalog name", () => {
const wrong: string[] = [];
for (const file of PAGES) {
const id = path.basename(file, ".md");
const name = NAME_BY_ID.get(id);
if (!name) {
wrong.push(`${file} has no tool with id ${id}`);
continue;
}
const h1 = readFileSync(path.join(ROOT, file), "utf8")
.split("\n")
.find((l) => l.startsWith("# "))
?.replace(/^#\s+/, "")
.replace(/\s*\{#.*\}$/, "")
.trim();
if (h1 !== name) wrong.push(`${file} H1 "${h1}" should be "${name}"`);
}
expect(wrong).toEqual([]);
});
it("tracks exactly the tools that still have no page", () => {
const documented = new Set(PAGES.map((f) => path.basename(f, ".md")));
const missing = TOOLS.filter((t) => !documented.has(t.id))
.map((t) => t.id)
// The 83 conversion presets are covered collectively by
// apps/docs/tools/conversion-presets.md rather than one page each.
.filter((id) => !id.includes("-to-"));
expect(missing.sort()).toEqual([...UNDOCUMENTED].sort());
});
});