Files
citation-engine/src/engine/rendering/html.test.ts
tegwick 78085e1eb3 Extract engine from citation-evidence umbrella (CENG-WP-0001)
Bootstrap @citation-evidence/engine as a standalone TypeScript package
with shared types and engine services copied from the umbrella MVP.
All 89 tests pass with lint and typecheck clean.
2026-06-22 18:02:05 +02:00

173 lines
5.8 KiB
TypeScript

/**
* HTML citation card renderer tests (CE-WP-0004-T03).
*
* Snapshots lock the output format defined in
* `docs/decisions/ADR-0007-citation-card-format.md`. Class names are
* part of the public contract — renaming any of them requires updating
* both this test and the ADR.
*/
import { describe, expect, it } from "vitest";
import type { Annotation } from "@shared/annotation";
import type { Document } from "@shared/document";
import type { EvidenceItem } from "@shared/evidence";
import type {
AnnotationId,
DocumentId,
EvidenceItemId,
RepresentationId,
} from "@shared/ids";
import { renderCitationCardHtml } from "./html";
const DOC_ID = "doc_2024-order" as DocumentId;
const REP_ID = "rep_2024-order" as RepresentationId;
const ANN_ID = "ann_para3" as AnnotationId;
const EV_ID = "ev_para3" as EvidenceItemId;
function makeDoc(overrides: Partial<Document> = {}): Document {
return {
id: DOC_ID,
title: "Order from 14 Mar 2024",
mediaType: "application/pdf",
createdAt: "2026-05-25T00:00:00.000Z",
updatedAt: "2026-05-25T00:00:00.000Z",
...overrides,
};
}
function makeAnn(overrides: Partial<Annotation> = {}): Annotation {
return {
id: ANN_ID,
documentId: DOC_ID,
representationId: REP_ID,
selectors: [],
quote: "Die Frist endet am 31. März 2024.",
normalizeVersion: 1,
createdAt: "2026-05-25T00:00:00.000Z",
updatedAt: "2026-05-25T00:00:00.000Z",
...overrides,
};
}
function makeEv(overrides: Partial<EvidenceItem> = {}): EvidenceItem {
return {
id: EV_ID,
annotationIds: [ANN_ID],
status: "candidate",
createdAt: "2026-05-25T00:00:00.000Z",
updatedAt: "2026-05-25T00:00:00.000Z",
...overrides,
};
}
describe("renderCitationCardHtml()", () => {
it("renders the full aside with quote, attribution, and commentary", () => {
const out = renderCitationCardHtml({
evidenceItem: makeEv({ commentary: "Deadline clause for the buyer." }),
document: makeDoc(),
annotation: makeAnn(),
});
expect(out).toMatchInlineSnapshot(`
"<aside class="citation-card">
<blockquote class="citation-card__quote">Die Frist endet am 31. März 2024.</blockquote>
<p class="citation-card__attribution"><cite class="citation-card__source">Order from 14 Mar 2024</cite> · <a class="citation-card__link" href="/viewer?document=doc_2024-order&amp;annotation=ann_para3">Open source</a></p>
<div class="citation-card__commentary">Deadline clause for the buyer.</div>
</aside>
"
`);
});
it("omits the commentary div when none is set", () => {
const out = renderCitationCardHtml({
evidenceItem: makeEv(),
document: makeDoc(),
annotation: makeAnn(),
});
expect(out).toMatchInlineSnapshot(`
"<aside class="citation-card">
<blockquote class="citation-card__quote">Die Frist endet am 31. März 2024.</blockquote>
<p class="citation-card__attribution"><cite class="citation-card__source">Order from 14 Mar 2024</cite> · <a class="citation-card__link" href="/viewer?document=doc_2024-order&amp;annotation=ann_para3">Open source</a></p>
</aside>
"
`);
});
it("converts \\n inside the quote to <br> for rich-text paste", () => {
const out = renderCitationCardHtml({
evidenceItem: makeEv(),
document: makeDoc(),
annotation: makeAnn({ quote: "Line one.\nLine two." }),
});
expect(out).toContain(
'<blockquote class="citation-card__quote">Line one.<br>Line two.</blockquote>',
);
});
it("HTML-escapes &, <, >, \", and ' in user-supplied text", () => {
const out = renderCitationCardHtml({
evidenceItem: makeEv({
commentary: `Notes: <script>alert("xss")</script> & 'untrusted'`,
}),
document: makeDoc({ title: `Order "draft" & more` }),
annotation: makeAnn({ quote: "<<value>> & 'inner'" }),
});
// Quote escaping
expect(out).toContain(
"<blockquote class=\"citation-card__quote\">&lt;&lt;value&gt;&gt; &amp; &#39;inner&#39;</blockquote>",
);
// Source label escaping
expect(out).toContain(
"<cite class=\"citation-card__source\">Order &quot;draft&quot; &amp; more</cite>",
);
// Commentary escaping — and especially: no raw <script>
expect(out).not.toContain("<script>");
expect(out).toContain(
"<div class=\"citation-card__commentary\">Notes: &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt; &amp; &#39;untrusted&#39;</div>",
);
});
it("falls back to metadata.filename when document.title is missing", () => {
const out = renderCitationCardHtml({
evidenceItem: makeEv(),
document: {
id: DOC_ID,
mediaType: "application/pdf",
createdAt: "2026-05-25T00:00:00.000Z",
updatedAt: "2026-05-25T00:00:00.000Z",
metadata: { filename: "order-2024.pdf" },
},
annotation: makeAnn(),
});
expect(out).toContain(
'<cite class="citation-card__source">order-2024.pdf</cite>',
);
});
it("drops the · separator and link when openContextUrlOverride is empty", () => {
const out = renderCitationCardHtml({
evidenceItem: makeEv(),
document: makeDoc(),
annotation: makeAnn(),
openContextUrlOverride: "",
});
expect(out).toMatchInlineSnapshot(`
"<aside class="citation-card">
<blockquote class="citation-card__quote">Die Frist endet am 31. März 2024.</blockquote>
<p class="citation-card__attribution"><cite class="citation-card__source">Order from 14 Mar 2024</cite></p>
</aside>
"
`);
});
it("does not emit inline style attributes (host page controls CSS)", () => {
const out = renderCitationCardHtml({
evidenceItem: makeEv({ commentary: "Some commentary." }),
document: makeDoc(),
annotation: makeAnn(),
});
expect(out).not.toMatch(/style=/);
});
});