import assert from "node:assert/strict"; import { test } from "node:test"; import { setupDom } from "./helpers.mjs"; import { createJuzhen } from "../dist/juzhen.mjs"; const ONLY_JIYA = { spacing: false, jinze: false, orphan: false, longWord: false }; for (const selector of [ "\uFE00", "\u{E0100}" ]) { for (const full of [ false, true ]) { test("jiya atom:跨透明節點 VS " + selector.codePointAt(0).toString(16) + " full=" + full, () => { const { dom, document } = setupDom('

甲,' + selector + '「乙丙」

'); const root = document.querySelector("p"); const original = root.innerHTML; const text = root.textContent; const base = document.getElementById("base"); const vs = document.getElementById("vs"); let clicks = 0; vs.addEventListener("click", () => { clicks += 1; }); const jz = createJuzhen(full ? {} : ONLY_JIYA); try { jz.render(root); const comma = root.querySelector("jz-char.bd-pause"); assert.equal(comma.textContent, "," + selector); assert.equal(comma.querySelector("jz-inner").textContent, "," + selector); assert.equal(comma.classList.contains("jz-half"), true); assert.equal(root.querySelector("jz-char.bd-open").classList.contains("jz-half"), true); assert.equal(document.getElementById("base"), base); assert.equal(document.getElementById("vs"), vs); vs.dispatchEvent(new dom.window.Event("click")); assert.equal(clicks, 1); assert.equal(root.textContent, text); const rendered = root.innerHTML; jz.render(root); assert.equal(root.innerHTML, rendered, "重複 render 不增殖"); jz.revert(root); assert.equal(root.innerHTML, original); } finally { jz.revert(root); jz.disconnect(); dom.window.close(); } }); } } test("jiya atom:astral base 與 VS 的四個 surrogate 半字合成一個原子", () => { const { dom, document } = setupDom('

「甲

'); const root = document.querySelector("p"); [ "\uD83D", "\uDE00", "\uDB40", "\uDD00" ].forEach((text, i) => { root.children[i].textContent = text; }); const original = root.innerHTML; const jz = createJuzhen({ ...ONLY_JIYA, charClass: { biaodian: { pause: "😀" } } }); try { jz.render(root); const chars = root.querySelectorAll("jz-char"); assert.equal(chars.length, 2); assert.equal(chars[0].textContent, "😀\u{E0100}"); assert.equal(chars[0].classList.contains("jz-half"), true); jz.revert(root); assert.equal(root.innerHTML, original); } finally { jz.revert(root); jz.disconnect(); dom.window.close(); } }); test("jiya 分類:astral 字集不包含原文中孤立的 surrogate 半字", () => { const { dom, document } = setupDom('

'); const root = document.querySelector("p"); root.textContent = "\uD83DA\uDE00"; const original = root.textContent; const jz = createJuzhen({ ...ONLY_JIYA, charClass: { biaodian: { pause: "😀" } } }); try { jz.render(root); assert.equal(root.querySelector("jz-char"), null); assert.equal(root.textContent, original); } finally { jz.revert(root); jz.disconnect(); dom.window.close(); } }); for (const excluded of [ false, true ]) { test("jiya 邊界:空 Text excluded=" + excluded, () => { const { dom, document } = setupDom('

,「

'); const root = document.querySelector("p"); const empty = document.createTextNode(""); root.querySelector("strong").appendChild(empty); const jz = createJuzhen({ ...ONLY_JIYA, isSkipped: (node) => excluded && node === empty }); try { jz.render(root); assert.equal(root.querySelectorAll("jz-char.jz-half").length, excluded ? 0 : 2); } finally { jz.revert(root); jz.disconnect(); dom.window.close(); } }); } for (const side of [ "base", "vs" ]) { test("jiya 跨原子局部回退:" + side + " 端保持原文及另一端位置", () => { const { dom, document } = setupDom('

,\uFE00「

'); const root = document.querySelector("p"); const jz = createJuzhen(ONLY_JIYA); try { jz.render(root); jz.revert(document.getElementById(side)); assert.equal(document.getElementById("base").textContent, ","); assert.equal(document.getElementById("vs").textContent, "\uFE00"); assert.equal(root.textContent, ",\uFE00「"); jz.revert(root); assert.equal(root.innerHTML, ',\uFE00「'); } finally { jz.revert(root); jz.disconnect(); dom.window.close(); } }); } for (const mutation of [ "replace-source", "move-carrier", "move-author-text", "foreign-revert" ]) { test("jiya 原子回退:" + mutation + " 保留作者外部修改/實例所有權", () => { const { dom, document } = setupDom('

,\uFE00「

'); const root = document.querySelector("p"); const jz = createJuzhen(ONLY_JIYA); const other = createJuzhen(ONLY_JIYA); const aside = document.querySelector("aside"); try { jz.render(root); const comma = root.querySelector("jz-char.bd-pause"); const selector = comma.querySelector("jz-inner").lastChild; if (mutation === "replace-source") { document.getElementById("vs").textContent = "作者新文"; } else if (mutation === "move-carrier") { aside.appendChild(comma); } else if (mutation === "move-author-text") { aside.appendChild(selector); } else { const html = root.innerHTML; other.revert(root); assert.equal(root.innerHTML, html); } jz.revert(root); assert.equal(document.getElementById("base").textContent, ","); if (mutation === "replace-source") { assert.equal(document.getElementById("vs").textContent, "作者新文"); assert.equal(root.textContent, ",作者新文「"); } else if (mutation === "move-author-text") { assert.equal(aside.textContent, "\uFE00"); } else { assert.equal(document.getElementById("vs").textContent, "\uFE00"); assert.equal(root.querySelector("[data-jz]"), null); assert.equal(aside.querySelector("[data-jz]"), null); } } finally { jz.revert(document.body); jz.disconnect(); other.disconnect(); dom.window.close(); } }); } for (const mutation of [ "clone-outer", "clone-inner", "clone-base", "remove-outer" ]) { test("jiya 原子失效:" + mutation + " 不重複或復活已刪內容", () => { const { dom, document } = setupDom('

,\uFE00「

'); const root = document.querySelector("p"); const jz = createJuzhen(ONLY_JIYA); try { jz.render(root); const outer = root.querySelector("jz-char.bd-pause"); const inner = outer.querySelector("jz-inner"); const base = document.getElementById("base"); if (mutation === "clone-outer") { outer.replaceWith(outer.cloneNode(true)); } else if (mutation === "clone-inner") { inner.replaceWith(inner.cloneNode(true)); } else if (mutation === "clone-base") { base.replaceWith(base.cloneNode(true)); } else { outer.remove(); } const before = root.textContent; jz.revert(root); assert.equal(root.textContent, before, "回退不能從失效的私有來源補回舊內容"); if (mutation !== "clone-inner") { const holder = document.createElement("aside"); holder.appendChild(outer); jz.revert(holder); assert.equal(holder.firstChild, outer, "失效舊產物撤銷所有權,重插入也不重新取得"); } } finally { jz.revert(root); jz.disconnect(); dom.window.close(); } }); } test("jiya 原子失效:來源父被替換不把 VS 放回已脫離的作者舊父", () => { const { dom, document } = setupDom('

,\uFE00「

'); const root = document.querySelector("p"); const source = document.getElementById("vs"); const jz = createJuzhen(ONLY_JIYA); try { jz.render(root); source.replaceWith(source.cloneNode(true)); jz.revert(root); assert.equal(source.textContent, ""); assert.equal(root.textContent, ",「"); } finally { jz.revert(root); jz.disconnect(); dom.window.close(); } }); test("jiya 原子生命週期:完整 render 根脫離文檔仍精確回退", () => { const { dom, document } = setupDom('

,\uFE00「

'); const root = document.querySelector("p"); const original = root.innerHTML; const jz = createJuzhen(ONLY_JIYA); try { jz.render(root); root.remove(); jz.revert(root); assert.equal(root.innerHTML, original); } finally { jz.disconnect(); dom.window.close(); } });