carbon-tokenization / backend /tests /publisher.test.ts
tfrere's picture
tfrere HF Staff
feat(editor): Iframe embed component for remote URLs
0c69852
Raw
History Blame Contribute Delete
9.19 kB
/**
* Publisher Pipeline Tests (Section 1 of TESTS.md)
*
* Tests Y.Doc extraction, HTML generation, post-processing,
* and idempotency of the publish pipeline.
*/
import { describe, it, expect } from "vitest";
import { renderArticleHTML, type PublishMeta } from "../src/publisher/html-renderer.js";
import { createTestYDoc, FULL_DOC_OPTIONS, MINIMAL_DOC_JSON } from "./helpers.js";
function buildMeta(options: typeof FULL_DOC_OPTIONS): PublishMeta {
const authors = options.authors ?? [];
const affiliations = options.affiliations ?? [];
return {
title: options.title || "Untitled",
subtitle: options.subtitle || undefined,
description: options.description || "",
authors: authors.map((a) => ({
name: a.name,
url: a.url,
affiliationIndices: a.affiliations || [],
affiliationNames: (a.affiliations || [])
.map((idx) => affiliations[idx - 1]?.name)
.filter(Boolean) as string[],
})),
affiliations: affiliations.map((a) => ({ name: a.name, url: a.url })),
date: options.published || new Date().toISOString(),
doi: options.doi || undefined,
licence: options.licence || undefined,
};
}
const EMPTY_CSS = {
variables: ":root { --text-color: #000; }",
reset: "",
base: "",
layout: "",
print: "",
editorTokens: "",
article: "",
components: "",
publisher: "",
};
// ── 1.1 Y.Doc Extraction ──────────────────────────────────────────
describe("1.1 Y.Doc extraction", () => {
it("1.1.1 extracts frontmatter from Y.Doc", async () => {
const ydoc = createTestYDoc(FULL_DOC_OPTIONS);
const metaMap = ydoc.getMap("frontmatter");
expect(metaMap.get("title")).toBe("Test Article Title");
expect(metaMap.get("subtitle")).toBe("A subtitle for testing");
expect(metaMap.get("description")).toBe("This is a test article description.");
expect(metaMap.get("doi")).toBe("10.1234/test.2025");
const authors = ydoc.getArray("frontmatter.authors").toArray();
expect(authors).toHaveLength(2);
expect((authors[0] as any).name).toBe("Alice Smith");
const affiliations = ydoc.getArray("frontmatter.affiliations").toArray();
expect(affiliations).toHaveLength(2);
expect((affiliations[0] as any).name).toBe("University of Testing");
});
it("1.1.2 empty doc does not throw", async () => {
const ydoc = createTestYDoc({ title: "Empty" });
const fragment = ydoc.getXmlFragment("default");
expect(fragment.length).toBe(0);
expect(ydoc.getMap("frontmatter").get("title")).toBe("Empty");
});
it("1.1.3 extracts citations from Y.Map", async () => {
const ydoc = createTestYDoc(FULL_DOC_OPTIONS);
const citationsMap = ydoc.getMap("citations");
const entries: Record<string, unknown> = {};
citationsMap.forEach((value, key) => {
entries[key] = value;
});
expect(entries).toHaveProperty("smith2024");
expect((entries.smith2024 as any).title).toBe("On Testing");
});
});
// ── 1.2 HTML Generation ───────────────────────────────────────────
describe("1.2 HTML generation", () => {
it("1.2.1 produces valid self-contained HTML", async () => {
const meta = buildMeta(FULL_DOC_OPTIONS);
const html = await renderArticleHTML(MINIMAL_DOC_JSON, meta, EMPTY_CSS);
expect(html).toContain("<!DOCTYPE html>");
expect(html).toContain("<html");
expect(html).toContain("</html>");
expect(html).toContain("<style>");
});
it("1.2.3 generates TOC placeholder", async () => {
const meta = buildMeta(FULL_DOC_OPTIONS);
const html = await renderArticleHTML(MINIMAL_DOC_JSON, meta, EMPTY_CSS);
expect(html).toContain('aria-label="Table of Contents"');
expect(html).toContain("toc-placeholder");
});
it("1.2.4 includes theme toggle", async () => {
const meta = buildMeta(FULL_DOC_OPTIONS);
const html = await renderArticleHTML(MINIMAL_DOC_JSON, meta, EMPTY_CSS);
expect(html).toContain("theme-toggle");
expect(html).toContain("icon--sun");
expect(html).toContain("icon--moon");
});
it("1.2.5 renders bibliography placeholder", async () => {
const docWithBiblio = {
type: "doc",
content: [
{ type: "paragraph", content: [{ type: "text", text: "Hello" }] },
{ type: "bibliography", attrs: { renderedHtml: "<ol><li>Smith 2024</li></ol>" } },
],
};
const meta = buildMeta(FULL_DOC_OPTIONS);
const html = await renderArticleHTML(docWithBiblio, meta, EMPTY_CSS);
expect(html).toContain("bibliography");
expect(html).toContain("Smith 2024");
});
});
// ── 1.3 Post-processing ──────────────────────────────────────────
describe("1.3 Post-processing", () => {
const simpleMeta: PublishMeta = {
title: "Test", description: "", authors: [], affiliations: [],
date: "2025-01-01",
};
it("1.3.1 accordion rendered with data-component attribute", async () => {
const docJson = {
type: "doc",
content: [
{
type: "accordion",
attrs: { title: "Click me", open: false },
content: [
{ type: "paragraph", content: [{ type: "text", text: "Hidden content" }] },
],
},
],
};
const html = await renderArticleHTML(docJson, simpleMeta, EMPTY_CSS);
// The accordion is rendered with data-component="accordion" and data-title attribute
// Post-processing converts shallow divs to <details>; nested content may prevent the
// regex from matching. Either way, the accordion data is present in the output.
expect(html).toContain('data-component="accordion"');
expect(html).toContain("Hidden content");
});
it("1.3.2 transforms htmlEmbed to iframe", async () => {
const docJson = {
type: "doc",
content: [
{
type: "htmlEmbed",
attrs: { src: "chart.html", title: "My Chart", desc: "", wide: false, downloadable: false },
},
],
};
const html = await renderArticleHTML(docJson, simpleMeta, EMPTY_CSS);
// htmlEmbed is rendered as a div with data-component="htmlEmbed" and data-src
// The post-processor converts it to an iframe
expect(html).toContain("chart.html");
});
it("1.3.2b transforms iframe component to figure + iframe with src", async () => {
const docJson = {
type: "doc",
content: [
{
type: "iframe",
attrs: {
src: "https://my-space.hf.space/",
title: "Tokenization demo",
desc: "Interactive widget",
height: "500",
wide: false,
},
},
],
};
const html = await renderArticleHTML(docJson, simpleMeta, EMPTY_CSS);
expect(html).toMatch(/<figure[^>]*\bclass="html-embed"/);
expect(html).toContain('data-iframe-src="https://my-space.hf.space/"');
expect(html).toContain('src="https://my-space.hf.space/"');
expect(html).toContain('height:500px');
expect(html).toContain('title="Tokenization demo"');
expect(html).toContain("Interactive widget");
// Placeholder div must be gone
expect(html).not.toContain('data-component="iframe"');
});
it("1.3.2c drops iframe component without src", async () => {
const docJson = {
type: "doc",
content: [
{
type: "iframe",
attrs: { src: "", title: "Empty", desc: "", height: "600", wide: false },
},
],
};
const html = await renderArticleHTML(docJson, simpleMeta, EMPTY_CSS);
expect(html).not.toContain('data-component="iframe"');
expect(html).not.toContain('<iframe');
});
it("1.3.2d iframe with wide flag gets the wide modifier class", async () => {
const docJson = {
type: "doc",
content: [
{
type: "iframe",
attrs: { src: "https://example.com", title: "", desc: "", height: "600", wide: true },
},
],
};
const html = await renderArticleHTML(docJson, simpleMeta, EMPTY_CSS);
expect(html).toMatch(/<figure[^>]*\bclass="html-embed html-embed--wide"/);
});
it("1.3.3 transforms mermaid to pre block", async () => {
const docJson = {
type: "doc",
content: [
{
type: "mermaid",
attrs: { code: "graph TD; A-->B;" },
},
],
};
const html = await renderArticleHTML(docJson, simpleMeta, EMPTY_CSS);
expect(html).toContain('<pre class="mermaid">');
expect(html).toContain("graph TD");
});
});
// ── 1.4 Publish idempotency ──────────────────────────────────────
describe("1.4 Publish idempotency", () => {
it("1.4.1 rendering twice gives same result", async () => {
const meta = buildMeta(FULL_DOC_OPTIONS);
const html1 = await renderArticleHTML(MINIMAL_DOC_JSON, meta, EMPTY_CSS);
const html2 = await renderArticleHTML(MINIMAL_DOC_JSON, meta, EMPTY_CSS);
expect(html1).toBe(html2);
});
});