buninu-browser 0.5.1 → 0.6.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 +9 -3
- package/lib/audio/pcm.js +11 -0
- package/lib/audio/pulse-client.js +7 -2
- package/lib/cdp-server.js +31 -7
- 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/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/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,234 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { parseHTMLDocument } from "../lib/happy-dom/parser.js";
|
|
3
|
+
import { createResourceElements } from "../lib/renderer/resource-elements.js";
|
|
4
|
+
|
|
5
|
+
async function fixture(run) {
|
|
6
|
+
const { window, document } = parseHTMLDocument(`<!doctype html><head>
|
|
7
|
+
<style>body {color:red}</style><link rel="preload" href="one.css"></head>
|
|
8
|
+
<body><img src="one.png"><video></video><iframe></iframe></body>`, "https://resource.test/");
|
|
9
|
+
const query = document.querySelectorAll.bind(document), queries = [];
|
|
10
|
+
document.querySelectorAll = selector => { queries.push(selector); return query(selector); };
|
|
11
|
+
const cache = createResourceElements(document);
|
|
12
|
+
try { await run({ window, document, cache, queries }); }
|
|
13
|
+
finally { cache.dispose(); await window.happyDOM.close(); }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test("metadata changes retain discovery while resource attributes and sheet text remain live", async () => {
|
|
17
|
+
await fixture(async ({ document, cache, queries }) => {
|
|
18
|
+
const before = Object.fromEntries(["media", "images", "sheets", "frames"].map(kind => [kind, cache.get(kind)]));
|
|
19
|
+
const [image] = before.images, [style] = before.sheets;
|
|
20
|
+
queries.length = 0;
|
|
21
|
+
for (let i = 0; i < 20; i++) {
|
|
22
|
+
document.body.setAttribute("data-counter", String(i));
|
|
23
|
+
image.setAttribute("aria-label", String(i));
|
|
24
|
+
image.setAttribute("class", `item-${i}`);
|
|
25
|
+
for (const kind of Object.keys(before)) expect(cache.get(kind)).toBe(before[kind]);
|
|
26
|
+
}
|
|
27
|
+
image.setAttribute("src", "two.png");
|
|
28
|
+
image.setAttribute("crossorigin", "anonymous");
|
|
29
|
+
style.setAttribute("media", "print");
|
|
30
|
+
style.setAttribute("type", "text/plain");
|
|
31
|
+
style.firstChild.data = "body {color:blue}";
|
|
32
|
+
expect(cache.get("images")[0].getAttribute("src")).toBe("two.png");
|
|
33
|
+
expect(cache.get("images")[0].getAttribute("crossorigin")).toBe("anonymous");
|
|
34
|
+
expect(cache.get("sheets")[0].textContent).toBe("body {color:blue}");
|
|
35
|
+
expect(cache.get("sheets")[0].getAttribute("media")).toBe("print");
|
|
36
|
+
expect(cache.get("sheets")[0].getAttribute("type")).toBe("text/plain");
|
|
37
|
+
expect(queries).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("rel membership is refreshed synchronously without rescanning other resource kinds", async () => {
|
|
42
|
+
await fixture(async ({ document, cache, queries }) => {
|
|
43
|
+
const imageList = cache.get("images"), sheets = cache.get("sheets");
|
|
44
|
+
const link = document.getElementsByTagName("link")[0];
|
|
45
|
+
queries.length = 0;
|
|
46
|
+
link.setAttribute("rel", "alternate stylesheet");
|
|
47
|
+
expect(cache.get("sheets")).toEqual([sheets[0], link]);
|
|
48
|
+
expect(cache.get("images")).toBe(imageList);
|
|
49
|
+
link.setAttribute("href", "two.css");
|
|
50
|
+
link.setAttribute("disabled", "");
|
|
51
|
+
expect(cache.get("sheets")[1].getAttribute("href")).toBe("two.css");
|
|
52
|
+
expect(cache.get("sheets")[1].hasAttribute("disabled")).toBe(true);
|
|
53
|
+
link.removeAttribute("rel");
|
|
54
|
+
expect(cache.get("sheets")).toEqual(sheets);
|
|
55
|
+
expect(queries).toEqual(["style, link[rel~='stylesheet']", "style, link[rel~='stylesheet']"]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("text and comment replacement preserves element discovery but stylesheet contents stay live", async () => {
|
|
60
|
+
await fixture(async ({ document, cache, queries }) => {
|
|
61
|
+
const counter = document.createElement("span");
|
|
62
|
+
document.body.append(counter);
|
|
63
|
+
const kinds = ["media", "images", "sheets", "frames"];
|
|
64
|
+
const lists = Object.fromEntries(kinds.map(kind => [kind, cache.get(kind)]));
|
|
65
|
+
const style = lists.sheets[0];
|
|
66
|
+
queries.length = 0;
|
|
67
|
+
for (let index = 0; index < 20; index++) {
|
|
68
|
+
counter.textContent = String(index);
|
|
69
|
+
const comment = document.createComment("counter");
|
|
70
|
+
counter.append(comment);
|
|
71
|
+
comment.remove();
|
|
72
|
+
style.textContent = `body { z-index: ${index} }`;
|
|
73
|
+
for (const kind of kinds) expect(cache.get(kind)).toBe(lists[kind]);
|
|
74
|
+
expect(cache.get("sheets")[0].textContent).toBe(`body { z-index: ${index} }`);
|
|
75
|
+
}
|
|
76
|
+
counter.textContent = "after delivery";
|
|
77
|
+
await Bun.sleep(10);
|
|
78
|
+
for (const kind of kinds) expect(cache.get(kind)).toBe(lists[kind]);
|
|
79
|
+
expect(queries).toEqual([]);
|
|
80
|
+
// An element removed through textContent still invalidates membership.
|
|
81
|
+
counter.append(lists.images[0]);
|
|
82
|
+
cache.get("images");
|
|
83
|
+
counter.textContent = "removed image";
|
|
84
|
+
expect(cache.get("images")).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("insert, remove and reattach are discovered before and after observer delivery", async () => {
|
|
89
|
+
await fixture(async ({ document, cache }) => {
|
|
90
|
+
const old = cache.get("images")[0];
|
|
91
|
+
const subtree = document.createElement("section");
|
|
92
|
+
subtree.innerHTML = '<img src="new.png"><audio></audio><style>p{color:red}</style><iframe></iframe>';
|
|
93
|
+
document.body.append(subtree);
|
|
94
|
+
expect(cache.get("images")).toEqual([old, subtree.firstChild]);
|
|
95
|
+
expect(cache.get("media").map(element => element.tagName)).toEqual(["VIDEO", "AUDIO"]);
|
|
96
|
+
expect(cache.get("frames")).toHaveLength(2);
|
|
97
|
+
expect(cache.get("sheets")).toHaveLength(2);
|
|
98
|
+
subtree.remove();
|
|
99
|
+
expect(cache.get("images")).toEqual([old]);
|
|
100
|
+
document.body.append(subtree);
|
|
101
|
+
await Bun.sleep(10); // exercise the observer callback, not just takeRecords()
|
|
102
|
+
expect(cache.get("frames")).toHaveLength(2);
|
|
103
|
+
old.remove();
|
|
104
|
+
expect(cache.get("images")).toEqual([subtree.firstChild]);
|
|
105
|
+
document.body.append(old);
|
|
106
|
+
expect(cache.get("images")).toEqual([subtree.firstChild, old]);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("navigation/disposal releases old discovery and base URLs are read from live elements", async () => {
|
|
111
|
+
await fixture(async ({ document, cache }) => {
|
|
112
|
+
const image = cache.get("images")[0];
|
|
113
|
+
const base = document.createElement("base");
|
|
114
|
+
base.setAttribute("href", "/first/"); document.head.prepend(base);
|
|
115
|
+
expect(cache.get("images")[0]).toBe(image);
|
|
116
|
+
expect(new URL(image.getAttribute("src"), image.baseURI).href).toBe("https://resource.test/first/one.png");
|
|
117
|
+
base.setAttribute("href", "/second/");
|
|
118
|
+
expect(new URL(cache.get("images")[0].getAttribute("src"), image.baseURI).href).toBe("https://resource.test/second/one.png");
|
|
119
|
+
cache.dispose();
|
|
120
|
+
expect(() => cache.get("images")).toThrow("disposed");
|
|
121
|
+
const fresh = createResourceElements(document);
|
|
122
|
+
try {
|
|
123
|
+
image.remove();
|
|
124
|
+
expect(fresh.get("images")).toEqual([]);
|
|
125
|
+
} finally { fresh.dispose(); }
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("ordinary subtree edits retain resource lists and image edits only rescan images", async () => {
|
|
130
|
+
for (const delivered of [false, true]) await fixture(async ({ document, cache, queries }) => {
|
|
131
|
+
const container = document.createElement("div");
|
|
132
|
+
document.body.append(container);
|
|
133
|
+
const before = Object.fromEntries(["media", "images", "sheets", "frames"].map(kind => [kind, cache.get(kind)]));
|
|
134
|
+
queries.length = 0;
|
|
135
|
+
for (let index = 0; index < 3; index++) {
|
|
136
|
+
container.innerHTML = `<section><span>counter ${index}</span></section>`;
|
|
137
|
+
if (delivered) await Bun.sleep(10);
|
|
138
|
+
for (const kind of Object.keys(before)) expect(cache.get(kind)).toBe(before[kind]);
|
|
139
|
+
}
|
|
140
|
+
expect(queries).toEqual([]);
|
|
141
|
+
// Several connected insertion records overlap the same subtree.
|
|
142
|
+
let parent = container;
|
|
143
|
+
for (let index = 0; index < 100; index++) {
|
|
144
|
+
const child = document.createElement("div");
|
|
145
|
+
parent.append(child); parent = child;
|
|
146
|
+
}
|
|
147
|
+
const image = document.createElement("img");
|
|
148
|
+
parent.append(image);
|
|
149
|
+
if (delivered) await Bun.sleep(10);
|
|
150
|
+
expect(cache.get("images")).toEqual([...before.images, image]);
|
|
151
|
+
for (const kind of ["media", "sheets", "frames"]) expect(cache.get(kind)).toBe(before[kind]);
|
|
152
|
+
expect(queries).toEqual(["img"]);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("removed subtrees still invalidate after their resource descendants are detached", async () => {
|
|
157
|
+
for (const delivered of [false, true]) await fixture(async ({ document, cache }) => {
|
|
158
|
+
const kinds = ["media", "images", "sheets", "frames"];
|
|
159
|
+
const original = Object.fromEntries(kinds.map(kind => [kind, [...cache.get(kind)]]));
|
|
160
|
+
const subtree = document.createElement("section");
|
|
161
|
+
subtree.innerHTML = '<div><img><audio></audio><style>p{color:blue}</style><iframe></iframe></div>';
|
|
162
|
+
document.body.append(subtree);
|
|
163
|
+
for (const kind of kinds) expect(cache.get(kind)).toHaveLength(original[kind].length + 1);
|
|
164
|
+
subtree.remove();
|
|
165
|
+
subtree.firstChild.replaceChildren();
|
|
166
|
+
subtree.replaceChildren();
|
|
167
|
+
if (delivered) await Bun.sleep(10);
|
|
168
|
+
for (const kind of kinds) expect(cache.get(kind)).toEqual(original[kind]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("detached rel edits, reordering and moves into shadow roots refresh old membership", async () => {
|
|
173
|
+
for (const delivered of [false, true]) await fixture(async ({ document, cache }) => {
|
|
174
|
+
const subtree = document.createElement("section");
|
|
175
|
+
subtree.innerHTML = '<link rel="alternate stylesheet"><img id="second">';
|
|
176
|
+
document.body.append(subtree);
|
|
177
|
+
const sheets = cache.get("sheets"), images = cache.get("images");
|
|
178
|
+
const link = subtree.firstChild, second = subtree.lastChild;
|
|
179
|
+
subtree.remove();
|
|
180
|
+
link.setAttribute("rel", "preload");
|
|
181
|
+
document.body.prepend(subtree);
|
|
182
|
+
if (delivered) await Bun.sleep(10);
|
|
183
|
+
expect(cache.get("sheets")).toEqual(sheets.filter(element => element !== link));
|
|
184
|
+
expect(cache.get("images")).toEqual([second, images[0]]);
|
|
185
|
+
const host = document.createElement("div");
|
|
186
|
+
document.body.append(host);
|
|
187
|
+
host.attachShadow({ mode: "open" }).append(second);
|
|
188
|
+
if (delivered) await Bun.sleep(10);
|
|
189
|
+
expect(cache.get("images")).toEqual([images[0]]);
|
|
190
|
+
document.body.append(second);
|
|
191
|
+
expect(cache.get("images")).toEqual([images[0], second]);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("adopting resource subtrees updates discovery in both documents", async () => {
|
|
196
|
+
await fixture(async ({ document, cache }) => {
|
|
197
|
+
const other = parseHTMLDocument("<body></body>");
|
|
198
|
+
const next = createResourceElements(other.document);
|
|
199
|
+
try {
|
|
200
|
+
const image = cache.get("images")[0];
|
|
201
|
+
expect(next.get("images")).toEqual([]);
|
|
202
|
+
const container = document.createElement("section");
|
|
203
|
+
document.body.append(container); container.append(image);
|
|
204
|
+
cache.get("images");
|
|
205
|
+
other.document.body.append(container);
|
|
206
|
+
expect(cache.get("images")).toEqual([]);
|
|
207
|
+
expect(next.get("images")).toEqual([image]);
|
|
208
|
+
container.remove(); container.replaceChildren();
|
|
209
|
+
await Bun.sleep(10);
|
|
210
|
+
expect(next.get("images")).toEqual([]);
|
|
211
|
+
} finally { next.dispose(); await other.window.happyDOM.close(); }
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("foreign element tag names follow the same matching rules as discovery selectors", async () => {
|
|
216
|
+
await fixture(async ({ document, cache }) => {
|
|
217
|
+
const selectors = { media: "video, audio", images: "img", sheets: "style, link[rel~='stylesheet']", frames: "iframe" };
|
|
218
|
+
for (const kind of Object.keys(selectors)) cache.get(kind);
|
|
219
|
+
for (const namespace of ["http://www.w3.org/2000/svg", "urn:resource-test"]) {
|
|
220
|
+
for (const name of ["img", "IMG", "ImG", "x:img", "STYLE", "StYle", "LINK", "lInK", "aUdIo", "ViDeO", "IfRaMe"]) {
|
|
221
|
+
const node = document.createElementNS(namespace, name);
|
|
222
|
+
node.setAttribute("rel", "stylesheet");
|
|
223
|
+
document.body.append(node);
|
|
224
|
+
for (const [kind, selector] of Object.entries(selectors)) {
|
|
225
|
+
expect(cache.get(kind)).toEqual([...document.querySelectorAll(selector)]);
|
|
226
|
+
}
|
|
227
|
+
node.remove();
|
|
228
|
+
for (const [kind, selector] of Object.entries(selectors)) {
|
|
229
|
+
expect(cache.get(kind)).toEqual([...document.querySelectorAll(selector)]);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {expect, test} from 'bun:test';
|
|
2
|
+
import {parseHTMLDocument} from '../lib/happy-dom/parser.js';
|
|
3
|
+
import {StyleEngine} from '../lib/style/computed-style.js';
|
|
4
|
+
|
|
5
|
+
function fixture(source, sheets = []) {
|
|
6
|
+
const parsed = parseHTMLDocument(source);
|
|
7
|
+
return {...parsed, sheets, engine: new StyleEngine().compute(parsed.document, sheets)};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function matchesFull({document, sheets, engine}) {
|
|
11
|
+
const reference = new StyleEngine().compute(document, sheets);
|
|
12
|
+
for (const element of document.querySelectorAll('*')) {
|
|
13
|
+
expect(engine.get(element)).toEqual(reference.get(element));
|
|
14
|
+
expect(engine.customPropertiesOf(element)).toEqual(reference.customPropertiesOf(element));
|
|
15
|
+
for (const pseudo of ['before', 'after']) expect(engine.getPseudo(element, pseudo)).toEqual(reference.getPseudo(element, pseudo));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test('an unchanged root-only selector match does not recascade its descendants', () => {
|
|
20
|
+
const f = fixture('<html><body><div><span>content</span></div></body></html>', ['html[data-check] { opacity:1 }']);
|
|
21
|
+
try {
|
|
22
|
+
const root = f.document.documentElement;
|
|
23
|
+
const retained = [...f.document.querySelectorAll('*')].map(element => [element, f.engine.get(element)]);
|
|
24
|
+
root.setAttribute('data-check', 'ready');
|
|
25
|
+
f.engine.compute(f.document, f.sheets, {}, new Set(), new Set([root]));
|
|
26
|
+
expect(f.engine.restyled).toBe(1);
|
|
27
|
+
for (const [element, style] of retained) expect(f.engine.get(element)).toBe(style);
|
|
28
|
+
matchesFull(f);
|
|
29
|
+
} finally { f.window.happyDOM.abort(); }
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('an unchanged root still processes simultaneous dirty subtrees and self-only elements', () => {
|
|
33
|
+
const f = fixture('<html><body><div id="subtree"><span>first</span></div><p id="other">other</p></body></html>', ['html[data-check] { opacity:1 }']);
|
|
34
|
+
try {
|
|
35
|
+
const root = f.document.documentElement, subtree = f.document.querySelector('#subtree'), other = f.document.querySelector('#other');
|
|
36
|
+
root.setAttribute('data-check', 'ready');
|
|
37
|
+
subtree.style.color = 'red';
|
|
38
|
+
subtree.insertAdjacentHTML('beforeend', '<span>new</span>');
|
|
39
|
+
other.style.color = 'blue';
|
|
40
|
+
f.engine.compute(f.document, f.sheets, {}, new Set([subtree]), new Set([root, other]));
|
|
41
|
+
expect(f.engine.restyled).toBe(5);
|
|
42
|
+
expect(f.engine.get(subtree.lastElementChild).color).toBe('rgba(255, 0, 0, 1)');
|
|
43
|
+
expect(f.engine.get(other).color).toBe('rgba(0, 0, 255, 1)');
|
|
44
|
+
matchesFull(f);
|
|
45
|
+
} finally { f.window.happyDOM.abort(); }
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('root-only font changes update inherited and rem sizes using the initial root font basis', () => {
|
|
49
|
+
const f = fixture('<html style="font-size:2rem"><body><div id="child" style="width:2rem;font-size:150%">text</div></body></html>');
|
|
50
|
+
try {
|
|
51
|
+
const root = f.document.documentElement, child = f.document.querySelector('#child');
|
|
52
|
+
// Rechecking the unchanged root must not multiply 2rem by its prior 32px.
|
|
53
|
+
f.engine.compute(f.document, f.sheets, {}, new Set(), new Set([root]));
|
|
54
|
+
expect(f.engine.restyled).toBe(1);
|
|
55
|
+
expect(f.engine.get(root).fontSize).toBe(32);
|
|
56
|
+
expect(f.engine.get(child)).toMatchObject({width:64, fontSize:48});
|
|
57
|
+
root.style.fontSize = '1.5rem';
|
|
58
|
+
f.engine.compute(f.document, f.sheets, {}, new Set(), new Set([root]));
|
|
59
|
+
expect(f.engine.get(root).fontSize).toBe(24);
|
|
60
|
+
expect(f.engine.get(child)).toMatchObject({width:48, fontSize:36});
|
|
61
|
+
matchesFull(f);
|
|
62
|
+
} finally { f.window.happyDOM.abort(); }
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('root custom properties and explicit inheritance still recascade descendants', () => {
|
|
66
|
+
const f = fixture('<html style="--accent:red;width:120px"><body style="width:inherit"><div id="child" style="color:var(--accent);width:inherit">text</div></body></html>');
|
|
67
|
+
try {
|
|
68
|
+
const root = f.document.documentElement, child = f.document.querySelector('#child');
|
|
69
|
+
const before = f.engine.get(root);
|
|
70
|
+
root.style.setProperty('--accent', 'blue');
|
|
71
|
+
child.insertAdjacentHTML('beforeend', '<span>inserted during the root change</span>');
|
|
72
|
+
f.engine.compute(f.document, f.sheets, {}, new Set([child]), new Set([root]));
|
|
73
|
+
expect(f.engine.get(root)).toBe(before);
|
|
74
|
+
expect(f.engine.get(child).color).toBe('rgba(0, 0, 255, 1)');
|
|
75
|
+
expect(f.engine.get(child.lastElementChild).color).toBe('rgba(0, 0, 255, 1)');
|
|
76
|
+
matchesFull(f);
|
|
77
|
+
root.style.width = '200px';
|
|
78
|
+
f.engine.compute(f.document, f.sheets, {}, new Set(), new Set([root]));
|
|
79
|
+
expect(f.engine.get(child).width).toBe(200);
|
|
80
|
+
matchesFull(f);
|
|
81
|
+
} finally { f.window.happyDOM.abort(); }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('a root feature used by descendant selectors keeps the subtree invalidation path', () => {
|
|
85
|
+
const f = fixture('<html><body><div class="target">text</div></body></html>', ['html.active .target { color:red }']);
|
|
86
|
+
try {
|
|
87
|
+
const root = f.document.documentElement;
|
|
88
|
+
root.className = 'active';
|
|
89
|
+
f.engine.compute(f.document, f.sheets, {}, new Set([root]), new Set([root]));
|
|
90
|
+
expect(f.engine.get(f.document.querySelector('.target')).color).toBe('rgba(255, 0, 0, 1)');
|
|
91
|
+
matchesFull(f);
|
|
92
|
+
} finally { f.window.happyDOM.abort(); }
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('root pseudo changes remain observable with an unchanged principal style', () => {
|
|
96
|
+
const f = fixture('<html><body><div>text</div></body></html>', ['html[data-check]::before { content:"ready";color:red }']);
|
|
97
|
+
try {
|
|
98
|
+
const root = f.document.documentElement;
|
|
99
|
+
root.setAttribute('data-check', 'ready');
|
|
100
|
+
f.engine.compute(f.document, f.sheets, {}, new Set(), new Set([root]));
|
|
101
|
+
expect(f.engine.getPseudo(root, 'before').content).toBe('ready');
|
|
102
|
+
matchesFull(f);
|
|
103
|
+
} finally { f.window.happyDOM.abort(); }
|
|
104
|
+
});
|
|
@@ -15,6 +15,41 @@ test("HTML comment markers in JavaScript values are escaped without changing the
|
|
|
15
15
|
expect(Function(`${escaped}; return [a, b.test('-->'), c]`)()).toEqual(["<!-- start -->", true, "<!-- 1 -->"]);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
+
test("SES preparation protects import property names while leaving dynamic imports to the loader", () => {
|
|
19
|
+
const source = `class Module {
|
|
20
|
+
static import(value) { return this.prefix + value }
|
|
21
|
+
static prefix = "class:";
|
|
22
|
+
import(value) { return value }
|
|
23
|
+
}
|
|
24
|
+
const object = { prefix: "object:", import(value) { return this.prefix + value } };
|
|
25
|
+
result = [Module . import ("a"), object. /* gap */ import /* gap */ ("b"),
|
|
26
|
+
Module.import.name, Object.hasOwn(Module, "import"), new Module().import(7)];`;
|
|
27
|
+
const code = escapeSesHtmlComments(source);
|
|
28
|
+
const result = Function(`let result; ${code}; return result`)();
|
|
29
|
+
expect(result).toEqual(["class:a", "object:b", "import", true, 7]);
|
|
30
|
+
expect(restoreSourceText(code)).toBe(source);
|
|
31
|
+
const dynamic = `import /* module */ ("./module.js"); import\n("./other.js");`;
|
|
32
|
+
expect(escapeSesHtmlComments(dynamic)).toBe(dynamic);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("SES preparation preserves import.meta alongside module methods and dynamic imports", async () => {
|
|
36
|
+
const source = `const registry = { import(value) { return value + 1 } };
|
|
37
|
+
export const meta = import.meta.url;
|
|
38
|
+
export const commentedMeta = import /* keyword */ .meta.url;
|
|
39
|
+
export const result = registry.import(6);
|
|
40
|
+
export const name = registry.import.name;
|
|
41
|
+
export const dynamic = (await import("data:text/javascript,export default 9")).default;`;
|
|
42
|
+
const code = escapeSesHtmlComments(source, "module");
|
|
43
|
+
expect(code).toContain("import.meta.url");
|
|
44
|
+
expect(code).toContain("import /* keyword */ .meta.url");
|
|
45
|
+
expect(restoreSourceText(code)).toBe(source);
|
|
46
|
+
const url = `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`;
|
|
47
|
+
const module = await import(url);
|
|
48
|
+
expect(module.meta).toContain("data:text/javascript;base64,");
|
|
49
|
+
expect(module.commentedMeta).toBe(module.meta);
|
|
50
|
+
expect([module.result, module.name, module.dynamic]).toEqual([7, "import", 9]);
|
|
51
|
+
});
|
|
52
|
+
|
|
18
53
|
test("constructor reads go through the helper; writes, delete and optional chains do not", () => {
|
|
19
54
|
expect(rewrite("var A = Object.getPrototypeOf(async function () {}).constructor;"))
|
|
20
55
|
.toBe("var A = $buninu$constructor(Object.getPrototypeOf(async function () {}));");
|
package/test/seccomp.test.js
CHANGED
|
@@ -7,6 +7,51 @@ const RET_ALLOW = 0x7fff0000;
|
|
|
7
7
|
const RET_EPERM = 0x00050001;
|
|
8
8
|
const RET_ENOSYS = 0x00050026;
|
|
9
9
|
|
|
10
|
+
// Execute the encoded classic-BPF instructions against a seccomp_data record.
|
|
11
|
+
// This checks actual jump destinations and argument loads, including the
|
|
12
|
+
// architecture we cannot install on the current test machine.
|
|
13
|
+
function filterVerdict(program, arch, syscall, args = []) {
|
|
14
|
+
const code = new DataView(encodeFilter(program)), data = new DataView(new ArrayBuffer(64));
|
|
15
|
+
data.setUint32(0, SYSCALL_NUMBERS[arch][syscall], true);
|
|
16
|
+
data.setUint32(4, arch === "x64" ? 0xc000003e : 0xc00000b7, true);
|
|
17
|
+
args.forEach((value, index) => data.setBigUint64(16 + index * 8, BigInt.asUintN(64, BigInt(value)), true));
|
|
18
|
+
let accumulator = 0, pc = 0;
|
|
19
|
+
for (let steps = 0; steps < program.length; steps++) {
|
|
20
|
+
const offset = pc * 8, op = code.getUint16(offset, true), value = code.getUint32(offset + 4, true);
|
|
21
|
+
const yes = code.getUint8(offset + 2), no = code.getUint8(offset + 3);
|
|
22
|
+
switch (op) {
|
|
23
|
+
case 0x20: accumulator = data.getUint32(value, true); pc++; break;
|
|
24
|
+
case 0x15: pc += 1 + (accumulator === value ? yes : no); break;
|
|
25
|
+
case 0x35: pc += 1 + (accumulator >= value ? yes : no); break;
|
|
26
|
+
case 0x45: pc += 1 + ((accumulator & value) !== 0 ? yes : no); break;
|
|
27
|
+
case RET: return value;
|
|
28
|
+
default: throw new Error(`unsupported BPF opcode ${op}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw new Error("filter did not return a verdict");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test("signal filters allow this process but never let tkill target an arbitrary thread", () => {
|
|
35
|
+
const pid = 1234;
|
|
36
|
+
for (const arch of ["x64", "arm64"]) {
|
|
37
|
+
// Even explicitly including the signal syscalls in the unconditional
|
|
38
|
+
// allowlist must not bypass their target checks.
|
|
39
|
+
for (const allowed of [ALLOWED_SYSCALLS, [...ALLOWED_SYSCALLS, "kill", "tkill", "tgkill"]]) {
|
|
40
|
+
const program = buildFilter(arch, pid, allowed);
|
|
41
|
+
for (const name of ["kill", "tkill", "tgkill"]) {
|
|
42
|
+
expect(filterVerdict(program, arch, name, [pid, 0, 0])).toBe(RET_ALLOW);
|
|
43
|
+
for (const other of [pid + 1, 1, 0, -1]) {
|
|
44
|
+
expect(filterVerdict(program, arch, name, [other, 0, 0])).toBe(RET_EPERM);
|
|
45
|
+
}
|
|
46
|
+
expect(program.filter(([op, , , value]) => op === 0x15 && value === SYSCALL_NUMBERS[arch][name])).toHaveLength(1);
|
|
47
|
+
}
|
|
48
|
+
// Exercise the syscall-number reload after the restricted signal blocks.
|
|
49
|
+
expect(filterVerdict(program, arch, "read")).toBe(RET_ALLOW);
|
|
50
|
+
expect(filterVerdict(program, arch, "openat")).toBe(RET_EPERM);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
10
55
|
test("builds an allowlist program that checks the architecture first and denies by default", () => {
|
|
11
56
|
for (const arch of ["x64", "arm64"]) {
|
|
12
57
|
const program = buildFilter(arch, 1234);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
|
|
3
|
+
test('spare warmup follows document load with bounded fallback and safe cancellation',async()=>{
|
|
4
|
+
const child=Bun.spawn([process.execPath,new URL('./fixtures/spare-renderer-scheduling.js',import.meta.url).pathname],{stdout:'pipe',stderr:'pipe'});
|
|
5
|
+
const [out,error,code]=await Promise.all([new Response(child.stdout).text(),new Response(child.stderr).text(),child.exited]);
|
|
6
|
+
expect({code,error}).toEqual({code:0,error:''});
|
|
7
|
+
expect(out).toContain('spare scheduling lifecycle, isolation, cancellation and failure checks passed');
|
|
8
|
+
},15000);
|