buninu-browser 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -6
- package/index.js +10 -6
- package/lib/audio/pcm.js +11 -0
- package/lib/audio/pulse-client.js +7 -2
- package/lib/cdp-server.js +94 -46
- package/lib/happy-dom/document-base.js +29 -1
- package/lib/happy-dom/mutation-observer.js +157 -0
- package/lib/happy-dom/parser.js +2 -0
- package/lib/headless-shell.js +49 -9
- package/lib/layout/repaint.js +89 -0
- package/lib/media/mp4-keyframes.js +144 -0
- package/lib/media/video-keyframe.js +29 -1
- package/lib/media/webm-snapshots.js +282 -0
- package/lib/network/cookie-jar.js +18 -2
- package/lib/paint/display-list.js +4 -3
- package/lib/paint/frame.js +12 -5
- package/lib/render-tree/index.js +3 -0
- package/lib/renderer/animation-frames.js +76 -0
- package/lib/renderer/bindings.js +2 -0
- package/lib/renderer/compositor-channel.js +95 -10
- package/lib/renderer/compositor.js +54 -18
- package/lib/renderer/document-cookies.js +87 -0
- package/lib/renderer/font-loading.js +60 -16
- package/lib/renderer/host.js +48 -19
- package/lib/renderer/idle-callbacks.js +109 -0
- package/lib/renderer/main.js +5 -1
- package/lib/renderer/media-source.js +4 -0
- package/lib/renderer/page-realm.js +159 -49
- package/lib/renderer/page-renderer.js +567 -197
- package/lib/renderer/resource-elements.js +88 -0
- package/lib/renderer/script-rewrite.js +18 -1
- package/lib/renderer/seccomp.js +6 -4
- package/lib/renderer/timers.js +71 -0
- package/lib/renderer/video-channel.js +94 -0
- package/lib/renderer/video-mask.js +115 -0
- package/lib/renderer/video-presentation.js +304 -39
- package/lib/renderer/video-worker.js +46 -0
- package/lib/renderer/workers.js +7 -17
- package/lib/style/computed-style.js +257 -61
- package/package.json +1 -1
- package/test/animation-frames.test.js +67 -0
- package/test/canvas-style-snapshot.test.js +18 -0
- package/test/cdp-stream.test.js +96 -3
- package/test/cdp-viewers.test.js +43 -0
- package/test/compositor-channel-ownership.test.js +194 -0
- package/test/compositor.test.js +27 -1
- package/test/computed-style-binding.test.js +49 -0
- package/test/computed-style-connectivity.test.js +13 -0
- package/test/computed-style.test.js +86 -0
- package/test/cookie-sync.test.js +82 -0
- package/test/document-base-cache.test.js +103 -0
- package/test/document-cookies.test.js +117 -0
- package/test/duplicate-style-keys.test.js +11 -0
- package/test/fixtures/animation-frames.js +56 -0
- package/test/fixtures/canvas-style-snapshot.js +81 -0
- package/test/fixtures/computed-style-connectivity.js +101 -0
- package/test/fixtures/computed-style-lazy.js +135 -0
- package/test/fixtures/duplicate-style-keys.js +97 -0
- package/test/fixtures/fake-renderer.js +12 -0
- package/test/fixtures/font-definition-invalidation.js +97 -0
- package/test/fixtures/font-metrics-invalidation.js +87 -0
- package/test/fixtures/full-style-identity.js +36 -0
- package/test/fixtures/hidden-style-engine.js +260 -0
- package/test/fixtures/hidden-style-reentry.js +232 -0
- package/test/fixtures/hidden-style-renderer.js +203 -0
- package/test/fixtures/layout-repaint.js +69 -0
- package/test/fixtures/media-presentation-continuation.js +141 -0
- package/test/fixtures/media-presentation-refill.js +113 -0
- package/test/fixtures/media-probe.js +148 -0
- package/test/fixtures/media-refill-cancel.js +79 -0
- package/test/fixtures/media-repaint.js +48 -0
- package/test/fixtures/media-resource-reselection.js +94 -0
- package/test/fixtures/media-source-attachment.js +136 -0
- package/test/fixtures/media-source-no-decoder.js +17 -0
- package/test/fixtures/media-track-refill.js +132 -0
- package/test/fixtures/media-webm-seek.js +51 -0
- package/test/fixtures/mutation-observer.js +201 -0
- package/test/fixtures/observer-invalidation.js +105 -0
- package/test/fixtures/prepaint-scheduling.js +73 -0
- package/test/fixtures/resource-discovery.js +32 -0
- package/test/fixtures/spare-renderer-scheduling.js +124 -0
- package/test/fixtures/style-propagation.js +26 -0
- package/test/fixtures/svg-node-reuse.js +83 -0
- package/test/fixtures/video-black-replay.js +140 -0
- package/test/fixtures/video-controls-invalidation.js +71 -0
- package/test/fixtures/viewport-bounds.js +148 -0
- package/test/font-loading.test.js +77 -0
- package/test/font-metrics-invalidation.test.js +60 -0
- package/test/full-style-identity.test.js +191 -0
- package/test/hidden-style-lazy.test.js +31 -0
- package/test/hidden-style-reentry.test.js +20 -0
- package/test/idle-callback.test.js +89 -0
- package/test/idle-scheduler.test.js +148 -0
- package/test/index.test.js +1 -0
- package/test/inline-style-cascade.test.js +63 -0
- package/test/layout-repaint-renderer.test.js +9 -0
- package/test/layout-repaint.test.js +222 -0
- package/test/media-presentation-continuation.test.js +14 -0
- package/test/media-presentation-refill.test.js +8 -0
- package/test/media-probe.test.js +16 -0
- package/test/media-repaint.test.js +8 -0
- package/test/media-source-attachment.test.js +34 -0
- package/test/media-track-refill.test.js +26 -0
- package/test/media-webm-seek.test.js +11 -0
- package/test/mp4-keyframes.test.js +36 -0
- package/test/mutation-observer.test.js +91 -0
- package/test/observer-invalidation.test.js +12 -0
- package/test/page-scripts.test.js +35 -0
- package/test/page-timers.test.js +67 -0
- package/test/paint-frame.test.js +18 -1
- package/test/pcm.test.js +63 -0
- package/test/prepaint-scheduling.test.js +13 -0
- package/test/pulse-client.test.js +165 -0
- package/test/raw-capture.test.js +34 -5
- package/test/renderer-host.test.js +51 -0
- package/test/resource-discovery.test.js +12 -0
- package/test/resource-elements.test.js +234 -0
- package/test/root-self-style.test.js +104 -0
- package/test/script-rewrite.test.js +35 -0
- package/test/seccomp.test.js +45 -0
- package/test/spare-renderer-scheduling.test.js +8 -0
- package/test/style-propagation.test.js +225 -0
- package/test/style-sheets.test.js +19 -0
- package/test/svg.test.js +45 -0
- package/test/timers.test.js +58 -0
- package/test/video-black-replay.test.js +34 -0
- package/test/video-capture.test.js +170 -0
- package/test/video-channel.test.js +253 -0
- package/test/video-continuation.test.js +57 -0
- package/test/video-controls-invalidation.test.js +12 -0
- package/test/video-keyframe.test.js +104 -4
- package/test/video-mask.test.js +94 -0
- package/test/video-presentation.test.js +239 -1
- package/test/video-timing-presentation.test.js +115 -0
- package/test/viewport-bounds.test.js +13 -0
- package/test/webm-snapshots.test.js +372 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
const root = new URL("../../", import.meta.url).pathname.replace(/\/$/, "");
|
|
4
|
+
if (process.argv.includes("--ses")) {
|
|
5
|
+
await import(pathToFileURL(root + "/vendor/ses/dist/ses.mjs"));
|
|
6
|
+
lockdown({
|
|
7
|
+
errorTaming: "safe",
|
|
8
|
+
overrideTaming: "severe",
|
|
9
|
+
consoleTaming: "unsafe",
|
|
10
|
+
localeTaming: "unsafe",
|
|
11
|
+
stackFiltering: "concise",
|
|
12
|
+
reporting: "none",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
const { parseHTMLDocument } = await import(
|
|
16
|
+
pathToFileURL(root + "/lib/happy-dom/parser.js")
|
|
17
|
+
);
|
|
18
|
+
const { StyleEngine, computeElementStyle } = await import(
|
|
19
|
+
pathToFileURL(root + "/lib/style/computed-style.js")
|
|
20
|
+
);
|
|
21
|
+
let assertions = 0,
|
|
22
|
+
states = 0,
|
|
23
|
+
metric = 7,
|
|
24
|
+
measurements = 0;
|
|
25
|
+
const measureText = (text, style) => {
|
|
26
|
+
measurements++;
|
|
27
|
+
return { width: text.length * metric, height: style.fontSize };
|
|
28
|
+
};
|
|
29
|
+
const f = parseHTMLDocument(
|
|
30
|
+
`<html style="font-size:18px"><body><section id="visible" style="color:purple;--tone:green"></section><div id="outer" style="color:red;--tone:blue"><div id="nested" style="display:none;font-size:20px"><i id="nestedchild" class="child"></i></div><div id="chain" style="font-size:1.2em;line-height:1.5"><span id="leaf" class="child" style="width:inherit"><b id="grand" class="child"></b></span></div><svg id="svg"><g id="g"><path id="path" style="fill:currentColor"/></g></svg></div></body></html>`,
|
|
31
|
+
);
|
|
32
|
+
let sheets = [
|
|
33
|
+
`.child{background:var(--tone);margin:2rem;padding:2ch;max-width:20vw}.child::before{content:"before";color:inherit;width:2ch} #outer:has(.active) .child {color:orange} #visible .child {font-weight:bold} @media(min-width:600px){.child{height:4em}}`,
|
|
34
|
+
];
|
|
35
|
+
let viewport = { width: 800, height: 600 },
|
|
36
|
+
lazy = new StyleEngine({ deferHidden: true, measureText }),
|
|
37
|
+
lastRestyled;
|
|
38
|
+
const el = (id) => f.document.getElementById(id);
|
|
39
|
+
function run(label, { dirty = null, self = null } = {}) {
|
|
40
|
+
lazy.compute(f.document, sheets, viewport, dirty, self);
|
|
41
|
+
lastRestyled = lazy.restyled;
|
|
42
|
+
const full = new StyleEngine({ measureText }).compute(
|
|
43
|
+
f.document,
|
|
44
|
+
sheets,
|
|
45
|
+
viewport,
|
|
46
|
+
);
|
|
47
|
+
// Query descendants before their ancestors to exercise path materialization.
|
|
48
|
+
const elements = [...f.document.querySelectorAll("*")].reverse();
|
|
49
|
+
for (const e of elements) {
|
|
50
|
+
assert.deepEqual(lazy.get(e), full.get(e), label + " style " + e.id);
|
|
51
|
+
assertions++;
|
|
52
|
+
assert.deepEqual(
|
|
53
|
+
lazy.customPropertiesOf(e),
|
|
54
|
+
full.customPropertiesOf(e),
|
|
55
|
+
label + " custom " + e.id,
|
|
56
|
+
);
|
|
57
|
+
assertions++;
|
|
58
|
+
for (const p of ["before", "after"]) {
|
|
59
|
+
assert.deepEqual(
|
|
60
|
+
lazy.getPseudo(e, p),
|
|
61
|
+
full.getPseudo(e, p),
|
|
62
|
+
label + " pseudo " + e.id,
|
|
63
|
+
);
|
|
64
|
+
assertions++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
assert.equal(
|
|
68
|
+
lazy.restyled,
|
|
69
|
+
lastRestyled,
|
|
70
|
+
label + " query does not affect traversal counter",
|
|
71
|
+
);
|
|
72
|
+
assertions++;
|
|
73
|
+
states++;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
run("visible with nested hidden");
|
|
77
|
+
el("outer").style.display = "none";
|
|
78
|
+
el("nested").style.fontSize = "31px";
|
|
79
|
+
run("outer hides stale nested boundary", { dirty: new Set([el("outer")]) });
|
|
80
|
+
el("outer").style.color = "green";
|
|
81
|
+
el("outer").style.setProperty("--tone", "pink");
|
|
82
|
+
run("hidden boundary inherit self", {
|
|
83
|
+
dirty: new Set(),
|
|
84
|
+
self: new Set([el("outer")]),
|
|
85
|
+
});
|
|
86
|
+
el("chain").style.fontSize = "33px";
|
|
87
|
+
el("leaf").style.width = "77px";
|
|
88
|
+
el("leaf").classList.add("active");
|
|
89
|
+
run("hidden inside roots", { dirty: new Set([el("chain"), el("leaf")]) });
|
|
90
|
+
el("nestedchild").style.cssText = "all:inherit;--tone:yellow";
|
|
91
|
+
run("hidden self inheritance", {
|
|
92
|
+
dirty: new Set(),
|
|
93
|
+
self: new Set([el("nestedchild")]),
|
|
94
|
+
});
|
|
95
|
+
sheets = [
|
|
96
|
+
...sheets,
|
|
97
|
+
".child{border:2px solid currentColor} #chain{width:91px}",
|
|
98
|
+
];
|
|
99
|
+
run("added sheet hidden", { dirty: new Set() });
|
|
100
|
+
viewport = { width: 500, height: 700 };
|
|
101
|
+
run("viewport full");
|
|
102
|
+
f.document.documentElement.style.fontSize = "22px";
|
|
103
|
+
run("root rem self", {
|
|
104
|
+
dirty: new Set(),
|
|
105
|
+
self: new Set([f.document.documentElement]),
|
|
106
|
+
});
|
|
107
|
+
metric = 11;
|
|
108
|
+
run("font environment full");
|
|
109
|
+
assert.equal(lazy.usesFontMetrics, true);
|
|
110
|
+
assertions++;
|
|
111
|
+
el("nested").style.display = "block";
|
|
112
|
+
run("nested reveal under hidden ancestor", {
|
|
113
|
+
dirty: new Set([el("nested")]),
|
|
114
|
+
});
|
|
115
|
+
el("outer").style.display = "block";
|
|
116
|
+
run("outer reveal", { dirty: new Set(), self: new Set([el("outer")]) });
|
|
117
|
+
el("visible").append(el("chain"));
|
|
118
|
+
run("move to visible different inherit", { dirty: new Set([el("chain")]) });
|
|
119
|
+
el("outer").style.display = "none";
|
|
120
|
+
run("hide again", { dirty: new Set([el("outer")]) });
|
|
121
|
+
el("outer").append(el("chain"));
|
|
122
|
+
run("move into hidden", { dirty: new Set([el("chain")]) });
|
|
123
|
+
el("chain").insertAdjacentHTML(
|
|
124
|
+
"beforeend",
|
|
125
|
+
'<em id="new" class="child" style="width:inherit"><strong id="newchild" style="font:inherit">new</strong></em>',
|
|
126
|
+
);
|
|
127
|
+
run("insert new hidden subtree", {
|
|
128
|
+
dirty: new Set([el("new"), el("newchild")]),
|
|
129
|
+
});
|
|
130
|
+
el("visible").append(el("nested"));
|
|
131
|
+
run("move stale former boundary visible", { dirty: new Set([el("nested")]) });
|
|
132
|
+
el("outer").classList.add("active");
|
|
133
|
+
run("hidden has ancestor dirty", { dirty: new Set([el("outer")]) });
|
|
134
|
+
f.document.body.style.display = "none";
|
|
135
|
+
run("body none conservative");
|
|
136
|
+
f.document.documentElement.style.display = "none";
|
|
137
|
+
run("html none conservative");
|
|
138
|
+
// Deterministic mutation stress with legal full/subtree invalidation.
|
|
139
|
+
let seed = 13;
|
|
140
|
+
const rnd = (n) => {
|
|
141
|
+
seed = (seed * 1664525 + 1013904223) >>> 0;
|
|
142
|
+
return seed % n;
|
|
143
|
+
};
|
|
144
|
+
const ids = ["outer", "nested", "chain", "leaf", "grand", "new", "newchild"];
|
|
145
|
+
for (let i = 0; i < 120; i++) {
|
|
146
|
+
const e = el(ids[rnd(ids.length)]),
|
|
147
|
+
op = rnd(6);
|
|
148
|
+
if (op === 0) e.style.display = rnd(2) ? "none" : "block";
|
|
149
|
+
if (op === 1) e.style.color = ["red", "blue", "green", "inherit"][rnd(4)];
|
|
150
|
+
if (op === 2)
|
|
151
|
+
e.style.setProperty("--tone", ["pink", "orange", "teal"][rnd(3)]);
|
|
152
|
+
if (op === 3) e.style.fontSize = 12 + rnd(15) + "px";
|
|
153
|
+
if (op === 4) e.style.width = ["inherit", "3rem", "5ch", "42px"][rnd(4)];
|
|
154
|
+
if (op === 5) e.classList.toggle("active");
|
|
155
|
+
run("stress " + i, { dirty: new Set([op === 5 ? f.document.body : e]) });
|
|
156
|
+
}
|
|
157
|
+
// Query only one hidden chain; siblings must stay uncomputed until requested.
|
|
158
|
+
const select = parseHTMLDocument(
|
|
159
|
+
'<div id="hide" style="display:none">' +
|
|
160
|
+
Array.from(
|
|
161
|
+
{ length: 500 },
|
|
162
|
+
(_, i) => `<span id="x${i}" style="width:1ch"></span>`,
|
|
163
|
+
).join("") +
|
|
164
|
+
"</div>",
|
|
165
|
+
);
|
|
166
|
+
try {
|
|
167
|
+
const limited = new StyleEngine({ deferHidden: true, measureText });
|
|
168
|
+
measurements = 0;
|
|
169
|
+
limited.compute(select.document);
|
|
170
|
+
assert.equal(measurements, 0);
|
|
171
|
+
const count = limited.restyled;
|
|
172
|
+
limited.get(select.document.getElementById("x250"));
|
|
173
|
+
assert.equal(measurements, 1);
|
|
174
|
+
assert.equal(limited.restyled, count);
|
|
175
|
+
limited.get(select.document.getElementById("x250"));
|
|
176
|
+
assert.equal(measurements, 1);
|
|
177
|
+
assertions += 4;
|
|
178
|
+
} finally {
|
|
179
|
+
select.window.happyDOM.abort();
|
|
180
|
+
}
|
|
181
|
+
// A new document/adopted subtree must use only its new context and maps.
|
|
182
|
+
const a = parseHTMLDocument(
|
|
183
|
+
'<div style="display:none;color:red;--x:old"><span id="adopted" style="width:3em"></span></div>',
|
|
184
|
+
);
|
|
185
|
+
const b = parseHTMLDocument(
|
|
186
|
+
'<div id="newhost" style="display:none;color:blue;font-size:25px;--x:new"></div>',
|
|
187
|
+
);
|
|
188
|
+
try {
|
|
189
|
+
const engine = new StyleEngine({ deferHidden: true }).compute(a.document),
|
|
190
|
+
node = a.document.getElementById("adopted");
|
|
191
|
+
const old = engine.get(node);
|
|
192
|
+
b.document.getElementById("newhost").append(node);
|
|
193
|
+
engine.compute(b.document);
|
|
194
|
+
assert.equal(engine.get(node).width, 75);
|
|
195
|
+
assert.equal(engine.get(node).color, "rgba(0, 0, 255, 1)");
|
|
196
|
+
assert.equal(engine.customPropertiesOf(node).get("--x"), "new");
|
|
197
|
+
assert.notEqual(engine.get(node), old);
|
|
198
|
+
assert.equal(engine.get(a.document.documentElement).display, "inline");
|
|
199
|
+
assertions += 5;
|
|
200
|
+
} finally {
|
|
201
|
+
a.window.happyDOM.abort();
|
|
202
|
+
b.window.happyDOM.abort();
|
|
203
|
+
}
|
|
204
|
+
// Lazy metric callbacks may compute another engine; their saved environment
|
|
205
|
+
// must not replace this query's viewport/rem context or survive an exception.
|
|
206
|
+
const envDoc = parseHTMLDocument(
|
|
207
|
+
'<html style="font-size:21px"><body><div style="display:none"><section id="partial" style="font-size:1.2em;width:11vw"><span id="metric" style="width:2ch;height:10vw;margin:2rem"></span></section></div></body></html>',
|
|
208
|
+
);
|
|
209
|
+
const otherDoc = parseHTMLDocument('<div style="width:10vw"></div>');
|
|
210
|
+
try {
|
|
211
|
+
const envEngine = new StyleEngine({
|
|
212
|
+
deferHidden: true,
|
|
213
|
+
measureText() {
|
|
214
|
+
new StyleEngine().compute(otherDoc.document, [], { width: 200 });
|
|
215
|
+
return 9;
|
|
216
|
+
},
|
|
217
|
+
}).compute(envDoc.document, [], { width: 800 });
|
|
218
|
+
const metricNode = envDoc.document.getElementById("metric");
|
|
219
|
+
Object.defineProperty(metricNode, "getAttribute", {
|
|
220
|
+
configurable: true,
|
|
221
|
+
value() {
|
|
222
|
+
throw Error("query failure");
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
let thrown = false;
|
|
226
|
+
try {
|
|
227
|
+
envEngine.get(metricNode);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
thrown = error.message === "query failure";
|
|
230
|
+
}
|
|
231
|
+
assert.equal(thrown, true);
|
|
232
|
+
delete metricNode.getAttribute;
|
|
233
|
+
const initial = envDoc.document.createElement("span");
|
|
234
|
+
initial.style.width = "2rem";
|
|
235
|
+
assert.equal(computeElementStyle(initial).width, 32);
|
|
236
|
+
assert.equal(
|
|
237
|
+
envEngine.get(envDoc.document.getElementById("partial")).width,
|
|
238
|
+
88,
|
|
239
|
+
);
|
|
240
|
+
assertions += 2;
|
|
241
|
+
const computed = envEngine.get(envDoc.document.getElementById("metric"));
|
|
242
|
+
assert.equal(computed.width, 18);
|
|
243
|
+
assert.equal(computed.height, 80);
|
|
244
|
+
assert.deepEqual(computed.margin, [42, 42, 42, 42]);
|
|
245
|
+
assertions += 4;
|
|
246
|
+
} finally {
|
|
247
|
+
envDoc.window.happyDOM.abort();
|
|
248
|
+
otherDoc.window.happyDOM.abort();
|
|
249
|
+
}
|
|
250
|
+
console.log(
|
|
251
|
+
JSON.stringify({
|
|
252
|
+
pass: true,
|
|
253
|
+
states,
|
|
254
|
+
assertions,
|
|
255
|
+
mode: process.argv.includes("--ses") ? "ses" : "plain",
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
} finally {
|
|
259
|
+
f.window.happyDOM.abort();
|
|
260
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
const root = new URL("../../", import.meta.url).pathname.replace(/\/$/, "");
|
|
3
|
+
if (process.argv.includes("--ses")) {
|
|
4
|
+
await import(root + "/vendor/ses/dist/ses.mjs");
|
|
5
|
+
lockdown({
|
|
6
|
+
errorTaming: "safe",
|
|
7
|
+
overrideTaming: "severe",
|
|
8
|
+
consoleTaming: "unsafe",
|
|
9
|
+
localeTaming: "unsafe",
|
|
10
|
+
stackFiltering: "concise",
|
|
11
|
+
reporting: "none",
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const { parseHTMLDocument } = await import(root + "/lib/happy-dom/parser.js");
|
|
15
|
+
const { StyleEngine, computeElementStyle } = await import(
|
|
16
|
+
root + "/lib/style/computed-style.js"
|
|
17
|
+
);
|
|
18
|
+
let assertions = 0;
|
|
19
|
+
function snapshot(engine, node) {
|
|
20
|
+
return {
|
|
21
|
+
width: engine.get(node).width,
|
|
22
|
+
fontSize: engine.get(node).fontSize,
|
|
23
|
+
custom: engine.customPropertiesOf(node).get("--x") ?? null,
|
|
24
|
+
pseudo: engine.getPseudo(node, "before")?.width ?? null,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function scenario(deferHidden, kind, warm = false) {
|
|
28
|
+
const f = parseHTMLDocument(
|
|
29
|
+
'<div id="hide" style="display:' +
|
|
30
|
+
(warm ? "block" : "none") +
|
|
31
|
+
'"><section id="parent" style="' +
|
|
32
|
+
(kind === "descendant" || kind === "parentpseudo"
|
|
33
|
+
? "width:1ch;font-size:24px"
|
|
34
|
+
: "") +
|
|
35
|
+
'"><span id="target" style="font-size:30px;--x:value;width:1ch"></span></section></div>',
|
|
36
|
+
);
|
|
37
|
+
const target = f.document.getElementById("target"),
|
|
38
|
+
parent = f.document.getElementById("parent");
|
|
39
|
+
let calls = 0,
|
|
40
|
+
inner = [];
|
|
41
|
+
const sheets =
|
|
42
|
+
kind === "pseudo"
|
|
43
|
+
? ['#target::before{content:"b";width:1ch}']
|
|
44
|
+
: kind === "parentpseudo"
|
|
45
|
+
? ['#parent::before{content:"b";width:1ch}']
|
|
46
|
+
: [];
|
|
47
|
+
const engine = new StyleEngine({
|
|
48
|
+
deferHidden,
|
|
49
|
+
measureText() {
|
|
50
|
+
if (++calls > 5) throw Error("reentry was not bounded");
|
|
51
|
+
inner.push({
|
|
52
|
+
target: snapshot(engine, target),
|
|
53
|
+
parent: snapshot(engine, parent),
|
|
54
|
+
});
|
|
55
|
+
return kind === "pseudo" ? engine.get(target).fontSize : 7;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
try {
|
|
59
|
+
engine.compute(f.document, sheets);
|
|
60
|
+
if (warm) {
|
|
61
|
+
f.document.getElementById("hide").style.display = "none";
|
|
62
|
+
target.style.fontSize = "40px";
|
|
63
|
+
calls = 0;
|
|
64
|
+
inner = [];
|
|
65
|
+
engine.compute(
|
|
66
|
+
f.document,
|
|
67
|
+
sheets,
|
|
68
|
+
{},
|
|
69
|
+
new Set([f.document.getElementById("hide")]),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const result = {
|
|
73
|
+
target: snapshot(engine, target),
|
|
74
|
+
parent: snapshot(engine, parent),
|
|
75
|
+
calls,
|
|
76
|
+
inner,
|
|
77
|
+
};
|
|
78
|
+
return result;
|
|
79
|
+
} finally {
|
|
80
|
+
f.window.happyDOM.abort();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const kind of ["same", "descendant", "pseudo", "parentpseudo"])
|
|
84
|
+
for (const warm of [false, true]) {
|
|
85
|
+
const eager = scenario(false, kind, warm),
|
|
86
|
+
lazy = scenario(true, kind, warm);
|
|
87
|
+
assert.deepEqual(
|
|
88
|
+
lazy,
|
|
89
|
+
eager,
|
|
90
|
+
kind + " reentrant values match eager snapshots",
|
|
91
|
+
);
|
|
92
|
+
assert.equal(
|
|
93
|
+
lazy.calls,
|
|
94
|
+
kind === "same" ? 1 : kind === "parentpseudo" ? 3 : 2,
|
|
95
|
+
);
|
|
96
|
+
assertions += 2;
|
|
97
|
+
}
|
|
98
|
+
// Ownership begins before ancestor/key extraction as well as measurements.
|
|
99
|
+
for (const ancestor of [false, true]) {
|
|
100
|
+
const f = parseHTMLDocument(
|
|
101
|
+
'<div id="hide" style="display:none"><span id="target" style="width:17px"></span></div>',
|
|
102
|
+
);
|
|
103
|
+
const target = f.document.getElementById("target"),
|
|
104
|
+
hooked = ancestor ? f.document.getElementById("hide") : target;
|
|
105
|
+
const engine = new StyleEngine({ deferHidden: true });
|
|
106
|
+
engine.compute(f.document);
|
|
107
|
+
let calls = 0,
|
|
108
|
+
inner = [];
|
|
109
|
+
const getAttribute = hooked.getAttribute;
|
|
110
|
+
Object.defineProperty(hooked, "getAttribute", {
|
|
111
|
+
configurable: true,
|
|
112
|
+
value(name) {
|
|
113
|
+
if (name === "id") {
|
|
114
|
+
if (++calls > 3) throw Error("key reentry was not bounded");
|
|
115
|
+
inner.push(engine.get(target).width);
|
|
116
|
+
}
|
|
117
|
+
return getAttribute.call(this, name);
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
try {
|
|
121
|
+
assert.equal(engine.get(target).width, 17);
|
|
122
|
+
assert.equal(calls, 1);
|
|
123
|
+
assert.deepEqual(inner, ["auto"]);
|
|
124
|
+
assertions += 3;
|
|
125
|
+
} finally {
|
|
126
|
+
delete hooked.getAttribute;
|
|
127
|
+
f.window.happyDOM.abort();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// A failure in a pseudo occurs after the principal values were stored. They
|
|
131
|
+
// must not make the next query mistake that failed node for a complete result.
|
|
132
|
+
{
|
|
133
|
+
const f = parseHTMLDocument(
|
|
134
|
+
'<html style="font-size:21px"><body><div id="hide" style="display:none"><span id="target" style="width:1ch"></span></div></body></html>',
|
|
135
|
+
);
|
|
136
|
+
const target = f.document.getElementById("target");
|
|
137
|
+
let calls = 0,
|
|
138
|
+
fail = true;
|
|
139
|
+
const engine = new StyleEngine({
|
|
140
|
+
deferHidden: true,
|
|
141
|
+
measureText() {
|
|
142
|
+
calls++;
|
|
143
|
+
if (fail && calls === 2)
|
|
144
|
+
Object.defineProperty(target, "matches", {
|
|
145
|
+
configurable: true,
|
|
146
|
+
get() {
|
|
147
|
+
throw Error("pseudo failure");
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
return 7;
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
try {
|
|
154
|
+
engine.compute(f.document, [
|
|
155
|
+
'#target::before{content:"b";width:1ch} #hide #target::after{content:"a"}',
|
|
156
|
+
]);
|
|
157
|
+
let thrown = false;
|
|
158
|
+
try {
|
|
159
|
+
engine.get(target);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
thrown = error.message === "pseudo failure";
|
|
162
|
+
}
|
|
163
|
+
assert.equal(thrown, true);
|
|
164
|
+
assert.equal(calls, 2);
|
|
165
|
+
delete target.matches;
|
|
166
|
+
fail = false;
|
|
167
|
+
const initial = f.document.createElement("i");
|
|
168
|
+
initial.style.width = "2rem";
|
|
169
|
+
assert.equal(computeElementStyle(initial).width, 32);
|
|
170
|
+
assert.equal(engine.get(target).width, 7);
|
|
171
|
+
assert.equal(engine.getPseudo(target, "before").width, 7);
|
|
172
|
+
assert.equal(engine.getPseudo(target, "after").content, "a");
|
|
173
|
+
assert.equal(calls, 4);
|
|
174
|
+
assertions += 7;
|
|
175
|
+
} finally {
|
|
176
|
+
f.window.happyDOM.abort();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// The guard belongs to an ensured generation, not the engine globally. A
|
|
180
|
+
// callback may start another document or another compute of the same document.
|
|
181
|
+
for (const sameDocument of [false, true]) {
|
|
182
|
+
const first = parseHTMLDocument(
|
|
183
|
+
'<div style="display:none;color:red"><span id="target" style="width:1ch;height:10vw"></span></div>',
|
|
184
|
+
);
|
|
185
|
+
const second = sameDocument
|
|
186
|
+
? first
|
|
187
|
+
: parseHTMLDocument(
|
|
188
|
+
'<div style="display:none;color:blue"><span id="target" style="width:1ch;height:10vw"></span></div>',
|
|
189
|
+
);
|
|
190
|
+
const oldTarget = first.document.getElementById("target"),
|
|
191
|
+
newTarget = second.document.getElementById("target");
|
|
192
|
+
let calls = 0,
|
|
193
|
+
nested = null;
|
|
194
|
+
const engine = new StyleEngine({
|
|
195
|
+
deferHidden: true,
|
|
196
|
+
measureText() {
|
|
197
|
+
calls++;
|
|
198
|
+
if (calls === 1) {
|
|
199
|
+
engine.compute(second.document, [], { width: 400 });
|
|
200
|
+
nested = engine.get(newTarget);
|
|
201
|
+
}
|
|
202
|
+
return 7;
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
try {
|
|
206
|
+
engine.compute(first.document, [], { width: 800 });
|
|
207
|
+
engine.get(oldTarget);
|
|
208
|
+
assert.equal(calls, 2);
|
|
209
|
+
assert.equal(nested.width, 7);
|
|
210
|
+
assert.equal(nested.height, 40);
|
|
211
|
+
assert.equal(engine.get(newTarget), nested);
|
|
212
|
+
assert.equal(
|
|
213
|
+
engine.get(newTarget).color,
|
|
214
|
+
sameDocument ? "rgba(255, 0, 0, 1)" : "rgba(0, 0, 255, 1)",
|
|
215
|
+
);
|
|
216
|
+
assertions += 5;
|
|
217
|
+
if (!sameDocument) {
|
|
218
|
+
assert.equal(engine.get(oldTarget).width, "auto");
|
|
219
|
+
assertions++;
|
|
220
|
+
}
|
|
221
|
+
} finally {
|
|
222
|
+
first.window.happyDOM.abort();
|
|
223
|
+
if (!sameDocument) second.window.happyDOM.abort();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
console.log(
|
|
227
|
+
JSON.stringify({
|
|
228
|
+
pass: true,
|
|
229
|
+
assertions,
|
|
230
|
+
mode: process.argv.includes("--ses") ? "ses" : "plain",
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
const root = new URL("../../", import.meta.url).pathname.replace(/\/$/, ""),
|
|
3
|
+
mode = "ses";
|
|
4
|
+
const { createPageRenderer } = await import(
|
|
5
|
+
root + "/lib/renderer/page-renderer.js"
|
|
6
|
+
);
|
|
7
|
+
const errors = [],
|
|
8
|
+
requests = [];
|
|
9
|
+
const png = Buffer.from(
|
|
10
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a6XQAAAAASUVORK5CYII=",
|
|
11
|
+
"base64",
|
|
12
|
+
);
|
|
13
|
+
const mono = await Bun.file(
|
|
14
|
+
root + "/usr/share/fonts/DejaVuSansMono.ttf",
|
|
15
|
+
).bytes();
|
|
16
|
+
const sans = await Bun.file(
|
|
17
|
+
root + "/usr/share/fonts/Roboto-Regular.ttf",
|
|
18
|
+
).bytes();
|
|
19
|
+
const options = {
|
|
20
|
+
reportError: (e) => errors.push(String(e)),
|
|
21
|
+
pageFetch: async ({ url }) => {
|
|
22
|
+
requests.push(url);
|
|
23
|
+
return {
|
|
24
|
+
status: 200,
|
|
25
|
+
body: url.endsWith("mono.ttf")
|
|
26
|
+
? mono
|
|
27
|
+
: url.endsWith("sans.ttf")
|
|
28
|
+
? sans
|
|
29
|
+
: png,
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
fetchResource: async (url) => {
|
|
33
|
+
requests.push(url);
|
|
34
|
+
return { ok: true, status: 200, url, body: png };
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
const renderer = await createPageRenderer(options);
|
|
38
|
+
if (mode === "ses") {
|
|
39
|
+
await import(root + "/vendor/ses/dist/ses.mjs");
|
|
40
|
+
lockdown({
|
|
41
|
+
errorTaming: "safe",
|
|
42
|
+
overrideTaming: "severe",
|
|
43
|
+
consoleTaming: "unsafe",
|
|
44
|
+
localeTaming: "unsafe",
|
|
45
|
+
stackFiltering: "concise",
|
|
46
|
+
reporting: "none",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const phases = [];
|
|
50
|
+
async function state(label, mutation = "") {
|
|
51
|
+
if (mutation) await renderer.evaluate(mutation);
|
|
52
|
+
const values = await renderer.evaluate(
|
|
53
|
+
`(() => {const ids=['outer','nested','leaf','grand','new','canvas'];return ids.map(id=>{const e=document.getElementById(id);if(!e)return null;const s=getComputedStyle(e);const b=e.getBoundingClientRect();return {id,color:s.color,width:s.width,height:s.height,padding:s.padding,fontSize:s.fontSize,display:s.display,custom:s.getPropertyValue('--tone'),rect:[b.x,b.y,b.width,b.height]};});})()`,
|
|
54
|
+
);
|
|
55
|
+
const saved = await renderer.evaluate(
|
|
56
|
+
`window.saved ? [saved.color,saved.width,saved.getPropertyValue('--tone'),saved.length,keys===Object.keys(saved).join('|')] : null`,
|
|
57
|
+
);
|
|
58
|
+
const image = await renderer.screenshot({ format: "rgba" });
|
|
59
|
+
phases.push({
|
|
60
|
+
label,
|
|
61
|
+
values,
|
|
62
|
+
saved,
|
|
63
|
+
pixelHash: new Bun.CryptoHasher("sha256")
|
|
64
|
+
.update(image.pixels)
|
|
65
|
+
.digest("hex"),
|
|
66
|
+
});
|
|
67
|
+
const source = await renderer.evaluate("document.documentElement.outerHTML");
|
|
68
|
+
const fresh = await createPageRenderer({
|
|
69
|
+
...options,
|
|
70
|
+
pageFetch: async ({ url }) => ({
|
|
71
|
+
status: 200,
|
|
72
|
+
body: url.endsWith("mono.ttf")
|
|
73
|
+
? mono
|
|
74
|
+
: url.endsWith("sans.ttf")
|
|
75
|
+
? sans
|
|
76
|
+
: png,
|
|
77
|
+
}),
|
|
78
|
+
fetchResource: async (url) => ({ ok: true, status: 200, url, body: png }),
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
fresh.setScripting(mode);
|
|
82
|
+
fresh.resize(
|
|
83
|
+
label === "viewport" ||
|
|
84
|
+
[
|
|
85
|
+
"remove-font",
|
|
86
|
+
"replace-font",
|
|
87
|
+
"replacement-ready",
|
|
88
|
+
"final-reveal",
|
|
89
|
+
].includes(label)
|
|
90
|
+
? 220
|
|
91
|
+
: 320,
|
|
92
|
+
180,
|
|
93
|
+
1,
|
|
94
|
+
);
|
|
95
|
+
await fresh.loadDocument({
|
|
96
|
+
url: "https://hidden.test/",
|
|
97
|
+
contentType: "text/html",
|
|
98
|
+
source,
|
|
99
|
+
});
|
|
100
|
+
await fresh.whenLoaded();
|
|
101
|
+
const expected = await fresh.screenshot({ format: "rgba" });
|
|
102
|
+
if (!Buffer.from(image.pixels).equals(Buffer.from(expected.pixels)))
|
|
103
|
+
throw new Error(
|
|
104
|
+
label +
|
|
105
|
+
" pixel mismatch " +
|
|
106
|
+
Bun.hash(image.pixels) +
|
|
107
|
+
"/" +
|
|
108
|
+
Bun.hash(expected.pixels),
|
|
109
|
+
);
|
|
110
|
+
const expectedGeometry = fresh.inspect(
|
|
111
|
+
"#outer,#nested,#leaf,#grand,#new,#canvas",
|
|
112
|
+
["color", "width", "height", "fontSize", "display"],
|
|
113
|
+
);
|
|
114
|
+
const actualGeometry = renderer.inspect(
|
|
115
|
+
"#outer,#nested,#leaf,#grand,#new,#canvas",
|
|
116
|
+
["color", "width", "height", "fontSize", "display"],
|
|
117
|
+
);
|
|
118
|
+
if (!Bun.deepEquals(actualGeometry, expectedGeometry, true))
|
|
119
|
+
throw new Error(
|
|
120
|
+
label +
|
|
121
|
+
" geometry mismatch " +
|
|
122
|
+
JSON.stringify({ actualGeometry, expectedGeometry }),
|
|
123
|
+
);
|
|
124
|
+
} finally {
|
|
125
|
+
fresh.dispose();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
renderer.setScripting(mode);
|
|
130
|
+
renderer.resize(320, 180, 1);
|
|
131
|
+
await renderer.loadDocument({
|
|
132
|
+
url: "https://hidden.test/",
|
|
133
|
+
contentType: "text/html",
|
|
134
|
+
source: `<!doctype html><style id="faces">@font-face{font-family:Probe;src:url(/mono.ttf)}</style><style id="rules">html{font-size:18px}body{margin:0}#visible{width:210px;background:yellow;color:purple;--tone:green}#outer{display:none;color:red;--tone:blue}#nested{display:none;font-size:20px}.child{display:inline-block;background:var(--tone);width:3ch;height:2em;padding:1rem;font-family:Probe,serif}.child::before{content:'x';color:orange}#outer:has(.active) .child{color:teal}@media(max-width:250px){.child{padding:3px}}</style><span style="font-family:Probe">0</span><div id="outer"><section id="nested"><i class="child" id="grand">grand</i></section><section id="chain"><span id="leaf" class="child">leaf</span><canvas id="canvas" width="30" height="20"></canvas><img src="/pixel.png" width="10" height="10"></section></div><div id="visible">visible</div>`,
|
|
135
|
+
});
|
|
136
|
+
await renderer.whenLoaded();
|
|
137
|
+
await renderer.evaluate(
|
|
138
|
+
`window.leaf=document.getElementById('leaf');window.saved=getComputedStyle(leaf);window.keys=Object.keys(saved).join('|')`,
|
|
139
|
+
);
|
|
140
|
+
await state("initial-hidden");
|
|
141
|
+
await state(
|
|
142
|
+
"hidden-values-mutate",
|
|
143
|
+
`document.getElementById('outer').style.cssText='display:none;color:green;--tone:pink';leaf.style.width='5ch';document.getElementById('nested').style.fontSize='31px'`,
|
|
144
|
+
);
|
|
145
|
+
await state("clean-repeat");
|
|
146
|
+
await state(
|
|
147
|
+
"insert",
|
|
148
|
+
`document.getElementById('chain').insertAdjacentHTML('beforeend','<em class="child" id="new" style="width:inherit">new</em>')`,
|
|
149
|
+
);
|
|
150
|
+
await state("selector-change", `leaf.classList.add('active')`);
|
|
151
|
+
await state(
|
|
152
|
+
"new-sheet",
|
|
153
|
+
`document.getElementById('rules').textContent+=' .child{border:2px solid currentColor}'`,
|
|
154
|
+
);
|
|
155
|
+
await state(
|
|
156
|
+
"nested-reveal-hidden",
|
|
157
|
+
`document.getElementById('nested').style.display='block'`,
|
|
158
|
+
);
|
|
159
|
+
await state(
|
|
160
|
+
"outer-reveal",
|
|
161
|
+
`document.getElementById('outer').style.display='block'`,
|
|
162
|
+
);
|
|
163
|
+
await state(
|
|
164
|
+
"hidden-again",
|
|
165
|
+
`document.getElementById('outer').style.display='none'`,
|
|
166
|
+
);
|
|
167
|
+
await state(
|
|
168
|
+
"move-leaf-visible",
|
|
169
|
+
`document.getElementById('visible').append(leaf)`,
|
|
170
|
+
);
|
|
171
|
+
await state("detached", `leaf.remove()`);
|
|
172
|
+
await state(
|
|
173
|
+
"detached-mutate",
|
|
174
|
+
`leaf.style.color='rgb(12,34,56)';leaf.style.width='77px'`,
|
|
175
|
+
);
|
|
176
|
+
await state(
|
|
177
|
+
"reattach-hidden",
|
|
178
|
+
`document.getElementById('chain').append(leaf)`,
|
|
179
|
+
);
|
|
180
|
+
await state("root-rem", `document.documentElement.style.fontSize='22px'`);
|
|
181
|
+
renderer.resize(220, 180, 1);
|
|
182
|
+
await state("viewport");
|
|
183
|
+
await state("remove-font", `document.getElementById('faces').textContent=''`);
|
|
184
|
+
await state(
|
|
185
|
+
"replace-font",
|
|
186
|
+
`document.getElementById('faces').textContent='@font-face{font-family:Probe;src:url(/sans.ttf)}'`,
|
|
187
|
+
);
|
|
188
|
+
await Bun.sleep(0);
|
|
189
|
+
await state("replacement-ready");
|
|
190
|
+
await state(
|
|
191
|
+
"final-reveal",
|
|
192
|
+
`document.getElementById('outer').style.display='block'`,
|
|
193
|
+
);
|
|
194
|
+
assert.deepEqual(errors, []);
|
|
195
|
+
assert.deepEqual(requests.map((x) => new URL(x).pathname).sort(), [
|
|
196
|
+
"/mono.ttf",
|
|
197
|
+
"/pixel.png",
|
|
198
|
+
"/sans.ttf",
|
|
199
|
+
]);
|
|
200
|
+
console.log(JSON.stringify({ pass: true, states: phases.length }));
|
|
201
|
+
} finally {
|
|
202
|
+
renderer.dispose();
|
|
203
|
+
}
|