test: 扩展DOCX全主题视觉矩阵
This commit is contained in:
@@ -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("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user