test: 扩展DOCX全主题视觉矩阵

This commit is contained in:
SkyJourney
2026-08-03 10:23:22 +08:00
parent 28c88faabb
commit f0fff7f726
6 changed files with 440 additions and 126 deletions
+10 -3
View File
@@ -1492,9 +1492,16 @@ ECharts 第二阶段浏览器与 PDF 验证结果:
全项目类型检查和生产构建通过;真实 GitHub 紧凑 Letter 纵向场景观测
22 个正文块、输出 102 张失败局部热力图并正确阻断。PDF.js 泛化字体名
不作为字体硬门禁,真实字体解析留在阶段 D;
- 阶段 C将语义块门禁扩展为 14 套内置主题 × 纵横两个方向 × 主题默认、
标准、紧凑、宽松、非对称装订五组页边距,共 140 个排版场景;主题
默认链路即使与显式数值相同也不得去重;
- 阶段 C已完成。矩阵生成器从 14 套内置主题清单生成纵横两个方向 ×
主题默认、标准、紧凑、宽松、非对称装订五组页边距,共 140 个唯一
排版场景;主题默认链路保持 `marginMode: theme`即使与显式数值相同
也不去重。机器报告记录定义、执行完整度以及主题、方向、边距分层汇总,
HTML 报告固定显示 14 行 × 10 列并链接三份引擎详细报告。5 项矩阵结构
测试通过;真实 `red-briefing--portrait--theme-default` 验证解析为主题
默认 25mm 四边距,14 个正文块中 Word 有 13 个失败、字体部件仍为 0,
总计 76 个阻断问题;`formal-feasibility--landscape--binding` 验证 A4
横向、非对称装订边距和独立封面整页 strict 门禁,封面因墨迹与边缘
IoU 不达标被阻断;全仓库测试、类型检查、生产构建和差异检查通过;
- 阶段 D:修复主题封面、块元素样式和字体解析、嵌入、回退;
- 阶段 E:完成源码服务、Docker Web API、Desktop 安装版发布后复验;
- 阶段 F:完成 `v0.6.1` 版本、发行说明、发布产物、发布提交和标签。
+6
View File
@@ -116,6 +116,12 @@ flowchart LR
Word PDF 和 WPS PDF,并在报告中保留主题、方向、边距场景、导出链和
渲染引擎维度,不允许用汇总绿灯掩盖单个失败。
场景 ID 固定为 `<theme-id>--<orientation>--<margin-scenario>`。总报告同时
输出机器可读 JSON 和 14 行 × 10 列 HTML 矩阵;每个单元格必须显示通过、
失败或未执行,并链接到 Chromium/Word、Chromium/WPS、Word/WPS 三份
详细报告。完整门禁必须执行 140 个场景后才允许标记 `complete: true`
单场景筛选只用于开发复现,不能冒充全矩阵结果。
## 4. 实施阶段
1. 阶段 A:修复 failure 聚合策略,补齐四套封面场景和封面正文隔离;
+3 -2
View File
@@ -34,6 +34,7 @@
"desktop:make:with-font-pack": "npm run build:font-pack:windows && npm run build:font-pack:archive && node scripts/build-desktop-with-font-pack.mjs",
"release:stage": "node scripts/stage-release.mjs",
"test:release-stage": "node --test scripts/stage-release.test.mjs",
"test:docx-layout-visual-matrix-cases": "node --test scripts/docx-layout-visual-matrix-cases.test.mjs",
"theme:import-typora": "node scripts/import-typora-theme.mjs",
"verify:docx-reference": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:pandoc -w @md-to-pdf/docx-engine",
"verify:docx-conversion": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:conversion -w @md-to-pdf/docx-engine",
@@ -45,8 +46,8 @@
"verify:docx-r4-matrix": "npm run verify:docx-themes && npm run verify:font-pack && npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run verify:docx-r4-matrix -w @md-to-pdf/server",
"verify:docx-page-decorations": "npm run build:web-runtime && npm run build -w @md-to-pdf/document-visual-diff && npm run build -w @md-to-pdf/server && node scripts/verify-docx-page-decoration-visuals.mjs",
"verify:docx-media-visuals": "npm run build:web-runtime && npm run build -w @md-to-pdf/document-visual-diff && npm run build -w @md-to-pdf/server && node scripts/verify-docx-media-visuals.mjs",
"verify:docx-layout-visual-matrix": "npm run verify:docx-r4-matrix && npm run build -w @md-to-pdf/document-visual-diff && node scripts/verify-docx-layout-visual-matrix.mjs",
"test": "npm run test:release-stage && npm run test -w @md-to-pdf/markdown-echarts && npm run test -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run test -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/semantic-document && npm run test -w @md-to-pdf/docx-theme-engine && npm run test -w @md-to-pdf/document-visual-diff && npm run test -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/font-pack-registry && npm run test -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/docx-engine && npm run test -w @md-to-pdf/font-pack-builder && npm run test -w @md-to-pdf/renderer && npm run test -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run test -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run test -w @md-to-pdf/web && npm run test -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop",
"verify:docx-layout-visual-matrix": "npm run test:docx-layout-visual-matrix-cases && npm run verify:font-pack && npm run build:web-runtime && npm run build -w @md-to-pdf/document-visual-diff && npm run build -w @md-to-pdf/server && node scripts/verify-docx-layout-visual-matrix.mjs",
"test": "npm run test:release-stage && npm run test:docx-layout-visual-matrix-cases && npm run test -w @md-to-pdf/markdown-echarts && npm run test -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run test -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/semantic-document && npm run test -w @md-to-pdf/docx-theme-engine && npm run test -w @md-to-pdf/document-visual-diff && npm run test -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/font-pack-registry && npm run test -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/docx-engine && npm run test -w @md-to-pdf/font-pack-builder && npm run test -w @md-to-pdf/renderer && npm run test -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run test -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run test -w @md-to-pdf/web && npm run test -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop",
"typecheck": "npm run typecheck -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/markdown-echarts && npm run typecheck -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run typecheck -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/semantic-document && npm run typecheck -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-theme-engine && npm run typecheck -w @md-to-pdf/document-visual-diff && npm run typecheck -w @md-to-pdf/font-pack-registry && npm run typecheck -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/docx-engine && npm run typecheck -w @md-to-pdf/font-pack-builder && npm run typecheck -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/renderer && npm run typecheck -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run typecheck -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run typecheck -w @md-to-pdf/web && npm run typecheck -w @md-to-pdf/server && npm run typecheck -w @md-to-pdf/desktop"
},
"engines": {
+203
View File
@@ -0,0 +1,203 @@
import fs from "node:fs";
import path from "node:path";
export const DOCX_LAYOUT_MATRIX_ORIENTATIONS = Object.freeze([
"portrait",
"landscape"
]);
export const DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS = Object.freeze([
Object.freeze({
id: "theme-default",
label: "主题默认边距",
marginMode: "theme"
}),
Object.freeze({
id: "standard",
label: "标准边距",
marginMode: "custom",
margins: Object.freeze({
top: "25.4mm",
right: "25.4mm",
bottom: "25.4mm",
left: "25.4mm"
})
}),
Object.freeze({
id: "compact",
label: "紧凑边距",
marginMode: "custom",
margins: Object.freeze({
top: "12.7mm",
right: "12.7mm",
bottom: "12.7mm",
left: "12.7mm"
})
}),
Object.freeze({
id: "wide",
label: "宽松边距",
marginMode: "custom",
margins: Object.freeze({
top: "31.8mm",
right: "31.8mm",
bottom: "31.8mm",
left: "31.8mm"
})
}),
Object.freeze({
id: "binding",
label: "非对称装订边距",
marginMode: "custom",
margins: Object.freeze({
top: "25.4mm",
right: "19.1mm",
bottom: "25.4mm",
left: "31.8mm"
})
})
]);
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function hasIndependentCover(manifest) {
const profiles = Array.isArray(manifest.compatibleProfiles)
? manifest.compatibleProfiles
: [];
return profiles.includes("project-report") || profiles.includes("tender");
}
export function readBundledThemeMatrixDefinitions(themesDirectory) {
assert(
typeof themesDirectory === "string" && themesDirectory.length > 0,
"主题目录不能为空"
);
return fs.readdirSync(themesDirectory, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.flatMap((entry) => {
const manifestPath = path.join(themesDirectory, entry.name, "theme.json");
if (!fs.existsSync(manifestPath)) {
return [];
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
if (manifest.bundled !== true) {
return [];
}
assert(
typeof manifest.id === "string" && manifest.id.length > 0,
`内置主题清单缺少 id${manifestPath}`
);
return [{
id: manifest.id,
name:
typeof manifest.name === "string" && manifest.name.length > 0
? manifest.name
: manifest.id,
coverPageCount: hasIndependentCover(manifest) ? 1 : 0,
compatibleProfiles: Array.isArray(manifest.compatibleProfiles)
? [...manifest.compatibleProfiles]
: [],
themeDefaultMargins: manifest.pageDefaults?.margins
? structuredClone(manifest.pageDefaults.margins)
: undefined
}];
})
.sort((left, right) => left.id.localeCompare(right.id, "en"));
}
function createPaperConfig(orientation, marginScenario) {
return {
format: "A4",
orientation,
marginMode: marginScenario.marginMode,
...(marginScenario.marginMode === "custom"
? { margins: structuredClone(marginScenario.margins) }
: {})
};
}
export function createDocxLayoutVisualMatrixCases(themeDefinitions) {
assert(Array.isArray(themeDefinitions), "主题定义必须是数组");
const ids = new Set();
const cases = [];
for (const theme of themeDefinitions) {
assert(
theme && typeof theme.id === "string" && theme.id.length > 0,
"矩阵主题必须包含非空 id"
);
assert(!ids.has(theme.id), `矩阵主题 ID 重复:${theme.id}`);
ids.add(theme.id);
for (const orientation of DOCX_LAYOUT_MATRIX_ORIENTATIONS) {
for (const marginScenario of DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS) {
const paper = createPaperConfig(orientation, marginScenario);
cases.push({
id: `${theme.id}--${orientation}--${marginScenario.id}`,
themeId: theme.id,
themeName: theme.name,
orientation,
marginScenarioId: marginScenario.id,
marginScenarioLabel: marginScenario.label,
coverPageCount: theme.coverPageCount,
themeDefaultMargins: theme.themeDefaultMargins
? structuredClone(theme.themeDefaultMargins)
: undefined,
paper: { format: "A4", orientation },
config: { paper }
});
}
}
}
return cases;
}
export function summarizeDocxLayoutVisualMatrix(themeDefinitions, cases) {
const themeIds = themeDefinitions.map((theme) => theme.id);
const caseIds = cases.map((entry) => entry.id);
const expectedCaseCount =
themeIds.length *
DOCX_LAYOUT_MATRIX_ORIENTATIONS.length *
DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS.length;
assert(
new Set(caseIds).size === caseIds.length,
"DOCX 布局视觉矩阵存在重复场景 ID"
);
assert(
cases.length === expectedCaseCount,
`DOCX 布局视觉矩阵场景数错误:${cases.length},期望 ${expectedCaseCount}`
);
for (const themeId of themeIds) {
const themeCaseCount = cases.filter(
(entry) => entry.themeId === themeId
).length;
assert(
themeCaseCount ===
DOCX_LAYOUT_MATRIX_ORIENTATIONS.length *
DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS.length,
`主题 ${themeId} 的矩阵场景数错误:${themeCaseCount}`
);
}
return {
themeCount: themeIds.length,
themeIds,
orientationCount: DOCX_LAYOUT_MATRIX_ORIENTATIONS.length,
orientations: [...DOCX_LAYOUT_MATRIX_ORIENTATIONS],
marginScenarioCount: DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS.length,
marginScenarios: DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS.map((scenario) => ({
id: scenario.id,
label: scenario.label,
marginMode: scenario.marginMode,
...(scenario.margins
? { margins: structuredClone(scenario.margins) }
: {})
})),
expectedCaseCount,
coverThemeCount: themeDefinitions.filter(
(theme) => theme.coverPageCount === 1
).length,
coverCaseCount: cases.filter((entry) => entry.coverPageCount === 1).length
};
}
@@ -0,0 +1,98 @@
import assert from "node:assert/strict";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import {
createDocxLayoutVisualMatrixCases,
DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS,
DOCX_LAYOUT_MATRIX_ORIENTATIONS,
readBundledThemeMatrixDefinitions,
summarizeDocxLayoutVisualMatrix
} from "./docx-layout-visual-matrix-cases.mjs";
const repositoryDirectory = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
".."
);
const themesDirectory = path.join(repositoryDirectory, "themes");
function matrix() {
const themes = readBundledThemeMatrixDefinitions(themesDirectory);
const cases = createDocxLayoutVisualMatrixCases(themes);
return { themes, cases, summary: summarizeDocxLayoutVisualMatrix(themes, cases) };
}
test("生成 14×2×5 的完整 DOCX 布局视觉矩阵", () => {
const { cases, summary } = matrix();
assert.equal(summary.themeCount, 14);
assert.equal(summary.orientationCount, 2);
assert.equal(summary.marginScenarioCount, 5);
assert.equal(summary.expectedCaseCount, 140);
assert.equal(cases.length, 140);
assert.equal(new Set(cases.map((entry) => entry.id)).size, 140);
assert.deepEqual(summary.orientations, ["portrait", "landscape"]);
});
test("每套主题都覆盖两个方向和五组边距", () => {
const { themes, cases } = matrix();
for (const theme of themes) {
const themeCases = cases.filter((entry) => entry.themeId === theme.id);
assert.equal(themeCases.length, 10, theme.id);
assert.deepEqual(
[...new Set(themeCases.map((entry) => entry.orientation))],
DOCX_LAYOUT_MATRIX_ORIENTATIONS
);
assert.deepEqual(
[...new Set(themeCases.map((entry) => entry.marginScenarioId))],
DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS.map((scenario) => scenario.id)
);
}
});
test("主题默认边距始终保留 theme 模式且不写 custom 数值", () => {
const { themes, cases } = matrix();
const themeDefaultCases = cases.filter(
(entry) => entry.marginScenarioId === "theme-default"
);
assert.equal(themeDefaultCases.length, 28);
for (const entry of themeDefaultCases) {
assert.equal(entry.config.paper.marginMode, "theme");
assert.equal("margins" in entry.config.paper, false);
const theme = themes.find((candidate) => candidate.id === entry.themeId);
assert.deepEqual(entry.themeDefaultMargins, theme?.themeDefaultMargins);
}
});
test("显式边距场景全部使用 custom 模式并保留稳定数值", () => {
const { cases } = matrix();
for (const scenario of DOCX_LAYOUT_MATRIX_MARGIN_SCENARIOS.slice(1)) {
const scenarioCases = cases.filter(
(entry) => entry.marginScenarioId === scenario.id
);
assert.equal(scenarioCases.length, 28, scenario.id);
for (const entry of scenarioCases) {
assert.equal(entry.config.paper.marginMode, "custom");
assert.deepEqual(entry.config.paper.margins, scenario.margins);
}
}
});
test("四套独立封面主题扩展为 40 个严格封面场景", () => {
const { cases, summary } = matrix();
assert.equal(summary.coverThemeCount, 4);
assert.equal(summary.coverCaseCount, 40);
assert.deepEqual(
[...new Set(
cases
.filter((entry) => entry.coverPageCount === 1)
.map((entry) => entry.themeId)
)],
[
"formal-feasibility",
"tender-blind",
"tender-business-blue",
"tender-classic"
]
);
});
+120 -121
View File
@@ -18,6 +18,11 @@ import {
formatPageNumber,
resolvePageNumberAlignment
} from "../packages/preview-engine/dist/index.js";
import {
createDocxLayoutVisualMatrixCases,
readBundledThemeMatrixDefinitions,
summarizeDocxLayoutVisualMatrix
} from "./docx-layout-visual-matrix-cases.mjs";
const execFileAsync = promisify(execFile);
const WORD_NAMESPACE =
@@ -39,127 +44,16 @@ const matrixScript = path.join(
"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] }
A4: { portrait: [595.276, 841.89], landscape: [841.89, 595.276] }
};
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: "tender-blind-cover-a4-portrait",
themeId: "tender-blind",
coverPageCount: 1,
paper: { format: "A4", orientation: "portrait" },
config: {}
},
{
id: "tender-classic-cover-a4-portrait",
themeId: "tender-classic",
coverPageCount: 1,
paper: { format: "A4", orientation: "portrait" },
config: {}
},
{
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 themeDefinitions = readBundledThemeMatrixDefinitions(
path.join(repositoryDirectory, "themes")
);
const cases = createDocxLayoutVisualMatrixCases(themeDefinitions);
const matrixDefinition = summarizeDocxLayoutVisualMatrix(
themeDefinitions,
cases
);
const selectedCaseId =
process.env.MD_TO_PDF_LAYOUT_VISUAL_CASE?.trim() || undefined;
@@ -415,6 +309,83 @@ function reportSummary(report) {
};
}
function summarizeDimension(entries, key) {
return Object.fromEntries(
[...new Set(entries.map((entry) => entry[key]))]
.sort((left, right) => left.localeCompare(right, "en"))
.map((value) => {
const dimensionCases = entries.filter((entry) => entry[key] === value);
const failedCases = dimensionCases.filter(
(entry) => entry.gateFailures.length > 0
);
return [value, {
caseCount: dimensionCases.length,
passedCaseCount: dimensionCases.length - failedCases.length,
failedCaseCount: failedCases.length,
gatePassed: failedCases.length === 0
}];
})
);
}
function summarizeExecution(entries) {
return {
byTheme: summarizeDimension(entries, "themeId"),
byOrientation: summarizeDimension(entries, "orientation"),
byMarginScenario: summarizeDimension(entries, "marginScenarioId")
};
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function renderMatrixSummaryHtml(summary) {
const caseByDimension = new Map(
summary.cases.map((entry) => [
`${entry.themeId}|${entry.orientation}|${entry.marginScenarioId}`,
entry
])
);
const columns = summary.matrixDefinition.orientations.flatMap(
(orientation) =>
summary.matrixDefinition.marginScenarios.map((marginScenario) => ({
orientation,
marginScenario
}))
);
const header = columns.map(({ orientation, marginScenario }) =>
`<th><span>${orientation === "portrait" ? "纵向" : "横向"}</span><small>${escapeHtml(marginScenario.label)}</small></th>`
).join("");
const rows = summary.matrixDefinition.themeIds.map((themeId) => {
const cells = columns.map(({ orientation, marginScenario }) => {
const entry = caseByDimension.get(
`${themeId}|${orientation}|${marginScenario.id}`
);
if (!entry) {
return '<td class="not-run">未执行</td>';
}
const failed = entry.gateFailures.length > 0;
const base = `./${entry.id}/raster-pages`;
return `<td class="${failed ? "failed" : "passed"}"><strong>${failed ? `失败 ${entry.gateFailures.length}` : "通过"}</strong><nav><a href="${base}/word-report.html">Word</a><a href="${base}/wps-report.html">WPS</a><a href="${base}/office-report.html">Office</a></nav></td>`;
}).join("");
const themeName = summary.cases.find(
(entry) => entry.themeId === themeId
)?.themeName;
return `<tr><th class="theme"><strong>${escapeHtml(themeId)}</strong><small>${escapeHtml(themeName ?? "")}</small></th>${cells}</tr>`;
}).join("");
return `<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>DOCX 视觉门禁矩阵</title>
<style>
:root{font-family:Inter,"Microsoft YaHei",sans-serif;color:#172033;background:#f3f5f8}body{margin:0;padding:24px}main{max-width:1800px;margin:auto}h1{margin:0 0 8px}.summary{margin:0 0 20px;color:#475467}.table-wrap{overflow:auto;background:#fff;border:1px solid #d0d5dd;border-radius:10px}table{border-collapse:collapse;min-width:1500px;width:100%}th,td{border:1px solid #e4e7ec;padding:9px;text-align:center;vertical-align:top}thead th{position:sticky;top:0;background:#f8fafc;z-index:1}.theme{position:sticky;left:0;background:#fff;text-align:left;min-width:190px;z-index:2}th span,th small,.theme strong,.theme small{display:block}.passed{background:#e6f7ec}.failed{background:#ffe4e4}.not-run{background:#f2f4f7;color:#667085}nav{display:flex;justify-content:center;gap:6px;margin-top:6px}a{font-size:12px;color:#175cd3}small{margin-top:3px;color:#667085;font-weight:400}
</style></head><body><main><h1>DOCX 视觉门禁矩阵</h1><p class="summary">定义:${summary.matrixDefinition.themeCount} 套主题 × ${summary.matrixDefinition.orientationCount} 个方向 × ${summary.matrixDefinition.marginScenarioCount} 组页边距 = ${summary.matrixDefinition.expectedCaseCount} 个场景;本次执行 ${summary.execution.executedCaseCount} 个,整体 ${summary.gatePassed ? "通过" : "失败"}。</p><div class="table-wrap"><table><thead><tr><th class="theme">主题</th>${header}</tr></thead><tbody>${rows}</tbody></table></div></main></body></html>`;
}
function writeSnapshotPages(directory, prefix, snapshot) {
for (const page of snapshot.pages) {
if (page.raster) {
@@ -479,6 +450,13 @@ function writeReportArtifacts(directory, prefix, report) {
}
fs.mkdirSync(outputDirectory, { recursive: true });
assert(
matrixDefinition.themeCount === 14 &&
matrixDefinition.orientationCount === 2 &&
matrixDefinition.marginScenarioCount === 5 &&
matrixDefinition.expectedCaseCount === 140,
`布局视觉矩阵定义必须为 14×2×5=140,实际为 ${JSON.stringify(matrixDefinition)}`
);
assert(
selectedCases.length > 0,
`视觉矩阵用例不存在:${selectedCaseId}`
@@ -604,6 +582,11 @@ for (const caseDefinition of selectedCases) {
summaries.push({
id: caseDefinition.id,
themeId: caseDefinition.themeId,
themeName: caseDefinition.themeName,
orientation: caseDefinition.orientation,
marginScenarioId: caseDefinition.marginScenarioId,
marginScenarioLabel: caseDefinition.marginScenarioLabel,
themeDefaultMargins: caseDefinition.themeDefaultMargins,
paper: caseDefinition.paper,
exportConfig: result.exportConfig,
coverPageCount: caseDefinition.coverPageCount,
@@ -632,7 +615,15 @@ for (const caseDefinition of selectedCases) {
const summary = {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
matrixDefinition,
selectedCaseId,
execution: {
mode: selectedCaseId ? "single-case" : "full-matrix",
executedCaseCount: summaries.length,
expectedFullMatrixCaseCount: matrixDefinition.expectedCaseCount,
complete: !selectedCaseId && summaries.length === matrixDefinition.expectedCaseCount
},
executionSummary: summarizeExecution(summaries),
word: wordCapability,
wps: wpsCapability,
caseCount: summaries.length,
@@ -640,8 +631,16 @@ const summary = {
gateFailures,
cases: summaries
};
const summaryPath = path.join(outputDirectory, "summary.json");
const summaryPath = path.join(
outputDirectory,
selectedCaseId ? `summary-${selectedCaseId}.json` : "summary.json"
);
const matrixHtmlPath = path.join(
outputDirectory,
selectedCaseId ? `matrix-${selectedCaseId}.html` : "matrix.html"
);
fs.writeFileSync(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, "utf8");
fs.writeFileSync(matrixHtmlPath, renderMatrixSummaryHtml(summary), "utf8");
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
assert(
gateFailures.length === 0,