import { describe, expect, it } from "vitest";
import { collectMergeFieldKeys, renderToHtml } from "../src/documents/editor-content-renderer";
/**
* Unidade pura, sem app/DB — cobre a porta do serializador Tiptap→HTML do
* legado (renderEditorContent.js). documents-pdf.spec.ts cobre o pipeline
* completo (geração real via HTTP); aqui é só a árvore de nós, isolada.
*/
describe("editor-content-renderer", () => {
it("renderiza parágrafo com alinhamento e marks (bold/italic/underline/strike/fontSize)", () => {
const doc = {
type: "doc",
content: [
{
type: "paragraph",
attrs: { textAlign: "center" },
content: [
{ type: "text", text: "Olá ", marks: [{ type: "bold" }] },
{ type: "text", text: "mundo", marks: [{ type: "italic" }, { type: "underline" }, { type: "strike" }] },
{ type: "text", text: " grande", marks: [{ type: "textStyle", attrs: { fontSize: "18px" } }] },
],
},
],
};
const html = renderToHtml(doc);
expect(html).toBe(
'
Olá mundo grande
',
);
});
it("renderiza heading, listas e parágrafo vazio como ", () => {
const doc = {
type: "doc",
content: [
{ type: "heading", attrs: { level: 2 }, content: [{ type: "text", text: "Título" }] },
{ type: "paragraph", content: [] },
{
type: "bulletList",
content: [
{ type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: "item 1" }] }] },
],
},
{
type: "orderedList",
content: [
{ type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: "item A" }] }] },
],
},
],
};
const html = renderToHtml(doc);
expect(html).toContain("Título
");
expect(html).toContain("
");
expect(html).toContain("");
expect(html).toContain("item A
");
});
it("renderiza imagem e quebra de página", () => {
const doc = {
type: "doc",
content: [
{ type: "image", attrs: { src: "data:image/png;base64,abc", alt: "logo" } },
{ type: "pageBreak" },
],
};
const html = renderToHtml(doc);
expect(html).toContain('
');
expect(html).toContain('style="break-after:page;page-break-after:always;"');
});
it("gera real só quando há tableHeader, mesmo no meio das linhas", () => {
const doc = {
type: "doc",
content: [
{
type: "table",
content: [
{
type: "tableRow",
content: [{ type: "tableHeader", attrs: { colspan: 2 }, content: [{ type: "paragraph", content: [] }] }],
},
{
type: "tableRow",
content: [{ type: "tableCell", content: [{ type: "paragraph", content: [{ type: "text", text: "x" }] }] }],
},
],
},
],
};
const html = renderToHtml(doc);
expect(html).toMatch(/.*/s);
expect(html).toMatch(/ | .*/s);
});
it("mergeField vira chip visual sem resolveMergeField, e valor resolvido com resolveMergeField", () => {
const doc = {
type: "doc",
content: [
{
type: "paragraph",
content: [{ type: "mergeField", attrs: { key: "cliente.razao_social", label: "Razão Social" } }],
},
],
};
const chipHtml = renderToHtml(doc);
expect(chipHtml).toContain('data-merge-field="cliente.razao_social"');
expect(chipHtml).toContain("Razão Social");
const resolvedHtml = renderToHtml(doc, (key) => (key === "cliente.razao_social" ? "ACME LTDA" : ""));
expect(resolvedHtml).toBe(" ACME LTDA ");
expect(resolvedHtml).not.toContain("merge-field-chip");
});
it("escapa HTML de texto e de valores resolvidos (proteção contra XSS)", () => {
const doc = {
type: "doc",
content: [
{ type: "paragraph", content: [{ type: "text", text: "" }] },
{ type: "paragraph", content: [{ type: "mergeField", attrs: { key: "x" } }] },
],
};
const html = renderToHtml(doc, () => " ");
expect(html).not.toContain(" |