mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 05:51:44 +08:00
test: migrate tests from centralized mjs to colocated TypeScript
Move test files from tests/ directory to colocate with source code, convert from .mjs to .ts using tsx runner, add workspaces and npm cache to CI workflow.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
extractSummaryFromBody,
|
||||
extractTitleFromMarkdown,
|
||||
parseFrontmatter,
|
||||
pickFirstString,
|
||||
serializeFrontmatter,
|
||||
stripWrappingQuotes,
|
||||
toFrontmatterString,
|
||||
} from "./content.ts";
|
||||
|
||||
test("parseFrontmatter extracts YAML fields and strips wrapping quotes", () => {
|
||||
const input = `---
|
||||
title: "Hello World"
|
||||
author: ‘Baoyu’
|
||||
summary: plain text
|
||||
---
|
||||
# Heading
|
||||
|
||||
Body`;
|
||||
|
||||
const result = parseFrontmatter(input);
|
||||
|
||||
assert.deepEqual(result.frontmatter, {
|
||||
title: "Hello World",
|
||||
author: "Baoyu",
|
||||
summary: "plain text",
|
||||
});
|
||||
assert.match(result.body, /^# Heading/);
|
||||
});
|
||||
|
||||
test("parseFrontmatter returns original content when no frontmatter exists", () => {
|
||||
const input = "# No frontmatter";
|
||||
assert.deepEqual(parseFrontmatter(input), {
|
||||
frontmatter: {},
|
||||
body: input,
|
||||
});
|
||||
});
|
||||
|
||||
test("serializeFrontmatter renders YAML only when fields exist", () => {
|
||||
assert.equal(serializeFrontmatter({}), "");
|
||||
assert.equal(
|
||||
serializeFrontmatter({ title: "Hello", author: "Baoyu" }),
|
||||
"---\ntitle: Hello\nauthor: Baoyu\n---\n",
|
||||
);
|
||||
});
|
||||
|
||||
test("quote and frontmatter string helpers normalize mixed scalar values", () => {
|
||||
assert.equal(stripWrappingQuotes(`" quoted "`), "quoted");
|
||||
assert.equal(stripWrappingQuotes("“ 中文标题 ”"), "中文标题");
|
||||
assert.equal(stripWrappingQuotes("plain"), "plain");
|
||||
|
||||
assert.equal(toFrontmatterString("'hello'"), "hello");
|
||||
assert.equal(toFrontmatterString(42), "42");
|
||||
assert.equal(toFrontmatterString(false), "false");
|
||||
assert.equal(toFrontmatterString({}), undefined);
|
||||
|
||||
assert.equal(
|
||||
pickFirstString({ summary: 123, title: "" }, ["title", "summary"]),
|
||||
"123",
|
||||
);
|
||||
});
|
||||
|
||||
test("markdown title and summary extraction skip non-body content and clean formatting", () => {
|
||||
const markdown = `
|
||||

|
||||
## “My Title”
|
||||
|
||||
Body paragraph
|
||||
`;
|
||||
assert.equal(extractTitleFromMarkdown(markdown), "My Title");
|
||||
|
||||
const summary = extractSummaryFromBody(
|
||||
`
|
||||
# Heading
|
||||
> quote
|
||||
- list
|
||||
1. ordered
|
||||
\`\`\`
|
||||
code
|
||||
\`\`\`
|
||||
This is **the first paragraph** with [a link](https://example.com) and \`inline code\` that should be summarized cleanly.
|
||||
`,
|
||||
70,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
summary,
|
||||
"This is the first paragraph with a link and inline code that should...",
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import test, { type TestContext } from "node:test";
|
||||
|
||||
import { COLOR_PRESETS, FONT_FAMILY_MAP } from "./constants.ts";
|
||||
import {
|
||||
buildMarkdownDocumentMeta,
|
||||
formatTimestamp,
|
||||
resolveColorToken,
|
||||
resolveFontFamilyToken,
|
||||
resolveMarkdownStyle,
|
||||
resolveRenderOptions,
|
||||
} from "./document.ts";
|
||||
|
||||
function useCwd(t: TestContext, cwd: string): void {
|
||||
const previous = process.cwd();
|
||||
process.chdir(cwd);
|
||||
t.after(() => {
|
||||
process.chdir(previous);
|
||||
});
|
||||
}
|
||||
|
||||
async function makeTempDir(prefix: string): Promise<string> {
|
||||
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||
}
|
||||
|
||||
test("document token resolvers map known presets and allow passthrough values", () => {
|
||||
assert.equal(resolveColorToken("green"), COLOR_PRESETS.green);
|
||||
assert.equal(resolveColorToken("#123456"), "#123456");
|
||||
assert.equal(resolveColorToken(), undefined);
|
||||
|
||||
assert.equal(resolveFontFamilyToken("mono"), FONT_FAMILY_MAP.mono);
|
||||
assert.equal(resolveFontFamilyToken("Custom Font"), "Custom Font");
|
||||
assert.equal(resolveFontFamilyToken(), undefined);
|
||||
});
|
||||
|
||||
test("formatTimestamp uses compact sortable datetime output", () => {
|
||||
const date = new Date("2026-03-13T21:04:05.000Z");
|
||||
const pad = (value: number) => String(value).padStart(2, "0");
|
||||
const expected = `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(
|
||||
date.getDate(),
|
||||
)}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
||||
|
||||
assert.equal(formatTimestamp(date), expected);
|
||||
});
|
||||
|
||||
test("buildMarkdownDocumentMeta prefers frontmatter and falls back to markdown title and summary", () => {
|
||||
const metaFromYaml = buildMarkdownDocumentMeta(
|
||||
"# Markdown Title\n\nBody summary paragraph that should be ignored.",
|
||||
{
|
||||
title: `" YAML Title "`,
|
||||
author: "'Baoyu'",
|
||||
summary: `" YAML Summary "`,
|
||||
},
|
||||
"fallback",
|
||||
);
|
||||
|
||||
assert.deepEqual(metaFromYaml, {
|
||||
title: "YAML Title",
|
||||
author: "Baoyu",
|
||||
description: "YAML Summary",
|
||||
});
|
||||
|
||||
const metaFromMarkdown = buildMarkdownDocumentMeta(
|
||||
`## “Markdown Title”\n\nThis is the first body paragraph that should become the summary because it is long enough.`,
|
||||
{},
|
||||
"fallback",
|
||||
);
|
||||
|
||||
assert.equal(metaFromMarkdown.title, "Markdown Title");
|
||||
assert.match(metaFromMarkdown.description ?? "", /^This is the first body paragraph/);
|
||||
});
|
||||
|
||||
test("resolveMarkdownStyle merges theme defaults with explicit overrides", () => {
|
||||
const style = resolveMarkdownStyle({
|
||||
theme: "modern",
|
||||
primaryColor: "#112233",
|
||||
fontFamily: "Custom Sans",
|
||||
});
|
||||
|
||||
assert.equal(style.primaryColor, "#112233");
|
||||
assert.equal(style.fontFamily, "Custom Sans");
|
||||
assert.equal(style.fontSize, "15px");
|
||||
assert.equal(style.containerBg, "rgba(250, 249, 245, 1)");
|
||||
});
|
||||
|
||||
test("resolveRenderOptions loads workspace EXTEND settings and lets explicit options win", async (t) => {
|
||||
const root = await makeTempDir("baoyu-md-render-options-");
|
||||
useCwd(t, root);
|
||||
|
||||
const extendPath = path.join(
|
||||
root,
|
||||
".baoyu-skills",
|
||||
"baoyu-markdown-to-html",
|
||||
"EXTEND.md",
|
||||
);
|
||||
await fs.mkdir(path.dirname(extendPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
extendPath,
|
||||
`---
|
||||
default_theme: modern
|
||||
default_color: green
|
||||
default_font_family: mono
|
||||
default_font_size: 17
|
||||
default_code_theme: nord
|
||||
mac_code_block: false
|
||||
show_line_number: true
|
||||
cite: true
|
||||
count: true
|
||||
legend: title-alt
|
||||
keep_title: true
|
||||
---
|
||||
`,
|
||||
);
|
||||
|
||||
const fromExtend = resolveRenderOptions();
|
||||
assert.equal(fromExtend.theme, "modern");
|
||||
assert.equal(fromExtend.primaryColor, COLOR_PRESETS.green);
|
||||
assert.equal(fromExtend.fontFamily, FONT_FAMILY_MAP.mono);
|
||||
assert.equal(fromExtend.fontSize, "17px");
|
||||
assert.equal(fromExtend.codeTheme, "nord");
|
||||
assert.equal(fromExtend.isMacCodeBlock, false);
|
||||
assert.equal(fromExtend.isShowLineNumber, true);
|
||||
assert.equal(fromExtend.citeStatus, true);
|
||||
assert.equal(fromExtend.countStatus, true);
|
||||
assert.equal(fromExtend.legend, "title-alt");
|
||||
assert.equal(fromExtend.keepTitle, true);
|
||||
|
||||
const explicit = resolveRenderOptions({
|
||||
theme: "simple",
|
||||
fontSize: "18px",
|
||||
keepTitle: false,
|
||||
});
|
||||
assert.equal(explicit.theme, "simple");
|
||||
assert.equal(explicit.fontSize, "18px");
|
||||
assert.equal(explicit.keepTitle, false);
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { DEFAULT_STYLE } from "./constants.ts";
|
||||
import {
|
||||
buildCss,
|
||||
buildHtmlDocument,
|
||||
modifyHtmlStructure,
|
||||
normalizeCssText,
|
||||
normalizeInlineCss,
|
||||
removeFirstHeading,
|
||||
} from "./html-builder.ts";
|
||||
|
||||
test("buildCss injects style variables and concatenates base and theme CSS", () => {
|
||||
const css = buildCss("body { color: red; }", ".theme { color: blue; }");
|
||||
|
||||
assert.match(css, /--md-primary-color: #0F4C81;/);
|
||||
assert.match(css, /body \{ color: red; \}/);
|
||||
assert.match(css, /\.theme \{ color: blue; \}/);
|
||||
});
|
||||
|
||||
test("buildHtmlDocument includes optional meta tags and code theme CSS", () => {
|
||||
const html = buildHtmlDocument(
|
||||
{
|
||||
title: "Doc",
|
||||
author: "Baoyu",
|
||||
description: "Summary",
|
||||
},
|
||||
"body { color: red; }",
|
||||
"<article>Hello</article>",
|
||||
".hljs { color: blue; }",
|
||||
);
|
||||
|
||||
assert.match(html, /<title>Doc<\/title>/);
|
||||
assert.match(html, /meta name="author" content="Baoyu"/);
|
||||
assert.match(html, /meta name="description" content="Summary"/);
|
||||
assert.match(html, /<style>body \{ color: red; \}<\/style>/);
|
||||
assert.match(html, /<style>\.hljs \{ color: blue; \}<\/style>/);
|
||||
assert.match(html, /<article>Hello<\/article>/);
|
||||
});
|
||||
|
||||
test("normalizeCssText and normalizeInlineCss replace variables and strip declarations", () => {
|
||||
const rawCss = `
|
||||
:root { --md-primary-color: #000; --md-font-size: 12px; --foreground: 0 0% 5%; }
|
||||
.box { color: var(--md-primary-color); font-size: var(--md-font-size); background: hsl(var(--foreground)); }
|
||||
`;
|
||||
|
||||
const normalizedCss = normalizeCssText(rawCss, DEFAULT_STYLE);
|
||||
assert.match(normalizedCss, /color: #0F4C81/);
|
||||
assert.match(normalizedCss, /font-size: 16px/);
|
||||
assert.match(normalizedCss, /background: #3f3f3f/);
|
||||
assert.doesNotMatch(normalizedCss, /--md-primary-color/);
|
||||
|
||||
const normalizedHtml = normalizeInlineCss(
|
||||
`<style>${rawCss}</style><div style="color: var(--md-primary-color)"></div>`,
|
||||
DEFAULT_STYLE,
|
||||
);
|
||||
assert.match(normalizedHtml, /color: #0F4C81/);
|
||||
assert.doesNotMatch(normalizedHtml, /var\(--md-primary-color\)/);
|
||||
});
|
||||
|
||||
test("HTML structure helpers hoist nested lists and remove the first heading", () => {
|
||||
const nestedList = `<ul><li>Parent<ul><li>Child</li></ul></li></ul>`;
|
||||
assert.equal(
|
||||
modifyHtmlStructure(nestedList),
|
||||
`<ul><li>Parent</li><ul><li>Child</li></ul></ul>`,
|
||||
);
|
||||
|
||||
const html = `<h1>Title</h1><p>Intro</p><h2>Sub</h2>`;
|
||||
assert.equal(removeFirstHeading(html), `<p>Intro</p><h2>Sub</h2>`);
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
getImageExtension,
|
||||
replaceMarkdownImagesWithPlaceholders,
|
||||
resolveContentImages,
|
||||
resolveImagePath,
|
||||
} from "./images.ts";
|
||||
|
||||
async function makeTempDir(prefix: string): Promise<string> {
|
||||
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||
}
|
||||
|
||||
test("replaceMarkdownImagesWithPlaceholders rewrites markdown and tracks image metadata", () => {
|
||||
const result = replaceMarkdownImagesWithPlaceholders(
|
||||
`\n\nText\n\n`,
|
||||
"IMG_",
|
||||
);
|
||||
|
||||
assert.equal(result.markdown, `IMG_1\n\nText\n\nIMG_2`);
|
||||
assert.deepEqual(result.images, [
|
||||
{ alt: "cover", originalPath: "images/cover.png", placeholder: "IMG_1" },
|
||||
{ alt: "diagram", originalPath: "images/diagram.webp", placeholder: "IMG_2" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("image extension and local fallback resolution handle common path variants", async (t) => {
|
||||
assert.equal(getImageExtension("https://example.com/a.jpeg?x=1"), "jpeg");
|
||||
assert.equal(getImageExtension("/tmp/figure"), "png");
|
||||
|
||||
const root = await makeTempDir("baoyu-md-images-");
|
||||
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||
|
||||
const baseDir = path.join(root, "article");
|
||||
const tempDir = path.join(root, "tmp");
|
||||
await fs.mkdir(baseDir, { recursive: true });
|
||||
await fs.mkdir(tempDir, { recursive: true });
|
||||
await fs.writeFile(path.join(baseDir, "figure.webp"), "webp");
|
||||
|
||||
const resolved = await resolveImagePath("figure.png", baseDir, tempDir, "test");
|
||||
assert.equal(resolved, path.join(baseDir, "figure.webp"));
|
||||
});
|
||||
|
||||
test("resolveContentImages resolves image placeholders against the content directory", async (t) => {
|
||||
const root = await makeTempDir("baoyu-md-content-images-");
|
||||
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||
|
||||
const baseDir = path.join(root, "article");
|
||||
const tempDir = path.join(root, "tmp");
|
||||
await fs.mkdir(baseDir, { recursive: true });
|
||||
await fs.mkdir(tempDir, { recursive: true });
|
||||
await fs.writeFile(path.join(baseDir, "cover.png"), "png");
|
||||
|
||||
const resolved = await resolveContentImages(
|
||||
[
|
||||
{
|
||||
alt: "cover",
|
||||
originalPath: "cover.png",
|
||||
placeholder: "IMG_1",
|
||||
},
|
||||
],
|
||||
baseDir,
|
||||
tempDir,
|
||||
"test",
|
||||
);
|
||||
|
||||
assert.deepEqual(resolved, [
|
||||
{
|
||||
alt: "cover",
|
||||
originalPath: "cover.png",
|
||||
placeholder: "IMG_1",
|
||||
localPath: path.join(baseDir, "cover.png"),
|
||||
},
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user