Files
MorphDoc/scripts/docx-layout-visual-matrix-cases.test.mjs
T
SkyJourney 2c5c1bd317 release: 发布 v0.6.1 DOCX 视觉一致性修复
新增通用 CSS 到 OOXML 翻译修复,统一字体、字距、精确行距、段落、列表、表格、引用、代码块与行内代码连续性,不引入按主题 ID 分支。

新增 MdTP Mono 并统一 Serif、Sans、Mono 三字体包的 Chromium 与 DOCX 使用链;字体声明、嵌入部件和 Word/WPS 实际采用均进入硬门禁。

重建封面整页及正文语义块视觉差分,14 套主题、纵横两个方向、五组页边距共 140 个真实场景全部通过,阻断失败和诊断失败均为零。

源码服务、Docker Web API 与实际安装 Desktop 的 red-briefing 导出均包含 5 个字体部件;Word/WPS 原生渲染和逐页复核通过。修复 Docker 构建上下文与运行层复用软链接,并完善 v0.6.1 版本、发行说明和发布归集。

验证:npm test(116 个文件、616 项测试)、npm run typecheck、npm run build、git diff --check 全部通过。Desktop 安装器与 ZIP、Docker v0.6.1 镜像已生成;Windows 产物仍为未签名内部发行。
2026-08-04 10:30:44 +08:00

159 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
getDocxFontGateFailures,
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("字体门禁区分缺少声明、缺少嵌入和 Office 未实际采用", () => {
const missing = getDocxFontGateFailures({
theme: { id: "missing", docxFontFaces: [] },
inspection: { embeddedFontPartCount: 0, embeddedFontNames: [] },
renderedFonts: [],
engine: "word"
});
assert.equal(missing.length, 2);
const fallback = getDocxFontGateFailures({
theme: {
id: "declared",
docxFontFaces: [{ family: "FandolSong", aliases: [], weight: 400, style: "normal" }]
},
inspection: { embeddedFontPartCount: 1, embeddedFontNames: ["MdTP Serif SC"] },
renderedFonts: [{ name: "SimSun" }],
engine: "word"
});
assert.deepEqual(fallback, ["Microsoft Word 输出未实际使用 DOCX 嵌入字体"]);
const embedded = getDocxFontGateFailures({
theme: {
id: "declared",
docxFontFaces: [{ family: "FandolSong", aliases: [], weight: 400, style: "normal" }]
},
inspection: { embeddedFontPartCount: 1, embeddedFontNames: ["MdTP Serif SC"] },
renderedFonts: [{ name: "___WRD_EMBED_SUB_65" }],
engine: "word"
});
assert.deepEqual(embedded, []);
const embeddedWithEngineFaces = getDocxFontGateFailures({
theme: {
id: "engine-faces",
docxFontFaces: [{ family: "FandolSong", aliases: [], weight: 400, style: "normal" }]
},
inspection: {
embeddedFontPartCount: 3,
embeddedFontNames: ["MdTP Serif SC", "MdTP Mono"]
},
renderedFonts: [{ name: "WPSEMBED1" }],
engine: "wps"
});
assert.deepEqual(embeddedWithEngineFaces, []);
});
test("14 套内置主题都声明字体且中文字体面不超过三个", () => {
const { themes } = matrix();
const cjkFamilies = /(?:Fandol|MdTP|Song|Hei|Kai|Fang|Sim|YaHei|CJK|Han)/iu;
for (const theme of themes) {
assert.ok(theme.docxFontFaces.length > 0, `${theme.id} 缺少 DOCX 字体声明`);
assert.ok(theme.docxFontFaces.length <= 16, `${theme.id} 字体面超过协议上限`);
const cjkFaces = theme.docxFontFaces.filter((face) =>
[face.family, ...face.aliases].some((family) => cjkFamilies.test(family))
);
assert.ok(cjkFaces.length <= 3, `${theme.id} 中文字体面超过三个`);
}
});
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"
]
);
});