Files
workspace/code/one-designer/packages/report-renderer/tests/renderer.test.ts
T
2026-09-20 17:34:40 +08:00

221 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Renderer 一致性测试(对应设计文档 §15 的
* "HTML、SVG、PDF Renderer 的公共布局结果一致性测试")。
*
* 核心断言:同一份 LayoutDocument 经 HTML / SVG 渲染后,
* 分页结构一致(页数相同、节点都能找到)。
*/
import { describe, expect, it } from "vitest"
import { createDocument, createTableElement, type ReportDocument } from "@report/core"
import { createEstimateMeasurer, createPassthroughFontResolver } from "@report/layout"
import { layoutDocument } from "@report/layout"
import { renderHtml, renderSvg, createPdfRenderer } from "../src/index.js"
import type { RenderContext } from "@report/layout"
const measurer = createEstimateMeasurer()
const fontResolver = createPassthroughFontResolver()
async function layout(doc: ReportDocument, context: RenderContext) {
return layoutDocument(doc, context, { textMeasurer: measurer, fontResolver })
}
/** 构造一份含页眉、文本、明细表、页脚的报表文档。 */
function sampleDoc(): ReportDocument {
const doc = createDocument({ name: "渲染测试" })
doc.datasets = { sales: { id: "sales" } }
const header = doc.sections.find((s) => s.kind === "page-header")!
header.children.push({
id: "h1",
type: "text",
frame: { x: 0, y: 0, width: 100, height: 8 },
props: { text: "销售单" },
})
const detail = doc.sections.find((s) => s.kind === "detail")!
detail.children.push(
createTableElement("tbl1", { x: 0, y: 0, width: 180, height: 20 }, "sales", [
{ id: "c1", width: 90, header: "商品", binding: { field: "name" } },
{ id: "c2", width: 90, header: "金额", binding: { field: "amount" }, align: "right" },
]),
)
const footer = doc.sections.find((s) => s.kind === "page-footer")!
footer.children.push({
id: "f1",
type: "text",
frame: { x: 0, y: 0, width: 100, height: 6 },
binding: { expression: "'第 ' + page.pageNumber + ' 页'" },
})
return doc
}
const ROWS = [
{ name: "苹果", amount: 100 },
{ name: "香蕉", amount: 200 },
{ name: "橙子", amount: 300 },
]
describe("HTML / SVG 分页一致性", () => {
it("同一文档渲染为 HTML 与 SVG 时页数一致", async () => {
const doc = sampleDoc()
const result = await layout(doc, { data: { sales: ROWS }, parameters: {} })
const html = renderHtml(result)
const svg = renderSvg(result)
expect(html.target).toBe("html")
expect(svg.target).toBe("svg")
expect(html.pageCount).toBe(result.pageCount)
expect(svg.pageCount).toBe(result.pageCount)
expect(html.pageCount).toBe(svg.pageCount)
})
it("HTML 输出包含与页数一致的 .rd-page 容器", async () => {
const doc = sampleDoc()
const result = await layout(doc, { data: { sales: ROWS }, parameters: {} })
const html = renderHtml(result)
const pageCount = (html.content.match(/class="rd-page"/g) ?? []).length
expect(pageCount).toBe(result.pageCount)
})
it("SVG 输出包含与页数一致的 <svg> 元素", async () => {
const doc = sampleDoc()
const result = await layout(doc, { data: { sales: ROWS }, parameters: {} })
const svg = renderSvg(result)
const svgCount = (svg.content.match(/<svg /g) ?? []).length
expect(svgCount).toBe(result.pageCount)
})
it("文本节点在 HTML 与 SVG 中都被渲染", async () => {
const doc = sampleDoc()
const result = await layout(doc, { data: { sales: ROWS }, parameters: {} })
const html = renderHtml(result)
const svg = renderSvg(result)
// 页眉文本 "销售单" 应出现在两种输出中
expect(html.content).toContain("销售单")
expect(svg.content).toContain("销售单")
})
it("明细表数据在 HTML 与 SVG 中都被渲染", async () => {
const doc = sampleDoc()
const result = await layout(doc, { data: { sales: ROWS }, parameters: {} })
const html = renderHtml(result)
const svg = renderSvg(result)
expect(html.content).toContain("苹果")
expect(html.content).toContain("橙子")
expect(svg.content).toContain("苹果")
expect(svg.content).toContain("橙子")
})
it("页脚页码占位符在渲染前已被替换", async () => {
const doc = sampleDoc()
const result = await layout(doc, { data: { sales: ROWS }, parameters: {} })
const html = renderHtml(result)
// 页脚应有 "第 1 页" 而非原始占位符
expect(html.content).toContain("第 1 页")
expect(html.content).not.toContain("{pageNumber}")
})
})
describe("多页渲染", () => {
it("多页文档在 HTML 与 SVG 中页数一致", async () => {
const doc = sampleDoc()
const manyRows = Array.from({ length: 200 }, (_, i) => ({
name: `商品${i}`,
amount: i,
}))
const result = await layout(doc, { data: { sales: manyRows }, parameters: {} })
expect(result.pageCount).toBeGreaterThan(1)
const html = renderHtml(result)
const svg = renderSvg(result)
const htmlPages = (html.content.match(/class="rd-page"/g) ?? []).length
const svgPages = (svg.content.match(/<svg /g) ?? []).length
expect(htmlPages).toBe(result.pageCount)
expect(svgPages).toBe(result.pageCount)
})
})
describe("SVG 输出结构", () => {
it("SVG 使用 mm 标注宽度和 viewBox", async () => {
const doc = sampleDoc()
const result = await layout(doc, { data: { sales: ROWS }, parameters: {} })
const svg = renderSvg(result)
const page = result.pages[0]!
// width 与 height 用 mm,viewBox 用无单位数值
expect(svg.content).toContain(`width="${page.width}mm"`)
expect(svg.content).toContain(`viewBox="0 0 ${page.width} ${page.height}"`)
})
it("矩形节点渲染为 <rect>", async () => {
const doc = createDocument({ name: "矩形测试" })
const detail = doc.sections.find((s) => s.kind === "detail")!
detail.children.push({
id: "r1",
type: "rect",
frame: { x: 5, y: 5, width: 50, height: 20 },
})
const result = await layout(doc, { data: {}, parameters: {} })
const svg = renderSvg(result)
expect(svg.content).toContain("<rect")
expect(svg.content).toContain('id="r1"')
})
})
describe("PDF 后端契约", () => {
it("未配置后端时返回诊断而非抛异常", () => {
const result = createPdfRenderer().render({
pages: [],
diagnostics: [],
pageCount: 0,
})
expect(result.target).toBe("pdf")
expect(result.bytes).toBeInstanceOf(Uint8Array)
expect(result.bytes.length).toBe(0)
expect(result.diagnostics.some((d) => d.code === "render.pdf-backend-missing")).toBe(true)
})
it("后端抛错时返回错误诊断", () => {
const backend = {
name: "mock",
render: () => {
throw new Error("boom")
},
}
const result = createPdfRenderer(backend).render({
pages: [],
diagnostics: [],
pageCount: 0,
})
expect(result.diagnostics.some((d) => d.code === "render.pdf-backend-error")).toBe(true)
})
it("后端正常时返回字节", () => {
const backend = {
name: "mock",
render: () => new Uint8Array([1, 2, 3]),
}
const result = createPdfRenderer(backend).render({
pages: [],
diagnostics: [],
pageCount: 0,
})
expect(result.bytes.length).toBe(3)
expect(result.diagnostics).toHaveLength(0)
})
})