import { describe, expect, it } from "vitest"; import { zipSync } from "fflate"; import { defaultExportConfig } from "@md-to-pdf/core"; import { inspectDocxAcceptance } from "../src/index.js"; const encoder = new TextEncoder(); const word = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; const relationships = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"; const packageRelationships = "http://schemas.openxmlformats.org/package/2006/relationships"; const math = "http://schemas.openxmlformats.org/officeDocument/2006/math"; const drawing = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"; const png = Uint8Array.of( 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0 ); function xml(value: string) { return encoder.encode(value); } function createAcceptanceDocx( overrides: { altChunk?: boolean; duplicateText?: boolean; internalMarker?: boolean; invalidPropertyOrder?: boolean; coverPageNumber?: boolean; bodyTitlePage?: boolean; footerAlignment?: "left" | "center" | "right"; evenAndOddHeaders?: boolean; logicalOuterFooter?: boolean; nativeOuterFooter?: boolean; } = {} ) { return zipSync({ "[Content_Types].xml": xml( `${overrides.nativeOuterFooter ? '' : ""}` ), "_rels/.rels": xml( `` ), "word/document.xml": xml( `可编辑正文x${overrides.invalidPropertyOrder ? '' : ""}${overrides.duplicateText ? "可编辑正文" : ""}${overrides.internalMarker ? "MD_TO_PDF_INTERNAL" : ""}${overrides.coverPageNumber ? '' : ""}原生表格${overrides.altChunk ? '' : ""}${overrides.nativeOuterFooter ? '' : ""}${overrides.bodyTitlePage ? "" : ""}` ), "word/styles.xml": xml( `${["Normal", "Heading1", "SourceCode", "Table", "Caption"].map((id) => ``).join("")}` ), "word/settings.xml": xml( `${overrides.evenAndOddHeaders ? "" : ""}` ), "word/fontTable.xml": xml(``), "word/numbering.xml": xml(``), "word/theme/theme1.xml": xml( '' ), "word/footer1.xml": xml( overrides.logicalOuterFooter ? ` IF MOD( PAGE PAGE SECTIONPAGES ` : ` PAGE \\* MERGEFORMAT SECTIONPAGES \\* MERGEFORMAT ` ), ...(overrides.nativeOuterFooter ? { "word/footer2.xml": xml( ` PAGE \\* MERGEFORMAT SECTIONPAGES \\* MERGEFORMAT ` ) } : {}), "word/footnotes.xml": xml( `脚注` ), "word/media/image1.png": png, "word/_rels/document.xml.rels": xml( `${overrides.nativeOuterFooter ? '' : ""}${overrides.altChunk ? '' : ""}` ), ...(overrides.altChunk ? { "word/chunk.html": encoder.encode("

整体 HTML

") } : {}) }); } const expectation = { id: "unit-fixture", page: { widthTwips: 11906, heightTwips: 16838, orientation: "portrait" as const, marginsTwips: { top: 907, right: 907, bottom: 907, left: 907 } }, requiredText: ["可编辑正文", "原生表格"], requiredFootnoteText: ["脚注"], minimumParagraphs: 3, minimumTextRuns: 2, minimumTables: 1, minimumNumberedParagraphs: 1, minimumHyperlinks: 1, minimumFootnoteReferences: 1, minimumMathObjects: 1, minimumPngImages: 1, requiredImageAltText: ["普通图片"], requiredStyleIds: ["Normal", "Heading1", "SourceCode", "Table"], requiredParagraphStyleIds: ["Heading1"], requirePageField: true, minimumSections: 2, requireFirstSectionWithoutHeaderFooter: true, finalPageNumberStart: 1, requireSectionPagesField: true, minimumFullWidthTables: 1, maximumTextOccurrences: { 可编辑正文: 1 }, forbidInternalMarkers: true, pageDecorations: { exportConfig: { header: defaultExportConfig.header, footer: defaultExportConfig.footer }, semanticDocument: { schemaVersion: 1 as const, titlePolicy: { metadataTitle: "suppress" as const, firstBodyHeading: "keep" as const }, regions: [ { kind: "cover" as const, nodes: [ { kind: "text" as const, role: "project-report-title" as const, text: "封面" } ], section: { headerFooter: "none" as const, pageNumber: "hidden" as const, breakAfter: "next-page" as const, followingPageNumberStart: 1 } } ] } } }; describe("DOCX 自动验收器", () => { it("验证纸张、可编辑结构、关系、PNG 和字段", () => { const report = inspectDocxAcceptance( createAcceptanceDocx(), expectation ); expect(report.page.orientation).toBe("portrait"); expect(report.structure).toMatchObject({ tableCount: 1, numberedParagraphCount: 1, hyperlinkCount: 1, footnoteReferenceCount: 1, mathObjectCount: 1, pngImageCount: 1, pageFieldCount: 1, sectionPageFieldCount: 1, sectionCount: 2, fullWidthTableCount: 1, internalMarkerCount: 0, altChunkCount: 0 }); expect(report.checks).toEqual( expect.objectContaining({ editableStructure: true, pngMedia: true, noAltChunk: true }) ); expect(report.pageDecorations).toEqual({ hasCover: true, usesDifferentFirstPage: false, usesEvenAndOddPages: false, bodyPageNumberStart: 1, headerReferenceTypes: [], footerReferenceTypes: ["default"], totalPageField: "SECTIONPAGES" }); }); it("拒绝通过 altChunk 嵌入的正文 HTML", () => { expect(() => inspectDocxAcceptance( createAcceptanceDocx({ altChunk: true }), expectation ) ).toThrow("altChunk"); }); it("拒绝重复文本和未清理的内部标记", () => { expect(() => inspectDocxAcceptance( createAcceptanceDocx({ duplicateText: true }), expectation ) ).toThrow("最多允许 1 次"); expect(() => inspectDocxAcceptance( createAcceptanceDocx({ internalMarker: true }), expectation ) ).toThrow("internal-markers"); }); it("拒绝 WordprocessingML 属性子节点乱序", () => { expect(() => inspectDocxAcceptance( createAcceptanceDocx({ invalidPropertyOrder: true }), expectation ) ).toThrow("w:pPr 子节点顺序无效"); }); it("拒绝封面节残留页码和正文节错误首页设置", () => { expect(() => inspectDocxAcceptance( createAcceptanceDocx({ coverPageNumber: true }), expectation ) ).toThrow("封面节不得包含 w:pgNumType"); expect(() => inspectDocxAcceptance( createAcceptanceDocx({ bodyTitlePage: true }), expectation ) ).toThrow("body-title-page"); }); it("拒绝页码位置和奇偶页设置偏离导出配置", () => { expect(() => inspectDocxAcceptance( createAcceptanceDocx({ footerAlignment: "right" }), expectation ) ).toThrow("footer-default-alignment"); expect(() => inspectDocxAcceptance( createAcceptanceDocx({ evenAndOddHeaders: true }), expectation ) ).toThrow("even-and-odd-setting"); }); it("封面重启页码时要求原生奇偶页脚表达外侧页码", () => { const outerExpectation = { ...expectation, pageDecorations: { ...expectation.pageDecorations, exportConfig: { ...expectation.pageDecorations.exportConfig, footer: { ...expectation.pageDecorations.exportConfig.footer, alignment: "outer" as const } } } }; const report = inspectDocxAcceptance( createAcceptanceDocx({ evenAndOddHeaders: true, nativeOuterFooter: true, footerAlignment: "left" }), outerExpectation ); expect(report.pageDecorations).toMatchObject({ hasCover: true, usesEvenAndOddPages: true, footerReferenceTypes: ["default", "even"] }); expect(() => inspectDocxAcceptance(createAcceptanceDocx(), outerExpectation) ).toThrow("body-page-number-start"); }); });