generated from coulomb/repo-seed
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.
173 lines
5.8 KiB
TypeScript
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&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&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\"><<value>> & 'inner'</blockquote>",
|
|
);
|
|
// Source label escaping
|
|
expect(out).toContain(
|
|
"<cite class=\"citation-card__source\">Order "draft" & more</cite>",
|
|
);
|
|
// Commentary escaping — and especially: no raw <script>
|
|
expect(out).not.toContain("<script>");
|
|
expect(out).toContain(
|
|
"<div class=\"citation-card__commentary\">Notes: <script>alert("xss")</script> & 'untrusted'</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=/);
|
|
});
|
|
});
|