Files
cjk-autospace/test/systematic-interactions.test.mjs

327 lines
14 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 { execFileSync } from "node:child_process";
import { setupDom } from "./helpers.mjs";
import { createJuzhen } from "../dist/juzhen.mjs";
const ONLY_SPACING = { jiya: false, jinze: false, orphan: false, longWord: false };
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
for (const emptied of [ false, true ])
{
test("revert provenance:重用已撤回容器 emptied=" + emptied + " 不循環或丟字", () =>
{
// 有界子進程使來源鏈循環的回歸失敗可終止,不能卡住整套測試。
const script = `
import assert from 'node:assert/strict';
import { setupDom } from ${JSON.stringify(new URL("./helpers.mjs", import.meta.url).href)};
import { createJuzhen } from ${JSON.stringify(new URL("../dist/juzhen.mjs", import.meta.url).href)};
const {dom,document}=setupDom('<p></p>');const root=document.querySelector('p');
for(const t of ['甲','「','禰','\\uFE00','A'])root.append(document.createTextNode(t));
const B=createJuzhen({spacing:false,orphan:false,longWord:false});
const C=createJuzhen({spacing:false,jiya:false,jinze:false,longWord:false});
const A=createJuzhen({jiya:false,jinze:false,orphan:false,longWord:false});
B.render(root);const oldJ=root.querySelector('jz-jinze');
C.render(root);const oldO=root.querySelector('jz-orphan');A.render(root);
B.revert(root);oldJ.append(oldO);C.revert(oldJ);
let request=oldJ;
if(${emptied}){root.append(oldJ.querySelector('jz-hws'));oldJ.replaceChildren();request=root;}
const text=request.textContent;A.revert(request);assert.equal(request.textContent,text);
A.disconnect();B.disconnect();C.disconnect();dom.window.close();
console.log('bounded revert PASS');
`;
const output = execFileSync(process.execPath, [ "--input-type=module", "-e", script ],
{ encoding: "utf8", timeout: 5000 });
assert.match(output, /bounded revert PASS/);
});
}
for (const [name, attrs, options] of [
[ "level", 'data-jz-level="text"', {} ],
[ "off", 'data-juzhen-off="jiya orphan jinze longWord spacing"', {} ],
[ "avoid", "data-jz-skip", {} ],
[ "lang", 'lang="zh-Hans"', {} ],
[ "pill", 'class="pill"', { pillSelector: ".pill" } ],
])
{
test("observer relocate:" + name + " 的增量結果與完整重排一致", async () =>
{
const { dom, document } = setupDom('<main><section id="from"><p>文字ABCDEF,甲乙。</p></section>'
+ '<section id="to" ' + attrs + '></section></main>');
const root = document.querySelector("main");
const paragraph = root.querySelector("p");
const jz = createJuzhen(options);
try
{
jz.render(root);
jz.observe(root);
document.getElementById("to").append(paragraph);
await tick();
jz.disconnect();
const incremental = paragraph.outerHTML;
jz.rerender(root);
assert.equal(incremental, paragraph.outerHTML);
assert.equal(paragraph.textContent, "文字ABCDEF,甲乙。");
}
finally { jz.revert(root); jz.disconnect(); dom.window.close(); }
});
}
test("observer relocate:舊 avoid 快取不帶入新父範圍", async () =>
{
const { dom, document } = setupDom('<main><section data-jz-skip><p>文字ABC,甲乙。</p></section><section id="to"></section></main>');
const root = document.querySelector("main");
const paragraph = root.querySelector("p");
const jz = createJuzhen();
try
{
jz.render(root);
jz.observe(root);
document.getElementById("to").append(paragraph);
await tick();
jz.disconnect();
const incremental = paragraph.outerHTML;
jz.rerender(root);
assert.equal(incremental, paragraph.outerHTML);
assert.ok(paragraph.querySelector("jz-char"));
}
finally { jz.revert(root); jz.disconnect(); dom.window.close(); }
});
for (const [name, marker, options] of [
[ "off", 'data-juzhen-off="spacing"', {} ],
[ "whitelist", 'data-juzhen="jiya"', {} ],
[ "blocks", 'class="disabled"', { blocks: [ { selector: ".disabled", features: [ "jiya" ] } ] } ],
])
{
test("spacing inline feature:" + name + " 保留禁用內容及作者空格", () =>
{
const { dom, document } = setupDom('<p>外ABC<span ' + marker + '>甲 ABC 乙</span>文XYZ</p>');
const root = document.querySelector("p");
const disabled = root.querySelector("span");
const original = root.textContent;
const jz = createJuzhen({ ...ONLY_SPACING, ...options });
try
{
jz.render(root);
assert.equal(disabled.innerHTML, "甲 ABC 乙");
assert.equal(root.textContent, original);
assert.equal(root.querySelectorAll("jz-hws").length, 2, "外部啟用區仍留隙");
jz.revert(root);
assert.equal(root.textContent, original);
}
finally { jz.revert(root); dom.window.close(); }
});
}
for (const marker of [ 'data-juzhen-off="spacing"', 'data-juzhen="jiya"' ])
{
test("spacing feature:禁用塊內最近子白名單可重新啟用 " + marker, () =>
{
const { dom, document } = setupDom('<p ' + marker + '>甲 ABC<span data-juzhen="spacing">乙 DEF</span>丙 GHI</p>');
const root = document.querySelector("p");
const jz = createJuzhen(ONLY_SPACING);
try
{
jz.render(root);
assert.equal(root.textContent, "甲 ABC乙DEF丙 GHI");
assert.equal(root.querySelectorAll("jz-hws").length, 1);
assert.equal(root.querySelector("span jz-hws").textContent, "乙");
}
finally { jz.revert(root); dom.window.close(); }
});
}
for (const side of [ "left", "right" ])
{
test("spacing pill feature:" + side + " 鄰字禁用時不刪空格或造隙", () =>
{
const html = side === "left" ? '<span data-juzhen-off="spacing">甲 </span><code>x</code>'
: '<code>x</code><span data-juzhen-off="spacing"> 甲</span>';
const { dom, document } = setupDom("<p>" + html + "</p>");
const root = document.querySelector("p");
const jz = createJuzhen(ONLY_SPACING);
try { jz.render(root); assert.equal(root.innerHTML, html); }
finally { jz.revert(root); dom.window.close(); }
});
}
for (const separator of [ '<span data-juzhen-off="spacing"></span>', '<span data-juzhen=""></span>' ])
{
test("spacing feature:空禁用元素中斷 run " + separator, () =>
{
const { dom, document } = setupDom("<p>甲" + separator + "ABC</p>");
const root = document.querySelector("p");
const jz = createJuzhen(ONLY_SPACING);
try { jz.render(root); assert.equal(root.querySelector("jz-hws"), null); }
finally { jz.revert(root); dom.window.close(); }
});
}
for (const full of [ false, true ])
{
for (const mutation of [ "clone-gap", "clone-base", "remove-gap", "replace-source-parent" ])
{
test("spacing carrier integrity:" + mutation + " full=" + full, () =>
{
const { dom, document } = setupDom('<p><strong id="base">禰</strong><em id="vs">\u{E0100}</em>A</p>');
const root = document.querySelector("p");
const source = root.querySelector("em");
const jz = createJuzhen(full ? {} : ONLY_SPACING);
try
{
jz.render(root);
const gap = root.querySelector("jz-hws");
const base = document.getElementById("base");
if (mutation === "clone-gap") { gap.replaceWith(gap.cloneNode(true)); }
else if (mutation === "clone-base") { base.replaceWith(base.cloneNode(true)); }
else if (mutation === "remove-gap") { gap.remove(); }
else { source.replaceWith(source.cloneNode(true)); }
const before = root.textContent;
jz.revert(root);
assert.equal(root.textContent, mutation === "replace-source-parent" ? "禰A" : before);
if (mutation === "replace-source-parent") { assert.equal(source.textContent, ""); }
}
finally { jz.revert(root); dom.window.close(); }
});
}
}
for (const order of [ "binding-first", "spacing-first" ])
{
test("spacing multi-owner:" + order + " 回退受控父包裹不丟 VS", () =>
{
const { dom, document } = setupDom("<p></p>");
const root = document.querySelector("p");
for (const value of [ "「", "禰", "\uFE00", "A" ]) { root.appendChild(document.createTextNode(value)); }
const binding = createJuzhen({ spacing: false, orphan: false, longWord: false });
const spacing = createJuzhen(ONLY_SPACING);
try
{
binding.render(root);
spacing.render(root);
if (order === "binding-first") { binding.revert(root); spacing.revert(root); }
else { spacing.revert(root); binding.revert(root); }
assert.equal(root.textContent, "「禰\uFE00A");
assert.equal(root.querySelector("[data-jz]"), null);
}
finally { binding.revert(root); spacing.revert(root); dom.window.close(); }
});
}
for (const side of [ "left", "right" ])
{
test("spacing pill feature:空禁用元素在 " + side + " 阻斷鄰字查詢", () =>
{
const barrier = '<span data-juzhen-off="spacing"></span>';
const html = side === "left" ? "甲" + barrier + "<code>x</code>"
: "<code>x</code>" + barrier + "甲";
const { dom, document } = setupDom("<p>" + html + "</p>");
const root = document.querySelector("p");
const jz = createJuzhen(ONLY_SPACING);
try { jz.render(root); assert.equal(root.innerHTML, html); }
finally { jz.revert(root); dom.window.close(); }
});
}
for (const order of [ [0,1,2], [1,0,2], [0,2,1], [1,2,0], [2,0,1], [2,1,0] ])
{
test("spacing multi-owner:傳遞解包鏈 " + order.join("→") + " 保留來源原子", () =>
{
const { dom, document } = setupDom("<p></p>");
const root = document.querySelector("p");
for (const value of [ "甲", "「", "禰", "\uFE00", "A" ]) { root.appendChild(document.createTextNode(value)); }
const instances = [
createJuzhen({ spacing: false, orphan: false, longWord: false }),
createJuzhen({ spacing: false, jiya: false, jinze: false, longWord: false }),
createJuzhen(ONLY_SPACING),
];
try
{
for (const instance of instances) { instance.render(root); }
for (const index of order) { instances[index].revert(root); }
assert.equal(root.textContent, "甲「禰\uFE00A");
assert.equal(root.querySelector("[data-jz]"), null);
}
finally { for (const instance of instances) { instance.revert(root); } dom.window.close(); }
});
}
for (const order of [ "punctuation-first", "spacing-first" ])
{
test("spacing multi-owner:跨 Text 標點父解包 " + order + " 保留 VS", () =>
{
const { dom, document } = setupDom('<p><strong>,</strong><em>\uFE00</em>A</p>');
const root = document.querySelector("p");
const punctuation = createJuzhen({ spacing: false, jinze: false, orphan: false, longWord: false });
const spacing = createJuzhen({ ...ONLY_SPACING, charClass: { cjk: "," } });
try
{
punctuation.render(root);
spacing.render(root);
if (order === "punctuation-first") { punctuation.revert(root); spacing.revert(root); }
else { spacing.revert(root); punctuation.revert(root); }
assert.equal(root.textContent, ",\uFE00A");
assert.equal(root.querySelector("[data-jz]"), null);
}
finally { punctuation.revert(root); spacing.revert(root); dom.window.close(); }
});
}
for (const mode of [ "all", "spacing-only" ])
{
test("spacing include:行內 opt-in " + mode + " 只處理選中子樹", () =>
{
const html = '<p>外ABC<span class="prose">甲DEF<span class="prose">乙GHI</span>丙</span>文XYZ</p>';
const { dom, document } = setupDom(html);
const root = document.querySelector("p");
const jz = createJuzhen({ ...(mode === "all" ? {} : ONLY_SPACING), scope: { include: ".prose" } });
try
{
jz.render(root);
const gaps = Array.from(root.querySelectorAll("jz-hws"));
assert.deepEqual(gaps.map((gap) => gap.textContent), [ "甲", "F", "乙", "I" ]);
assert.equal(root.firstChild.textContent, "外ABC");
assert.equal(root.lastChild.textContent, "文XYZ");
const rendered = root.innerHTML;
jz.render(root);
assert.equal(root.innerHTML, rendered);
jz.revert(root);
assert.equal(root.outerHTML, html);
}
finally { jz.revert(root); dom.window.close(); }
});
}
for (const marker of [ 'data-juzhen-off="jiya"', 'data-juzhen="spacing"' ])
{
test("jiya feature:空禁用元素中斷標點鄰接 " + marker, () =>
{
const { dom, document } = setupDom('<p lang="zh-Hant">,<span ' + marker + '></span>「</p>');
const root = document.querySelector("p");
const jz = createJuzhen({ spacing: false, orphan: false, jinze: false, longWord: false });
try
{
jz.render(root);
assert.equal(root.querySelectorAll("jz-char").length, 2);
assert.equal(root.querySelectorAll("jz-char.jz-half").length, 0);
}
finally { jz.revert(root); dom.window.close(); }
});
}
test("jiya feature:跨 Text 原子不越空禁用元素搬移 VS", () =>
{
const { dom, document } = setupDom('<p lang="zh-Hant"><strong>,</strong><span data-juzhen-off="jiya"></span><em>\uFE00</em>「</p>');
const root = document.querySelector("p");
const jz = createJuzhen({ spacing: false, orphan: false, jinze: false, longWord: false });
try
{
jz.render(root);
assert.equal(root.querySelector("jz-char").textContent, ",");
assert.equal(root.querySelector("em").textContent, "\uFE00");
assert.equal(root.querySelector("jz-char.jz-half"), null);
}
finally { jz.revert(root); dom.window.close(); }
});