fix(web): 修復排版邊界與增量生命週期

This commit is contained in:
2026-10-02 19:08:02 +08:00
parent 1c2fb0ed59
commit b1a16c4969
39 changed files with 4763 additions and 673 deletions
+92
View File
@@ -0,0 +1,92 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { setupDom } from "./helpers.mjs";
import { createJuzhen } from "../dist/juzhen.mjs";
function layoutHarness(document)
{
const originalResizeObserver = globalThis.ResizeObserver;
const originalRects = window.Element.prototype.getClientRects;
const observers = [];
let measurements = 0;
let resolveFonts;
const ready = new Promise((resolve) => { resolveFonts = resolve; });
Object.defineProperty(document, "fonts", { configurable: true, value: { ready } });
globalThis.ResizeObserver = class
{
constructor(callback) { this.callback = callback; this.disconnected = false; observers.push(this); }
observe() {}
disconnect() { this.disconnected = true; }
fire() { this.callback([], this); }
};
window.Element.prototype.getClientRects = function ()
{
measurements += 1;
return [ { top: 0, bottom: 16, left: 0, right: 16, width: 16, height: 16 } ];
};
return {
observers,
resolveFonts,
count: () => measurements,
restore()
{
window.Element.prototype.getClientRects = originalRects;
if (originalResizeObserver === undefined) { delete globalThis.ResizeObserver; }
else { globalThis.ResizeObserver = originalResizeObserver; }
},
};
}
for (const signal of [ "resize", "fonts" ])
{
test("layout lifecycle:重排後舊 " + signal + " 回調不得量測新產物", async () =>
{
const { dom, document } = setupDom('<p lang="zh-Hant">「甲乙</p>');
const root = document.querySelector("p");
const layout = layoutHarness(document);
const jz = createJuzhen({ spacing: false, orphan: false, jinze: false, longWord: false });
try
{
jz.render(root);
jz.rerender(root);
assert.equal(layout.observers.length, 2);
assert.equal(layout.observers[0].disconnected, true);
const before = layout.count();
if (signal === "resize")
{
layout.observers[0].fire();
assert.equal(layout.count(), before, "已卸載回調不碰目前產物");
layout.observers[1].fire();
assert.equal(layout.count(), before + 1, "目前回調正常執行");
}
else
{
layout.resolveFonts();
await Promise.resolve();
assert.equal(layout.count(), before + 1, "只執行目前安裝的字型回調");
}
}
finally { jz.revert(root); jz.disconnect(); layout.restore(); dom.window.close(); }
});
}
test("layout lifecycle:回退後字型 Promise 不再查詢根子樹", async () =>
{
const { dom, document } = setupDom('<p lang="zh-Hant">「甲乙</p>');
const root = document.querySelector("p");
const layout = layoutHarness(document);
const jz = createJuzhen({ spacing: false, orphan: false, jinze: false, longWord: false });
const nativeQuery = root.querySelectorAll;
let queries = 0;
try
{
jz.render(root);
jz.revert(root);
root.querySelectorAll = function (...args) { queries += 1; return nativeQuery.apply(this, args); };
layout.resolveFonts();
await Promise.resolve();
assert.equal(queries, 0, "卸載後不再進入 layout 工作");
assert.equal(root.innerHTML, "「甲乙");
}
finally { root.querySelectorAll = nativeQuery; jz.disconnect(); layout.restore(); dom.window.close(); }
});