Files
eden/apps/api/test/editor-content-renderer.spec.ts
Matheus (Handix) 3785b32c5f feat(documents): suporte a linha condicional (conditional) + catálogo completo
Necessário pra importar os 7 templates reais do legado (fornecidos pelo
usuário via export do OrçaFácil): o Termo de Contratação STFC usa o nó
`conditional` (envelope em volta de uma tableRow, franquia de minutos/0800)
— sem suporte, tanto o backend quanto o editor Tiptap rejeitavam/renderizavam
em branco esse documento específico, mesmo bug já documentado no export do
legado. A condição em si nunca é avaliada nesta leva (fora de escopo) — a
linha sempre passa adiante, comportamento explícito e documentado.

Também adiciona ao variable-catalog.ts as ~25 variáveis que faltavam
(tarifação STFC por banda, 0800, portabilidade, prazo de instalação),
descobertas comparando contra as chaves reais usadas nos 7 templates
importados.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 20:26:18 -03:00

222 lines
7.2 KiB
TypeScript

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(
'<p style="text-align:center"><strong>Olá </strong><s><u><em>mundo</em></u></s><span style="font-size:18px"> grande</span></p>',
);
});
it("renderiza heading, listas e parágrafo vazio como &nbsp;", () => {
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("<h2>Título</h2>");
expect(html).toContain("<p>&nbsp;</p>");
expect(html).toContain("<ul><li><p>item 1</p></li></ul>");
expect(html).toContain("<ol><li><p>item A</p></li></ol>");
});
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('<img src="data:image/png;base64,abc" alt="logo" />');
expect(html).toContain('style="break-after:page;page-break-after:always;"');
});
it("gera <thead> 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(/<thead>.*<th colspan="2">/s);
expect(html).toMatch(/<tbody>.*<td>/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("<p>ACME LTDA</p>");
expect(resolvedHtml).not.toContain("merge-field-chip");
});
it("conditional sempre renderiza sua linha (avaliação real de condição é trabalho futuro)", () => {
const doc = {
type: "doc",
content: [
{
type: "table",
content: [
{
type: "tableRow",
content: [{ type: "tableHeader", content: [{ type: "paragraph", content: [{ type: "text", text: "Coluna" }] }] }],
},
{
type: "conditional",
attrs: { field: "oferta.franquia_minutos", operator: "exists" },
content: [
{
type: "tableRow",
content: [
{ type: "tableCell", content: [{ type: "paragraph", content: [{ type: "text", text: "linha condicional" }] }] },
],
},
],
},
],
},
],
};
const html = renderToHtml(doc);
expect(html).toMatch(/<thead>.*<th><p>Coluna<\/p><\/th>.*<\/thead>/s);
expect(html).toContain("<tr><td><p>linha condicional</p></td></tr>");
expect(html).not.toContain("conditional");
});
it("collectMergeFieldKeys enxerga chaves dentro de um conditional", () => {
const doc = {
type: "doc",
content: [
{
type: "conditional",
attrs: { field: "x" },
content: [
{ type: "tableRow", content: [{ type: "tableCell", content: [{ type: "mergeField", attrs: { key: "a.b" } }] }] },
],
},
],
};
expect(collectMergeFieldKeys(doc)).toEqual(["a.b"]);
});
it("escapa HTML de texto e de valores resolvidos (proteção contra XSS)", () => {
const doc = {
type: "doc",
content: [
{ type: "paragraph", content: [{ type: "text", text: "<script>alert(1)</script>" }] },
{ type: "paragraph", content: [{ type: "mergeField", attrs: { key: "x" } }] },
],
};
const html = renderToHtml(doc, () => "<img onerror=alert(1)>");
expect(html).not.toContain("<script>");
expect(html).toContain("&lt;script&gt;");
expect(html).not.toContain("<img onerror");
expect(html).toContain("&lt;img onerror=alert(1)&gt;");
});
it("collectMergeFieldKeys varre a árvore inteira e dedupe chaves repetidas", () => {
const doc = {
type: "doc",
content: [
{
type: "paragraph",
content: [
{ type: "mergeField", attrs: { key: "cliente.razao_social" } },
{ type: "mergeField", attrs: { key: "cliente.cnpj" } },
],
},
{
type: "table",
content: [
{
type: "tableRow",
content: [
{
type: "tableCell",
content: [{ type: "paragraph", content: [{ type: "mergeField", attrs: { key: "cliente.razao_social" } }] }],
},
],
},
],
},
],
};
expect(collectMergeFieldKeys(doc)).toEqual(["cliente.razao_social", "cliente.cnpj"]);
});
it("devolve string vazia para conteúdo que não é um doc Tiptap válido", () => {
expect(renderToHtml(null)).toBe("");
expect(renderToHtml({ type: "doc" })).toBe("");
expect(collectMergeFieldKeys(undefined)).toEqual([]);
});
});