// @vitest-environment happy-dom import { beforeEach, describe, expect, it, vi } from "vitest"; import { collectDocxDocumentLayoutPlan, collectDocxInlineCodeLayouts, collectDocxListItemLayouts, collectDocxTextBlockLayouts, collectDocxMediaCaptureTargets, removeInheritedPagedSplitJustification, renderDocxMediaCapturePlan } from "../src/index.js"; describe("DOCX 媒体捕获计划", () => { beforeEach(() => { document.body.innerHTML = ""; window.scrollTo(0, 0); }); it("连续布局使用原始打印媒体主题 CSS", async () => { const renderContinuous = vi.fn(async () => ({ echartsErrors: [], mermaidErrors: [] })); const root = document.createElement("main"); root.innerHTML = '
'; await renderDocxMediaCapturePlan( { renderContinuous } as never, root, { articleHtml: '
', fileName: "打印媒体.md", metadata: { title: "", author: "", subject: "", keywords: [], language: "zh-CN" }, semanticDocument: { schemaVersion: 1, titlePolicy: { metadataTitle: "suppress", firstBodyHeading: "keep" }, regions: [] }, features: [], themeCss: "@media print { html { font-size: 13px; } }", exportConfig: {} as never }, { contentWidthPx: 640, contentHeightPx: 900 } ); expect(renderContinuous).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ themeMedia: "print" }) ); }); it("只移除会把 Paged.js 末行两端对齐继承给子块的容器标记", () => { document.body.innerHTML = `

标题

跨页正文

`; expect( removeInheritedPagedSplitJustification( document.querySelector("#pages")! ) ).toBe(1); expect( document.querySelector("#write")?.hasAttribute( "data-align-last-split-element" ) ).toBe(false); expect( document.querySelector("p")?.getAttribute( "data-align-last-split-element" ) ).toBe("justify"); }); it("按文档顺序标记图片、Mermaid 和 ECharts", () => { document.body.innerHTML = `
架构截图
系统架构
收入趋势
`; const media = Array.from( document.querySelectorAll( "img, svg" ) ); const article = document.querySelector("#write")!; article.getBoundingClientRect = () => ({ x: 0, y: 0, left: 0, top: 0, right: 700, bottom: 900, width: 700, height: 900, toJSON: () => ({}) }) as DOMRect; const horizontalPositions = [0, 200, 400]; media.forEach((element, index) => { const left = horizontalPositions[index]!; element.getBoundingClientRect = () => ({ x: left, y: 20 + index * 100, left, top: 20 + index * 100, right: left + 300, bottom: 170 + index * 100, width: 300, height: 150, toJSON: () => ({}) }) as DOMRect; }); const targets = collectDocxMediaCaptureTargets(document, { contentWidthPx: 700, contentHeightPx: 900 }); expect( targets.map( ({ id, kind, kindOrdinal, altText, caption, alignment }) => ({ id, kind, kindOrdinal, altText, caption, alignment }) ) ).toEqual([ { id: "docx-media-1", kind: "image", kindOrdinal: 1, altText: "架构截图", caption: "系统架构", alignment: "left" }, { id: "docx-media-2", kind: "mermaid", kindOrdinal: 1, altText: "处理流程", caption: undefined, alignment: "center" }, { id: "docx-media-3", kind: "echarts", kindOrdinal: 1, altText: "年度收入", caption: "收入趋势", alignment: "right" } ]); expect( media.map((element) => element.dataset.docxMediaId) ).toEqual(["docx-media-1", "docx-media-2", "docx-media-3"]); expect(targets[0]?.rasterScale).toBe(3.125); }); it("限制超大媒体的显示尺寸和 PNG 像素规模", () => { document.body.innerHTML = `
`; const image = document.querySelector("img")!; const article = document.querySelector("#write")!; article.getBoundingClientRect = () => ({ x: 0, y: 0, left: 0, top: 0, right: 800, bottom: 900, width: 800, height: 900, toJSON: () => ({}) }) as DOMRect; image.getBoundingClientRect = () => ({ x: 0, y: 0, left: 0, top: 0, right: image.style.width ? 800 : 4000, bottom: image.style.width ? 600 : 3000, width: image.style.width ? 800 : 4000, height: image.style.width ? 600 : 3000, toJSON: () => ({}) }) as DOMRect; const [target] = collectDocxMediaCaptureTargets(document, { contentWidthPx: 800, contentHeightPx: 900 }); expect(target?.displayWidthPx).toBeLessThanOrEqual(800); expect(target?.displayHeightPx).toBeLessThanOrEqual(900); expect(target?.alignment).toBe("center"); expect( (target?.captureWidthPx ?? 0) * (target?.rasterScale ?? 0) ).toBeLessThanOrEqual(4096); expect(target?.altText).toBe("图片 1"); }); it("从真实 DOM 几何采集主题无关的表格列宽比例", () => { document.body.innerHTML = `
短列较长内容列
12
`; const article = document.querySelector("#write")!; const table = document.querySelector("table")!; const cells = Array.from(table.querySelectorAll("td")); article.getBoundingClientRect = () => ({ left: 100, right: 900, top: 0, bottom: 900, width: 800, height: 900 }) as DOMRect; table.getBoundingClientRect = () => ({ left: 140, right: 860, top: 20, bottom: 220, width: 720, height: 200 }) as DOMRect; cells.forEach((cell, index) => { const firstColumn = index % 2 === 0; cell.getBoundingClientRect = () => ({ left: firstColumn ? 140 : 356, right: firstColumn ? 356 : 860, top: index < 2 ? 20 : 120, bottom: index < 2 ? 120 : 220, width: firstColumn ? 216 : 504, height: 100 }) as DOMRect; }); expect( collectDocxDocumentLayoutPlan(document, { contentWidthPx: 800, contentHeightPx: 900 }) ).toEqual({ inlineCodes: [], listItems: [], textBlocks: [], tables: [ { ordinal: 1, widthPercent: 90, leftOffsetPercent: 5, columnWidthPercents: [30, 70], rows: [ { cells: [ { columnSpan: 1, backgroundColor: "#ffffff", color: "#000000", bold: false, italic: false, alignment: "left" }, { columnSpan: 1, backgroundColor: "#ffffff", color: "#000000", bold: false, italic: false, alignment: "left" } ] }, { cells: [ { columnSpan: 1, backgroundColor: "#ffffff", color: "#000000", bold: false, italic: false, alignment: "left" }, { columnSpan: 1, backgroundColor: "#ffffff", color: "#000000", bold: false, italic: false, alignment: "left" } ] } ] } ] }); }); it("按真实上下文采集行内代码字号、颜色和盒模型", () => { document.body.innerHTML = `

inline

block
`; expect( collectDocxInlineCodeLayouts( document.querySelector("#write")! ) ).toEqual([ { ordinal: 1, text: "inline", fontSizePt: 12, letterSpacingPt: 0.75, color: "#112233", backgroundColor: "#f5f5f5", paddingPt: { top: 1.5, right: 3, bottom: 1.5, left: 3 }, borderPt: { top: 0, right: 0, bottom: 0, left: 0 } } ]); }); it("按列表项实际文字起点采集嵌套缩进且排除子列表文字", () => { document.body.innerHTML = `
  • 一级 代码
    • 二级项目
`; const article = document.querySelector("#write")!; article.getBoundingClientRect = () => ({ left: 100, right: 900, top: 0, bottom: 900, width: 800, height: 900 }) as DOMRect; const rangePrototype = Object.getPrototypeOf(document.createRange()) as { getBoundingClientRect?: () => DOMRect; }; const original = rangePrototype.getBoundingClientRect; rangePrototype.getBoundingClientRect = function (this: Range) { const left = this.startContainer.textContent === "二级项目" ? 340 : 220; return { left, right: left + 10, top: 20, bottom: 32, width: 10, height: 12 } as DOMRect; }; try { expect(collectDocxListItemLayouts(article)).toEqual([ { ordinal: 1, text: "一级 代码", depth: 0, textStartPt: 90 }, { ordinal: 2, text: "二级项目", depth: 1, textStartPt: 180 } ]); } finally { rangePrototype.getBoundingClientRect = original; } }); it("采集正文文本块在 Chromium 中的实际行断点", () => { document.body.innerHTML = `

独立封面

甲乙丙丁

`; const rangePrototype = Object.getPrototypeOf(document.createRange()) as { getBoundingClientRect?: () => DOMRect; }; const original = rangePrototype.getBoundingClientRect; rangePrototype.getBoundingClientRect = function (this: Range) { const top = this.startOffset < 2 ? 100 : 120; return { x: this.startOffset * 10, y: top, left: this.startOffset * 10, right: this.startOffset * 10 + 10, top, bottom: top + 12, width: 10, height: 12, toJSON: () => ({}) } as DOMRect; }; try { expect( collectDocxTextBlockLayouts( document.querySelector("#write")! ) ).toEqual([ { ordinal: 1, text: "甲乙丙丁", letterSpacingPt: 0, alignment: "left", linePitchPt: 15, lineBreakOffsets: [2] } ]); } finally { rangePrototype.getBoundingClientRect = original; } }); });