Files
cjk-autospace/test/scan-regressions.test.mjs
admin 3a4fa1c932 fix: 修复行缘、切分还原与语言解析并同步文档
保留逐字 margin/jiya.bias 字体后备方案;补齐 demo 重排、量测边界说明及共享规则 1.0.1 反例向量。

验证:完整构建通过,Node 376 项、WKWebView 64 项及原生 demo 9 项通过;Firefox 仍暂缓。
2026-10-03 21:15:54 +08:00

190 lines
7.8 KiB
JavaScript
Raw Permalink 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 { test } from "node:test";
import { setupDom } from "./helpers.mjs";
import { createJuzhen } from "../dist/juzhen.mjs";
function sameLine(document)
{
const view = document.defaultView;
const previous = globalThis.ResizeObserver;
const elementRects = view.Element.prototype.getClientRects;
const rangeRects = view.Range.prototype.getClientRects;
const rect = { top: 0, bottom: 20, left: 0, right: 20, width: 20, height: 20 };
globalThis.ResizeObserver = class { observe() {} disconnect() {} };
view.Element.prototype.getClientRects = () => [ rect ];
view.Range.prototype.getClientRects = () => [ rect ];
return () =>
{
view.Element.prototype.getClientRects = elementRects;
view.Range.prototype.getClientRects = rangeRects;
if (previous === undefined) { delete globalThis.ResizeObserver; }
else { globalThis.ResizeObserver = previous; }
};
}
for (const orphan of [ false, true ])
{
test("line edge:同行 orphan 不製造行首 orphan=" + orphan, () =>
{
const { dom, document } = setupDom('<main lang="zh-Hant"><p>前甲「乙丙」</p></main>');
const restore = sameLine(document);
const jz = createJuzhen({ orphan, spacing: false, longWord: false });
try
{
jz.render(document.querySelector("main"));
assert.equal(document.querySelector(".bd-open").classList.contains("jz-half-le"), false);
}
finally { jz.revert(); restore(); dom.window.close(); }
});
}
for (const display of [ "inline", "inline-block", "contents", "block" ])
{
test("line edge:自訂隔離元素按實際物理盒攀爬 display=" + display, () =>
{
const { dom, document } = setupDom('<p lang="zh-Hant">前甲<x-box style="display:'
+ display + '">「乙」</x-box>丙</p>');
const restore = sameLine(document);
const jz = createJuzhen({ isolateSelector: "x-box", orphan: false, spacing: false, longWord: false });
try
{
jz.render();
for (const char of document.querySelectorAll("jz-char"))
{
assert.equal(char.classList.contains("jz-half-le"), display === "block");
}
}
finally { jz.revert(); restore(); dom.window.close(); }
});
}
for (const hidden of [ false, true ])
{
test("line edge:BR 為強制行緣,隱藏時可穿越 hidden=" + hidden, () =>
{
const { dom, document } = setupDom('<p lang="zh-Hant">甲」<br'
+ (hidden ? ' style="display:none"' : '') + '>「乙</p>');
const restore = sameLine(document);
const jz = createJuzhen({ orphan: false, spacing: false, longWord: false });
try
{
jz.render();
for (const char of document.querySelectorAll("jz-char"))
{
assert.equal(char.classList.contains("jz-half-le"), !hidden);
}
}
finally { jz.revert(); restore(); dom.window.close(); }
});
}
for (const action of [ "insert", "replace-source", "edit-fragment", "edit-source" ])
{
test("splitoff:回退須驗來源身份並保留外部樣式 action=" + action, () =>
{
const { dom, document } = setupDom('<p><strong class="original">甲乙丙</strong>,丁</p>');
const p = document.querySelector("p");
const source = p.querySelector("strong");
const jz = createJuzhen({ orphan: false, spacing: false, longWord: false });
try
{
jz.render(p);
const fragment = p.querySelector('[data-jz-kind="splitoff"]');
assert.ok(fragment);
if (action === "insert")
{
const added = document.createElement("strong");
added.className = "external";
added.textContent = "新";
fragment.before(added);
}
else if (action === "replace-source")
{
const replacement = source.cloneNode(false);
replacement.textContent = "替";
source.replaceWith(replacement);
}
else if (action === "edit-fragment") { fragment.className = "edited"; }
else { source.className = "edited"; }
jz.revert(p);
assert.equal(fragment.parentNode, p, "來源失效時保留片段當前父與樣式");
assert.equal(fragment.textContent, "丙");
assert.equal(fragment.className, action === "edit-fragment" ? "edited" : "original");
assert.equal(p.querySelector("[data-jz]"), null);
if (action === "insert")
{
assert.equal(p.querySelector(".external").textContent, "新");
assert.equal(p.textContent, "甲乙新丙,丁");
}
if (action === "replace-source") { assert.equal(p.textContent, "替丙,丁"); }
jz.revert(p);
assert.equal(fragment.textContent, "丙", "撤回所有權後再次 revert 不碰片段");
}
finally { jz.revert(p); dom.window.close(); }
});
}
test("splitoff:同來源多層切分仍完整還原作者結構", () =>
{
const { dom, document } = setupDom('<p>「<strong><em>甲乙丙丁</em></strong>」</p>');
const p = document.querySelector("p");
const original = p.innerHTML;
const jz = createJuzhen();
try { jz.render(p); jz.revert(p); assert.equal(p.innerHTML, original); }
finally { jz.revert(p); dom.window.close(); }
});
for (const [ html, defaultLang, half ] of [
[ '<main lang="zh-Hans"><p lang="">甲,乙</p></main>', "zh-Hans", false ],
[ '<main><p>甲,乙</p></main>', "", false ],
[ '<main lang="zh-Hant"><p lang="zh-Hans-x-hant">甲,乙</p></main>', "zh-Hant", true ],
[ '<main lang="zh-Hans"><p lang="zh-TW-x-hans">甲,乙</p></main>', "zh-Hans", false ],
])
{
test("locale:空值及私用內容不冒充繼承語言 " + html, () =>
{
const { dom, document } = setupDom(html);
const jz = createJuzhen({ lang: { default: defaultLang }, jinze: false, orphan: false });
try
{
jz.render(document.querySelector("main"));
assert.equal(document.querySelector("jz-char").classList.contains("jz-half"), half);
}
finally { jz.revert(); dom.window.close(); }
});
}
test("splitoff:多實例六種回退順序恢復原結構,36 組合", () =>
{
const fixtures = [
'<strong>甲乙丙丁</strong>,戊己',
'「<strong>甲乙丙丁</strong>」',
'<strong><em>甲乙丙丁</em></strong>,戊己',
'<strong class="author" style="color:red">甲乙丙丁</strong>,戊己',
'<strong>甲乙𠀀\uDB40\uDD00丙丁</strong>,戊己',
'<strong data-jz="author" data-jz-kind="author">甲乙丙丁</strong>,戊己',
];
const orders = [ [ 0, 1, 2 ], [ 0, 2, 1 ], [ 1, 0, 2 ], [ 1, 2, 0 ], [ 2, 0, 1 ], [ 2, 1, 0 ] ];
for (const html of fixtures)
{
for (const order of orders)
{
const { dom, document } = setupDom('<p lang="zh-Hant">' + html + '</p>');
const p = document.querySelector("p");
const original = p.innerHTML;
const owners = [
createJuzhen({ jiya: false, spacing: false, orphan: false, longWord: false }),
createJuzhen({ jiya: false, spacing: false, jinze: false, longWord: false }),
createJuzhen({ spacing: false, orphan: false, longWord: false }),
];
try
{
owners.forEach((owner) => owner.render(p));
order.forEach((index) => owners[index].revert(p));
assert.equal(p.innerHTML, original, html + " order=" + order.join(","));
}
finally { owners.forEach((owner) => owner.disconnect()); dom.window.close(); }
}
}
});