import assert from "node:assert/strict";
import { test } from "node:test";
import { createJuzhen } from "../dist/juzhen.mjs";
import { setupDom } from "./helpers.mjs";
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
function installCountingMutationObserver(
maxCallbacks = Number.POSITIVE_INFINITY,
{ deferDelivery = false, wrapRecords = (records) => records } = {},
)
{
const NativeMutationObserver = globalThis.MutationObserver;
const instances = [];
class CountingMutationObserver
{
constructor(callback)
{
this.callback = callback;
this.observeCount = 0;
this.disconnectCount = 0;
this.callbackCount = 0;
this.pendingRecords = [];
this.suppressed = false;
this.inner = new NativeMutationObserver((records) =>
{
if (deferDelivery) { this.pendingRecords.push(...records); }
else { this.deliver(records); }
});
instances.push(this);
}
deliver(records)
{
this.callbackCount += 1;
if (this.callbackCount > maxCallbacks)
{
this.suppressed = true;
this.inner.disconnect();
return;
}
this.callback(wrapRecords(records), this);
}
observe(target, options)
{
this.observeCount += 1;
if (!this.suppressed) { this.inner.observe(target, options); }
}
disconnect()
{
this.disconnectCount += 1;
this.inner.disconnect();
}
takeRecords()
{
return this.inner.takeRecords();
}
}
globalThis.MutationObserver = CountingMutationObserver;
return {
instances,
restore()
{
globalThis.MutationObserver = NativeMutationObserver;
},
};
}
function installExpectationShiftProbe()
{
const nativeShift = Array.prototype.shift;
let calls = 0;
let work = 0;
Array.prototype.shift = function ()
{
const head = this[0];
if (head && typeof head === "object" && "target" in head
&& "oldValue" in head && "newValue" in head)
{
calls += 1;
work += this.length;
}
return nativeShift.call(this);
};
return {
result: () => ({ calls, work }),
restore()
{
Array.prototype.shift = nativeShift;
},
};
}
function installExpectationSpliceProbe()
{
const nativeSplice = Array.prototype.splice;
let calls = 0;
let removed = 0;
Array.prototype.splice = function (...args)
{
const [ start, deleteCount ] = args;
const head = this[0];
if (start === 0 && head && typeof head === "object" && "target" in head
&& "oldValue" in head && "newValue" in head)
{
calls += 1;
removed += deleteCount;
}
return nativeSplice.apply(this, args);
};
return {
result: () => ({ calls, removed }),
restore()
{
Array.prototype.splice = nativeSplice;
},
};
}
function installLayoutHarness(document, withFonts = false)
{
const NativeResizeObserver = globalThis.ResizeObserver;
const originalGetClientRects = window.Element.prototype.getClientRects;
const instances = [];
let rectsEnabled = true;
class CapturingResizeObserver
{
constructor(callback)
{
this.callback = callback;
this.target = null;
this.disconnected = false;
instances.push(this);
}
observe(target)
{
this.target = target;
}
disconnect()
{
this.disconnected = true;
}
fire()
{
this.callback([ { target: this.target } ], this);
}
}
globalThis.ResizeObserver = CapturingResizeObserver;
window.Element.prototype.getClientRects = function ()
{
if (!rectsEnabled) { return []; }
return [ {
bottom: 16, height: 16, left: 0, right: 16,
top: 0, width: 16, x: 0, y: 0,
} ];
};
if (withFonts)
{
Object.defineProperty(document, "fonts", {
configurable: true,
value: { ready: Promise.resolve() },
});
}
return {
instances,
setRectsEnabled(value)
{
rectsEnabled = value;
},
restore()
{
window.Element.prototype.getClientRects = originalGetClientRects;
if (NativeResizeObserver === undefined) { delete globalThis.ResizeObserver; }
else { globalThis.ResizeObserver = NativeResizeObserver; }
},
};
}
test("observer recovery: synchronous render failure restores later observation", async () =>
{
const { document } = setupDom('
');
const root = document.getElementById("root");
const paragraph = root.querySelector("p");
const expected = new Error("isSkipped render failure");
let throwNext = false;
const jz = createJuzhen({
isSkipped()
{
if (!throwNext) { return false; }
throwNext = false;
throw expected;
},
});
jz.render(root);
jz.observe(root);
jz.invalidate(root);
throwNext = true;
assert.throws(() => jz.render(root), (error) => error === expected);
paragraph.textContent = "錯誤後,仍可繼續觀察。";
await tick();
await tick();
jz.disconnect();
assert.ok(paragraph.querySelector("jz-char"), "之後的 DOM 變化仍自動處理");
assert.equal(paragraph.textContent, "錯誤後,仍可繼續觀察。");
});
test("observer recovery: mutation callback failure restores a later mutation", async () =>
{
const { document } = setupDom('');
const root = document.getElementById("root");
const paragraph = root.querySelector("p");
const expected = new Error("isSkipped mutation failure");
const reported = [];
let throwNext = false;
const onError = (event) =>
{
reported.push(event.error);
event.preventDefault();
};
window.addEventListener("error", onError);
const jz = createJuzhen({
isSkipped()
{
if (!throwNext) { return false; }
throwNext = false;
throw expected;
},
});
try
{
jz.render(root);
jz.observe(root);
jz.invalidate(root);
throwNext = true;
paragraph.textContent = "第一次,觸發錯誤。";
await tick();
assert.ok(reported.includes(expected), "MutationObserver 回調錯誤仍照常對外呈現");
paragraph.textContent = "第二次,恢復處理。";
await tick();
await tick();
assert.ok(paragraph.querySelector("jz-char"), "後續變化仍被觀察");
assert.equal(paragraph.textContent, "第二次,恢復處理。");
}
finally
{
jz.disconnect();
window.removeEventListener("error", onError);
}
});
test("observer depth: nested render does not resume or observe partial outer changes", async () =>
{
const { document } = setupDom('');
const counting = installCountingMutationObserver();
const root = document.getElementById("root");
const paragraph = root.querySelector("p");
let reenter = false;
let jz;
try
{
jz = createJuzhen({
isSkipped()
{
if (reenter)
{
reenter = false;
jz.render(paragraph);
}
return false;
},
});
jz.render(root);
jz.observe(root);
assert.equal(counting.instances.length, 1);
jz.invalidate(root);
reenter = true;
jz.rerender(root);
const observer = counting.instances[0];
assert.equal(observer.disconnectCount, 1, "只有最外層 0→1 暫停會 disconnect");
assert.equal(observer.observeCount, 2, "只有最外層 1→0 恢復會 observe");
await tick();
await tick();
assert.equal(observer.callbackCount, 0, "嵌套處理不會觀察半成品或自觸發");
assert.ok(paragraph.querySelector("jz-char"));
}
finally
{
if (jz) { jz.disconnect(); }
counting.restore();
}
});
test("observer depth: observe replacement inside a paused render is deferred coherently", async () =>
{
const { document } = setupDom('');
const counting = installCountingMutationObserver();
const root = document.getElementById("root");
const paragraph = root.querySelector("p");
let replaceObserver = false;
let observeCallsDuringPause = -1;
let jz;
try
{
jz = createJuzhen({
isSkipped()
{
if (replaceObserver)
{
replaceObserver = false;
jz.disconnect();
jz.observe(root);
observeCallsDuringPause = counting.instances.reduce(
(sum, observer) => sum + observer.observeCount, 0,
);
}
return false;
},
});
jz.render(root);
jz.observe(root);
jz.invalidate(root);
replaceObserver = true;
jz.rerender(root);
assert.equal(counting.instances.length, 2, "嵌套 observe 建立新觀察者");
assert.equal(observeCallsDuringPause, 1, "暫停期內新觀察者尚未 observe");
assert.equal(
counting.instances.reduce((sum, observer) => sum + observer.observeCount, 0),
2,
"最外層結束後新觀察者恰恢復一次",
);
paragraph.textContent = "重建後,仍能處理。";
await tick();
await tick();
assert.ok(paragraph.querySelector("jz-char"));
assert.equal(counting.instances[1].callbackCount, 1, "新觀察者接手後續變化");
}
finally
{
if (jz) { jz.disconnect(); }
counting.restore();
}
});
test("observer provenance: asynchronous internal layout classes do not self-trigger", async () =>
{
const { document } = setupDom('');
const counting = installCountingMutationObserver(8);
const layout = installLayoutHarness(document, true);
const root = document.getElementById("root");
const paragraph = root.querySelector("p");
let jz;
try
{
jz = createJuzhen({ spacing: false, orphan: false });
jz.render(root);
jz.observe(root);
await tick();
await tick();
const observer = counting.instances[0];
assert.equal(layout.instances.length, 1, "未因內部 class 記錄重建 layout observer");
assert.equal(observer.callbackCount, 1, "同批 remove/add provenance 被精確消費");
assert.equal(observer.suppressed, false, "防掛上限未被觸發");
assert.ok(paragraph.querySelector("jz-char.jz-half-le"), "異步行端結果保持");
}
finally
{
if (jz) { jz.disconnect(); }
layout.restore();
counting.restore();
}
});
test("observer provenance: author whitelist attributes still trigger rerender", async () =>
{
const { document } = setupDom('');
const counting = installCountingMutationObserver();
const root = document.getElementById("root");
const paragraph = root.querySelector("p");
const jz = createJuzhen();
try
{
jz.render(root);
jz.observe(root);
paragraph.classList.add("consumer-class");
await tick();
assert.equal(counting.instances[0].callbackCount, 1, "作者 class 觸發 observer");
assert.ok(paragraph.querySelector("jz-char"));
paragraph.setAttribute("data-jz-skip", "");
await tick();
assert.equal(counting.instances[0].callbackCount, 2, "作者非 class 白名單屬性亦觸發");
assert.equal(paragraph.querySelectorAll("[data-jz]").length, 0, "skip 重分類生效");
}
finally
{
jz.disconnect();
counting.restore();
}
});
test("observer provenance: external writes on owned products are not consumed", async () =>
{
const { document } = setupDom('');
const counting = installCountingMutationObserver();
const root = document.getElementById("root");
const jz = createJuzhen({ spacing: false, longWord: false, orphan: false });
try
{
jz.render(root);
jz.observe(root);
const atom = root.querySelector("jz-char");
atom.classList.add("consumer-class");
await tick();
assert.equal(counting.instances[0].callbackCount, 1, "owned 元素外部 class 觸發");
assert.equal(atom.isConnected, false, "外部 class 變更進入 rerender");
const nextAtom = root.querySelector("jz-char");
nextAtom.setAttribute("lang", "en");
await tick();
assert.equal(counting.instances[0].callbackCount, 2, "owned 元素外部 lang 觸發");
assert.equal(nextAtom.isConnected, false);
const splitoff = root.querySelector('[data-jz-kind="splitoff"]');
splitoff.classList.add("consumer-splitoff");
await tick();
assert.equal(counting.instances[0].callbackCount, 3, "owned splitoff 外部 class 觸發");
assert.equal(splitoff.isConnected, false);
}
finally
{
jz.disconnect();
counting.restore();
}
});
test("observer provenance: mixed internal and external class records stay distinguishable", async () =>
{
const { document } = setupDom('');
const counting = installCountingMutationObserver();
const layout = installLayoutHarness(document);
const root = document.getElementById("root");
const jz = createJuzhen({ spacing: false, orphan: false });
try
{
jz.render(root);
jz.observe(root);
const atom = root.querySelector("jz-char");
atom.classList.add("consumer-before-internal");
layout.instances[0].fire();
await tick();
assert.equal(counting.instances[0].callbackCount, 1, "同批記錄只派發一次");
assert.equal(atom.isConnected, false, "外部 mismatch 後之內部 transition 仍精確消費");
}
finally
{
jz.disconnect();
layout.restore();
counting.restore();
}
});
test(
"observer provenance: threshold compaction preserves a 1024-record tail across callbacks",
{ timeout: 20000 },
async () =>
{
const size = 2048;
const html = Array.from({ length: size }, (_, index) => `(${index}
`).join("");
const { document } = setupDom(`${html}
`);
const counting = installCountingMutationObserver(4, { deferDelivery: true });
const layout = installLayoutHarness(document);
const spliceProbe = installExpectationSpliceProbe();
const root = document.getElementById("root");
const jz = createJuzhen({ spacing: false, longWord: false, orphan: false });
try
{
jz.render(root);
jz.observe(root);
layout.setRectsEnabled(false);
layout.instances[0].fire();
await tick();
const observer = counting.instances[0];
const records = observer.pendingRecords.splice(0);
assert.equal(records.length, size, "每個 target 各排入一條 internal remove");
observer.deliver(records.slice(0, 1024));
assert.deepEqual(spliceProbe.result(), { calls: 1, removed: 1024 });
assert.equal(layout.instances.length, 1, "首批觸發 threshold compaction 而不 rerender");
observer.deliver(records.slice(1024));
assert.equal(observer.callbackCount, 2, "第二批由壓縮後 deque 繼續消費");
assert.equal(layout.instances.length, 1, "尾批全消費後 head/reset 正確");
assert.equal(root.querySelectorAll("jz-char.jz-half-le").length, 0);
}
finally
{
jz.disconnect();
spliceProbe.restore();
layout.restore();
counting.restore();
}
},
);
test(
"observer provenance: callback failure compacts the consumed prefix and preserves its tail",
{ timeout: 15000 },
async () =>
{
const size = 1025;
const html = Array.from({ length: size }, (_, index) => `(${index}
`).join("");
const { document } = setupDom(`${html}
`);
const counting = installCountingMutationObserver(4, { deferDelivery: true });
const layout = installLayoutHarness(document);
const spliceProbe = installExpectationSpliceProbe();
const root = document.getElementById("root");
const jz = createJuzhen({ spacing: false, longWord: false, orphan: false });
const expected = new Error("mid-callback classification failure");
const throwingRecord = {
type: "characterData",
get target()
{
throw expected;
},
};
try
{
jz.render(root);
jz.observe(root);
layout.setRectsEnabled(false);
layout.instances[0].fire();
await tick();
const observer = counting.instances[0];
const records = observer.pendingRecords.splice(0);
assert.equal(records.length, size);
assert.throws(
() => observer.deliver([ ...records.slice(0, 1024), throwingRecord ]),
(error) => error === expected,
);
assert.deepEqual(
spliceProbe.result(),
{ calls: 1, removed: 1024 },
"拋錯 callback 之 finally 立即壓縮已消費前綴",
);
assert.equal(layout.instances.length, 1, "finally 壓縮已消費前綴而不 rerender");
observer.deliver([ records[1024] ]);
assert.equal(layout.instances.length, 1, "下一 callback 精確消費保留之 tail");
const atom = root.querySelector("jz-char");
atom.classList.add("consumer-after-callback-error");
await tick();
assert.equal(observer.pendingRecords.length, 1, "外部 class 排入獨立 record");
observer.deliver(observer.pendingRecords.splice(0));
assert.equal(observer.callbackCount, 3, "錯誤、tail、外部 record 各交付一次");
assert.equal(atom.isConnected, false, "外部 record 未被殘留 expectation 誤吞");
}
finally
{
jz.disconnect();
spliceProbe.restore();
layout.restore();
counting.restore();
}
},
);
test("observer provenance: foreign-instance product changes still trigger", async () =>
{
const { document } = setupDom('');
const foreign = createJuzhen();
const root = document.getElementById("root");
foreign.render(root);
const atom = root.querySelector("jz-char");
const counting = installCountingMutationObserver();
const local = createJuzhen();
try
{
local.observe(root);
atom.classList.add("foreign-change");
await tick();
assert.equal(counting.instances[0].callbackCount, 1, "外來實例無本地 provenance");
}
finally
{
local.disconnect();
counting.restore();
}
});
for (const mutationKind of [ "childList", "characterData" ])
{
test(`observer provenance: owned ${mutationKind} changes still trigger`, async () =>
{
const { document } = setupDom('');
const counting = installCountingMutationObserver();
const root = document.getElementById("root");
const jz = createJuzhen();
try
{
jz.render(root);
jz.observe(root);
const atom = root.querySelector("jz-char");
if (mutationKind === "childList") { atom.append("外"); }
else
{
const text = document.createTreeWalker(atom, NodeFilter.SHOW_TEXT).nextNode();
text.data += "外";
}
await tick();
assert.equal(counting.instances[0].callbackCount, 1);
assert.equal(atom.isConnected, false, `${mutationKind} 進入 rerender`);
}
finally
{
jz.disconnect();
counting.restore();
}
});
}
for (const lifecycle of [ "pause", "replace", "disconnect" ])
{
test(`observer provenance: ${lifecycle} clears stale expectations`, async () =>
{
const { document } = setupDom('');
const counting = installCountingMutationObserver();
const layout = installLayoutHarness(document);
const root = document.getElementById("root");
const jz = createJuzhen({ spacing: false, orphan: false });
try
{
jz.render(root);
jz.observe(root);
layout.instances[0].fire();
if (lifecycle === "pause") { jz.rerender(root); }
else if (lifecycle === "replace") { jz.observe(root); }
else { jz.disconnect(); jz.observe(root); }
const activeObserver = counting.instances[counting.instances.length - 1];
root.querySelector("jz-char").classList.add("consumer-after-lifecycle");
await tick();
assert.equal(activeObserver.callbackCount, 1, "外部 class 未被陳舊 provenance 吞掉");
}
finally
{
jz.disconnect();
layout.restore();
counting.restore();
}
});
}
test("observer provenance: transition inference and queue consumption scale linearly", async (context) =>
{
const reports = [];
for (const size of [ 500, 1000, 2000 ])
{
const html = Array.from({ length: size }, (_, index) => `(${index}
`).join("");
const { document } = setupDom(`${html}
`);
let recordAccesses = 0;
let recordCount = 0;
const counting = installCountingMutationObserver(Number.POSITIVE_INFINITY, {
wrapRecords(records)
{
recordCount += records.length;
return records.map((record) => new Proxy({}, {
get(_target, property)
{
recordAccesses += 1;
return record[property];
},
}));
},
});
const layout = installLayoutHarness(document);
const shiftProbe = installExpectationShiftProbe();
const root = document.getElementById("root");
const jz = createJuzhen({ spacing: false, longWord: false, orphan: false });
try
{
jz.render(root);
jz.observe(root);
assert.equal(root.querySelectorAll("jz-char.jz-half-le").length, size);
layout.instances[0].fire();
await tick();
const shift = shiftProbe.result();
assert.equal(recordCount, size * 2, "每 target 恰有 remove/add 兩條 transition");
const report = { size, recordCount, recordAccesses, queueShiftWork: shift.work };
context.diagnostic("linear probe " + JSON.stringify(report));
assert.ok(
recordAccesses <= recordCount * 12 && shift.calls === 0 && shift.work === 0,
"transition 推斷與 queue 消費保持線性:" + JSON.stringify(report),
);
assert.equal(layout.instances.length, 1, "規模回放沒有自觸發 rerender");
reports.push(report);
}
finally
{
jz.disconnect();
shiftProbe.restore();
layout.restore();
counting.restore();
}
}
context.diagnostic("linear probes complete " + JSON.stringify(reports));
});