feat: 完成 DOCX R4 元素级视觉门禁
建立 Chromium、Word 与 WPS 的可编辑内容、段落几何、页眉页脚、封面、媒体和逐页视觉比较链路。 支持封面独立分节、正文页码重启、主题页边距与横向纸张,并将自然分页差异降为诊断项。 修正代码块内边距和折叠表格单元格边框映射,14 套主题生产矩阵与六组布局视觉矩阵通过。
This commit is contained in:
@@ -0,0 +1,629 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { DOMParser } from "@xmldom/xmldom";
|
||||
import { unzipSync } from "fflate";
|
||||
|
||||
import {
|
||||
createPdfDocumentSnapshot,
|
||||
createPdfVisualDiffReport,
|
||||
createWordPdfAdapter,
|
||||
createWpsPdfAdapter,
|
||||
renderPdfVisualDiffHtml,
|
||||
serializePdfVisualDiffJson
|
||||
} from "../packages/document-visual-diff/dist/index.js";
|
||||
import {
|
||||
formatPageNumber,
|
||||
resolvePageNumberAlignment
|
||||
} from "../packages/preview-engine/dist/index.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const WORD_NAMESPACE =
|
||||
"http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||
const MATH_NAMESPACE =
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/math";
|
||||
const decoder = new TextDecoder();
|
||||
const repositoryDirectory = path.resolve(import.meta.dirname, "..");
|
||||
const outputDirectory = path.join(
|
||||
repositoryDirectory,
|
||||
"output",
|
||||
"docx-layout-visual-matrix"
|
||||
);
|
||||
const matrixScript = path.join(
|
||||
repositoryDirectory,
|
||||
"apps",
|
||||
"server",
|
||||
"scripts",
|
||||
"verify-docx-r4-matrix.mjs"
|
||||
);
|
||||
const pageSizes = {
|
||||
A4: { portrait: [595.276, 841.89], landscape: [841.89, 595.276] },
|
||||
Letter: { portrait: [612, 792], landscape: [792, 612] }
|
||||
};
|
||||
const strictIssueCodes = new Set([
|
||||
"PAGE_SIZE_MISMATCH",
|
||||
"PAGE_ORIENTATION_MISMATCH",
|
||||
"CONTENT_MISMATCH",
|
||||
"PAGE_NUMBER_VISIBILITY_MISMATCH",
|
||||
"PAGE_NUMBER_TEXT_MISMATCH",
|
||||
"PAGE_NUMBER_ALIGNMENT_MISMATCH",
|
||||
"PAGE_NUMBER_VERTICAL_POSITION_MISMATCH",
|
||||
"HEADER_VISIBILITY_MISMATCH",
|
||||
"HEADER_TEXT_MISSING",
|
||||
"HEADER_VERTICAL_POSITION_MISMATCH",
|
||||
"EDITABLE_PARAGRAPH_MAPPING_MISMATCH",
|
||||
"TEXT_BLOCK_LINE_COUNT_MISMATCH",
|
||||
"PARAGRAPH_LINE_BREAK_MISMATCH",
|
||||
"PARAGRAPH_LINE_GEOMETRY_MISMATCH",
|
||||
"COVER_LAYOUT_MISMATCH"
|
||||
]);
|
||||
|
||||
const cases = [
|
||||
{
|
||||
id: "official-theme-a4-portrait",
|
||||
themeId: "gov-red-standard",
|
||||
coverPageCount: 0,
|
||||
paper: { format: "A4", orientation: "portrait" },
|
||||
config: {}
|
||||
},
|
||||
{
|
||||
id: "official-asymmetric-a4-portrait",
|
||||
themeId: "gov-red-standard",
|
||||
coverPageCount: 0,
|
||||
paper: { format: "A4", orientation: "portrait" },
|
||||
config: {
|
||||
paper: {
|
||||
format: "A4",
|
||||
orientation: "portrait",
|
||||
marginMode: "custom",
|
||||
margins: {
|
||||
top: "18mm",
|
||||
right: "14mm",
|
||||
bottom: "30mm",
|
||||
left: "32mm"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "project-cover-custom-a4-portrait",
|
||||
themeId: "formal-feasibility",
|
||||
coverPageCount: 1,
|
||||
paper: { format: "A4", orientation: "portrait" },
|
||||
config: {
|
||||
paper: {
|
||||
format: "A4",
|
||||
orientation: "portrait",
|
||||
marginMode: "custom",
|
||||
margins: {
|
||||
top: "20mm",
|
||||
right: "18mm",
|
||||
bottom: "25mm",
|
||||
left: "30mm"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "tender-cover-custom-letter-landscape",
|
||||
themeId: "tender-business-blue",
|
||||
coverPageCount: 1,
|
||||
paper: { format: "Letter", orientation: "landscape" },
|
||||
config: {
|
||||
paper: {
|
||||
format: "Letter",
|
||||
orientation: "landscape",
|
||||
marginMode: "custom",
|
||||
margins: {
|
||||
top: "16mm",
|
||||
right: "18mm",
|
||||
bottom: "20mm",
|
||||
left: "24mm"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "formal-custom-a4-landscape",
|
||||
themeId: "formal-regulation",
|
||||
coverPageCount: 0,
|
||||
paper: { format: "A4", orientation: "landscape" },
|
||||
config: {
|
||||
paper: {
|
||||
format: "A4",
|
||||
orientation: "landscape",
|
||||
marginMode: "custom",
|
||||
margins: {
|
||||
top: "15mm",
|
||||
right: "22mm",
|
||||
bottom: "18mm",
|
||||
left: "25mm"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "general-compact-letter-portrait",
|
||||
themeId: "typora-github",
|
||||
coverPageCount: 0,
|
||||
paper: { format: "Letter", orientation: "portrait" },
|
||||
config: {
|
||||
paper: {
|
||||
format: "Letter",
|
||||
orientation: "portrait",
|
||||
marginMode: "custom",
|
||||
margins: {
|
||||
top: "12.7mm",
|
||||
right: "12.7mm",
|
||||
bottom: "12.7mm",
|
||||
left: "12.7mm"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const selectedCaseId =
|
||||
process.env.MD_TO_PDF_LAYOUT_VISUAL_CASE?.trim() || undefined;
|
||||
const selectedCases = selectedCaseId
|
||||
? cases.filter((entry) => entry.id === selectedCaseId)
|
||||
: cases;
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function paragraphStyleId(paragraph) {
|
||||
const style = Array.from(paragraph.getElementsByTagName("*")).find(
|
||||
(node) =>
|
||||
node.localName === "pStyle" && node.namespaceURI === WORD_NAMESPACE
|
||||
);
|
||||
return style?.getAttribute("w:val") || style?.getAttribute("val") || undefined;
|
||||
}
|
||||
|
||||
function paragraphRole(styleId) {
|
||||
if (!styleId) {
|
||||
return "body";
|
||||
}
|
||||
if (/^Heading[1-6]$/u.test(styleId)) {
|
||||
return "heading";
|
||||
}
|
||||
if (
|
||||
styleId === "Title" ||
|
||||
/(?:Title|Masthead|ProjectName)$/u.test(styleId)
|
||||
) {
|
||||
return "title";
|
||||
}
|
||||
if (/Caption/u.test(styleId)) {
|
||||
return "caption";
|
||||
}
|
||||
return "body";
|
||||
}
|
||||
|
||||
function isInternalLayoutSpacerParagraph(descendants, text) {
|
||||
if (text !== ".") {
|
||||
return false;
|
||||
}
|
||||
const attributeValue = (node, name) =>
|
||||
node.getAttribute(`w:${name}`) || node.getAttribute(name) || undefined;
|
||||
return (
|
||||
descendants.some(
|
||||
(node) =>
|
||||
node.localName === "sz" &&
|
||||
node.namespaceURI === WORD_NAMESPACE &&
|
||||
attributeValue(node, "val") === "2"
|
||||
) &&
|
||||
descendants.some(
|
||||
(node) =>
|
||||
node.localName === "spacing" &&
|
||||
node.namespaceURI === WORD_NAMESPACE &&
|
||||
attributeValue(node, "lineRule") === "exact"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function extractEditableContract(docxPath) {
|
||||
const entries = unzipSync(Uint8Array.from(fs.readFileSync(docxPath)));
|
||||
const documentXml = entries["word/document.xml"];
|
||||
assert(documentXml, `DOCX 缺少 word/document.xml:${docxPath}`);
|
||||
const document = new DOMParser().parseFromString(
|
||||
decoder.decode(documentXml),
|
||||
"application/xml"
|
||||
);
|
||||
let section = "cover";
|
||||
let hasCoverSection = false;
|
||||
const paragraphs = [];
|
||||
for (const paragraph of Array.from(document.getElementsByTagName("*")).filter(
|
||||
(node) => node.localName === "p" && node.namespaceURI === WORD_NAMESPACE
|
||||
)) {
|
||||
const descendants = Array.from(paragraph.getElementsByTagName("*"));
|
||||
const hasSectionBreak = descendants.some(
|
||||
(node) =>
|
||||
node.localName === "sectPr" && node.namespaceURI === WORD_NAMESPACE
|
||||
);
|
||||
if (hasSectionBreak) {
|
||||
hasCoverSection = true;
|
||||
section = "body";
|
||||
continue;
|
||||
}
|
||||
const text = descendants.flatMap((node) =>
|
||||
node.localName === "t" &&
|
||||
(node.namespaceURI === WORD_NAMESPACE ||
|
||||
node.namespaceURI === MATH_NAMESPACE)
|
||||
? [node.textContent ?? ""]
|
||||
: []
|
||||
).join("");
|
||||
if (!text || isInternalLayoutSpacerParagraph(descendants, text)) {
|
||||
continue;
|
||||
}
|
||||
const styleId = paragraphStyleId(paragraph);
|
||||
paragraphs.push({
|
||||
index: paragraphs.length,
|
||||
text,
|
||||
...(styleId ? { styleId } : {}),
|
||||
role: paragraphRole(styleId),
|
||||
section
|
||||
});
|
||||
}
|
||||
if (!hasCoverSection) {
|
||||
for (const paragraph of paragraphs) {
|
||||
paragraph.section = "body";
|
||||
}
|
||||
}
|
||||
return {
|
||||
text: paragraphs.map((paragraph) => paragraph.text).join(""),
|
||||
paragraphs
|
||||
};
|
||||
}
|
||||
|
||||
async function generateCase(caseDefinition) {
|
||||
const caseDirectory = path.join(outputDirectory, caseDefinition.id);
|
||||
fs.mkdirSync(caseDirectory, { recursive: true });
|
||||
const { stdout } = await execFileAsync(process.execPath, [matrixScript], {
|
||||
cwd: repositoryDirectory,
|
||||
windowsHide: true,
|
||||
timeout: 360_000,
|
||||
maxBuffer: 24 * 1024 * 1024,
|
||||
env: {
|
||||
...process.env,
|
||||
MD_TO_PDF_R4_THEME_ID: caseDefinition.themeId,
|
||||
MD_TO_PDF_R4_OUTPUT_DIR: caseDirectory,
|
||||
MD_TO_PDF_R4_EXPORT_CONFIG_JSON: JSON.stringify(caseDefinition.config)
|
||||
}
|
||||
});
|
||||
const result = JSON.parse(stdout).results?.[0];
|
||||
assert(result, `视觉矩阵用例 ${caseDefinition.id} 没有生成产物`);
|
||||
return { caseDirectory, result };
|
||||
}
|
||||
|
||||
function expectedPageSize(caseDefinition) {
|
||||
const dimensions =
|
||||
pageSizes[caseDefinition.paper.format]?.[caseDefinition.paper.orientation];
|
||||
assert(dimensions, `未知纸张配置:${JSON.stringify(caseDefinition.paper)}`);
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
function pageGeometryFailures(snapshot, caseDefinition, label) {
|
||||
const [expectedWidth, expectedHeight] = expectedPageSize(caseDefinition);
|
||||
return snapshot.pages.flatMap((page) => {
|
||||
const widthDelta = Math.abs(page.widthPt - expectedWidth);
|
||||
const heightDelta = Math.abs(page.heightPt - expectedHeight);
|
||||
return widthDelta <= 0.5 && heightDelta <= 0.5
|
||||
? []
|
||||
: [
|
||||
`${label} 第 ${page.pageNumber} 页纸张尺寸为 ` +
|
||||
`${page.widthPt.toFixed(3)}×${page.heightPt.toFixed(3)} pt,` +
|
||||
`期望 ${expectedWidth.toFixed(3)}×${expectedHeight.toFixed(3)} pt`
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
function buildPageSemantics(pageCount, coverPageCount, config) {
|
||||
assert(
|
||||
pageCount > coverPageCount,
|
||||
`物理页数 ${pageCount} 必须大于封面页数 ${coverPageCount}`
|
||||
);
|
||||
const logicalPageCount = pageCount - coverPageCount;
|
||||
const headerSlots = ["left", "center", "right"].flatMap((alignment) => {
|
||||
const slot = config.header[alignment];
|
||||
return config.header.enabled && slot.enabled && slot.content
|
||||
? [{ alignment, text: slot.content }]
|
||||
: [];
|
||||
});
|
||||
return Array.from({ length: pageCount }, (_, index) => {
|
||||
const physicalPageNumber = index + 1;
|
||||
if (physicalPageNumber <= coverPageCount) {
|
||||
return {
|
||||
physicalPageNumber,
|
||||
kind: "cover",
|
||||
logicalPageCount,
|
||||
headerVisible: false,
|
||||
headerSlots,
|
||||
footerVisible: false
|
||||
};
|
||||
}
|
||||
const bodyPageIndex = index - coverPageCount;
|
||||
const isBodyFirst = bodyPageIndex === 0;
|
||||
const headerVisible =
|
||||
config.header.enabled &&
|
||||
(!isBodyFirst || config.header.showOnFirstPage);
|
||||
const footerVisible =
|
||||
config.footer.enabled &&
|
||||
(!isBodyFirst || config.footer.showOnFirstPage);
|
||||
return {
|
||||
physicalPageNumber,
|
||||
kind: isBodyFirst ? "body-first" : "body-rest",
|
||||
logicalPageNumber: config.footer.startFrom + bodyPageIndex,
|
||||
logicalPageCount,
|
||||
headerVisible,
|
||||
headerSlots,
|
||||
footerVisible,
|
||||
...(footerVisible
|
||||
? {
|
||||
footerAlignment: resolvePageNumberAlignment(
|
||||
config.footer,
|
||||
bodyPageIndex
|
||||
),
|
||||
pageNumberText: formatPageNumber(
|
||||
config.footer,
|
||||
bodyPageIndex,
|
||||
logicalPageCount
|
||||
)
|
||||
}
|
||||
: {})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function reportGateFailures(report, label) {
|
||||
const failures = report.issues
|
||||
.filter((issue) => strictIssueCodes.has(issue.code))
|
||||
.map((issue) => `${label}: ${issue.code} - ${issue.message}`);
|
||||
if (report.basic.baselineEditableCoverage !== 1) {
|
||||
failures.push(
|
||||
`${label}: Chromium 可编辑正文有序覆盖率不是 100%`
|
||||
);
|
||||
}
|
||||
if (report.basic.candidateEditableExact !== true) {
|
||||
failures.push(`${label}: Office 可编辑正文未精确匹配`);
|
||||
}
|
||||
if (report.basic.bodyFlow?.detected && !report.basic.bodyFlow.legal) {
|
||||
failures.push(`${label}: 分页差异未满足合法正文流条件`);
|
||||
}
|
||||
return failures;
|
||||
}
|
||||
|
||||
function reportSummary(report) {
|
||||
const paragraphIssues = report.basic.paragraphLayouts?.flatMap(
|
||||
(paragraph) => paragraph.issues
|
||||
) ?? [];
|
||||
return {
|
||||
status: report.status,
|
||||
issueCodes: [...new Set(report.issues.map((issue) => issue.code))],
|
||||
strictIssueCodes: [
|
||||
...new Set(
|
||||
report.issues
|
||||
.filter((issue) => strictIssueCodes.has(issue.code))
|
||||
.map((issue) => issue.code)
|
||||
)
|
||||
],
|
||||
editableExact: report.basic.candidateEditableExact,
|
||||
paragraphIssueCount: paragraphIssues.length,
|
||||
bodyFlow: report.basic.bodyFlow
|
||||
};
|
||||
}
|
||||
|
||||
function writeSnapshotPages(directory, prefix, snapshot) {
|
||||
for (const page of snapshot.pages) {
|
||||
if (page.raster) {
|
||||
fs.writeFileSync(
|
||||
path.join(directory, `${prefix}-${page.pageNumber}.png`),
|
||||
page.raster.png
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeReportArtifacts(directory, prefix, report) {
|
||||
fs.writeFileSync(
|
||||
path.join(directory, `${prefix}-report.json`),
|
||||
`${serializePdfVisualDiffJson(report)}\n`,
|
||||
"utf8"
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(directory, `${prefix}-report.html`),
|
||||
renderPdfVisualDiffHtml(report),
|
||||
"utf8"
|
||||
);
|
||||
for (const page of report.pages) {
|
||||
const pageNumber =
|
||||
page.pair.baselinePageNumber ?? page.pair.candidatePageNumber;
|
||||
if (!pageNumber || !page.artifacts) {
|
||||
continue;
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(directory, `${prefix}-overlay-${pageNumber}.png`),
|
||||
page.artifacts.overlayPng
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(directory, `${prefix}-heatmap-${pageNumber}.png`),
|
||||
page.artifacts.heatmapPng
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync(outputDirectory, { recursive: true });
|
||||
assert(
|
||||
selectedCases.length > 0,
|
||||
`视觉矩阵用例不存在:${selectedCaseId}`
|
||||
);
|
||||
const wordAdapter = createWordPdfAdapter({ timeoutMs: 240_000 });
|
||||
const wpsAdapter = createWpsPdfAdapter({ timeoutMs: 240_000 });
|
||||
const wordCapability = await wordAdapter.probe();
|
||||
const wpsCapability = await wpsAdapter.probe();
|
||||
assert(wordCapability.available, `Microsoft Word 不可用:${wordCapability.detail}`);
|
||||
assert(wpsCapability.available, `WPS Writer 不可用:${wpsCapability.detail}`);
|
||||
|
||||
const summaries = [];
|
||||
const gateFailures = [];
|
||||
for (const caseDefinition of selectedCases) {
|
||||
process.stderr.write(`[layout visual matrix] ${caseDefinition.id}\n`);
|
||||
const { caseDirectory, result } = await generateCase(caseDefinition);
|
||||
const chromiumPdfPath = path.resolve(
|
||||
repositoryDirectory,
|
||||
result.pdf.outputFile
|
||||
);
|
||||
const docxPath = path.resolve(repositoryDirectory, result.docx.outputFile);
|
||||
const editable = extractEditableContract(docxPath);
|
||||
const wordGeneration = await wordAdapter.generate({ docxPath });
|
||||
const wpsGeneration = await wpsAdapter.generate({ docxPath });
|
||||
const wordPdfPath = path.join(caseDirectory, "word.pdf");
|
||||
const wpsPdfPath = path.join(caseDirectory, "wps.pdf");
|
||||
fs.writeFileSync(wordPdfPath, wordGeneration.pdf);
|
||||
fs.writeFileSync(wpsPdfPath, wpsGeneration.pdf);
|
||||
|
||||
const chromium = await createPdfDocumentSnapshot(
|
||||
fs.readFileSync(chromiumPdfPath),
|
||||
{
|
||||
source: {
|
||||
kind: "chromium",
|
||||
label: `${caseDefinition.id} Chromium`,
|
||||
fileName: path.basename(chromiumPdfPath)
|
||||
},
|
||||
dpi: 144
|
||||
}
|
||||
);
|
||||
const word = await createPdfDocumentSnapshot(wordGeneration.pdf, {
|
||||
source: {
|
||||
kind: "word",
|
||||
label: `${caseDefinition.id} Microsoft Word`,
|
||||
fileName: path.basename(wordPdfPath)
|
||||
},
|
||||
dpi: 144
|
||||
});
|
||||
const wps = await createPdfDocumentSnapshot(wpsGeneration.pdf, {
|
||||
source: {
|
||||
kind: "wps",
|
||||
label: `${caseDefinition.id} WPS Writer`,
|
||||
fileName: path.basename(wpsPdfPath)
|
||||
},
|
||||
dpi: 144
|
||||
});
|
||||
const chromiumPageSemantics = buildPageSemantics(
|
||||
chromium.pageCount,
|
||||
caseDefinition.coverPageCount,
|
||||
result.exportConfig
|
||||
);
|
||||
const wordPageSemantics = buildPageSemantics(
|
||||
word.pageCount,
|
||||
caseDefinition.coverPageCount,
|
||||
result.exportConfig
|
||||
);
|
||||
const wpsPageSemantics = buildPageSemantics(
|
||||
wps.pageCount,
|
||||
caseDefinition.coverPageCount,
|
||||
result.exportConfig
|
||||
);
|
||||
const reportOptions = {
|
||||
thresholds: { strictPageCount: false },
|
||||
expectedEditableText: editable.text,
|
||||
expectedEditableParagraphs: editable.paragraphs
|
||||
};
|
||||
const wordReport = await createPdfVisualDiffReport(
|
||||
chromium,
|
||||
word,
|
||||
{
|
||||
...reportOptions,
|
||||
baselinePageSemantics: chromiumPageSemantics,
|
||||
candidatePageSemantics: wordPageSemantics
|
||||
}
|
||||
);
|
||||
const wpsReport = await createPdfVisualDiffReport(
|
||||
chromium,
|
||||
wps,
|
||||
{
|
||||
...reportOptions,
|
||||
baselinePageSemantics: chromiumPageSemantics,
|
||||
candidatePageSemantics: wpsPageSemantics
|
||||
}
|
||||
);
|
||||
const officeReport = await createPdfVisualDiffReport(word, wps, {
|
||||
...reportOptions,
|
||||
expectedEditableText: editable.text,
|
||||
baselinePageSemantics: wordPageSemantics,
|
||||
candidatePageSemantics: wpsPageSemantics
|
||||
});
|
||||
|
||||
const rasterDirectory = path.join(caseDirectory, "raster-pages");
|
||||
fs.rmSync(rasterDirectory, { recursive: true, force: true });
|
||||
fs.mkdirSync(rasterDirectory, { recursive: true });
|
||||
writeSnapshotPages(rasterDirectory, "chromium", chromium);
|
||||
writeSnapshotPages(rasterDirectory, "word", word);
|
||||
writeSnapshotPages(rasterDirectory, "wps", wps);
|
||||
writeReportArtifacts(rasterDirectory, "word", wordReport);
|
||||
writeReportArtifacts(rasterDirectory, "wps", wpsReport);
|
||||
writeReportArtifacts(rasterDirectory, "office", officeReport);
|
||||
|
||||
const currentFailures = [
|
||||
...pageGeometryFailures(chromium, caseDefinition, "Chromium"),
|
||||
...pageGeometryFailures(word, caseDefinition, "Microsoft Word"),
|
||||
...pageGeometryFailures(wps, caseDefinition, "WPS Writer"),
|
||||
...reportGateFailures(wordReport, "Chromium/Word"),
|
||||
...reportGateFailures(wpsReport, "Chromium/WPS"),
|
||||
...reportGateFailures(officeReport, "Word/WPS")
|
||||
];
|
||||
gateFailures.push(
|
||||
...currentFailures.map((failure) => `${caseDefinition.id}: ${failure}`)
|
||||
);
|
||||
summaries.push({
|
||||
id: caseDefinition.id,
|
||||
themeId: caseDefinition.themeId,
|
||||
paper: caseDefinition.paper,
|
||||
exportConfig: result.exportConfig,
|
||||
coverPageCount: caseDefinition.coverPageCount,
|
||||
pages: {
|
||||
chromium: chromium.pageCount,
|
||||
word: word.pageCount,
|
||||
wps: wps.pageCount
|
||||
},
|
||||
editableParagraphCount: editable.paragraphs.length,
|
||||
reports: {
|
||||
word: reportSummary(wordReport),
|
||||
wps: reportSummary(wpsReport),
|
||||
office: reportSummary(officeReport)
|
||||
},
|
||||
gateFailures: currentFailures,
|
||||
files: {
|
||||
chromiumPdf: path.relative(repositoryDirectory, chromiumPdfPath),
|
||||
docx: path.relative(repositoryDirectory, docxPath),
|
||||
wordPdf: path.relative(repositoryDirectory, wordPdfPath),
|
||||
wpsPdf: path.relative(repositoryDirectory, wpsPdfPath),
|
||||
rasterDirectory: path.relative(repositoryDirectory, rasterDirectory)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const summary = {
|
||||
schemaVersion: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
selectedCaseId,
|
||||
word: wordCapability,
|
||||
wps: wpsCapability,
|
||||
caseCount: summaries.length,
|
||||
gatePassed: gateFailures.length === 0,
|
||||
gateFailures,
|
||||
cases: summaries
|
||||
};
|
||||
const summaryPath = path.join(outputDirectory, "summary.json");
|
||||
fs.writeFileSync(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, "utf8");
|
||||
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
||||
assert(
|
||||
gateFailures.length === 0,
|
||||
`布局视觉矩阵存在 ${gateFailures.length} 个严格门禁问题,详见 ${path.relative(
|
||||
repositoryDirectory,
|
||||
summaryPath
|
||||
)}`
|
||||
);
|
||||
Reference in New Issue
Block a user