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,89 @@
|
|
|
1
|
+
import { afterAll, beforeAll, expect, test } from "bun:test";
|
|
2
|
+
import { RendererHost } from "../lib/renderer/host.js";
|
|
3
|
+
|
|
4
|
+
let renderer;
|
|
5
|
+
beforeAll(async () => {
|
|
6
|
+
renderer = new RendererHost({ timeout: 20_000 });
|
|
7
|
+
await renderer.start();
|
|
8
|
+
await renderer.call("loadDocument", {
|
|
9
|
+
url: "https://page.test/", source: "<!doctype html><p>idle callbacks</p>",
|
|
10
|
+
contentType: "text/html", status: 200,
|
|
11
|
+
});
|
|
12
|
+
await renderer.call("whenLoaded");
|
|
13
|
+
});
|
|
14
|
+
afterAll(() => renderer?.close());
|
|
15
|
+
const evaluate = (source) => renderer.call("evaluate", source);
|
|
16
|
+
|
|
17
|
+
test("idle work consumes its monotonic budget and yields", async () => {
|
|
18
|
+
const result = await evaluate(`new Promise(resolve => requestIdleCallback(deadline => {
|
|
19
|
+
const start = performance.now(), before = deadline.timeRemaining();
|
|
20
|
+
// Bound the regression: the old constant deadline must not hang the test.
|
|
21
|
+
while (deadline.timeRemaining() > 0 && performance.now() - start < 30) {}
|
|
22
|
+
resolve({before, after: deadline.timeRemaining(), didTimeout: deadline.didTimeout});
|
|
23
|
+
}))`);
|
|
24
|
+
expect(result.before).toBeGreaterThanOrEqual(0);
|
|
25
|
+
expect(result.before).toBeLessThanOrEqual(10);
|
|
26
|
+
expect(result.after).toBe(0);
|
|
27
|
+
expect(result.didTimeout).toBeFalse();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("a burst of idle work does not drain before an ordinary due timer", async () => {
|
|
31
|
+
const result = await evaluate(`new Promise(resolve => {
|
|
32
|
+
let completed = 0, whenTimer = null;
|
|
33
|
+
const finish = () => { if (completed === 16 && whenTimer !== null) resolve({completed, whenTimer}); };
|
|
34
|
+
for (let i = 0; i < 16; i++) requestIdleCallback(deadline => {
|
|
35
|
+
const start = performance.now();
|
|
36
|
+
while (deadline.timeRemaining() > 0 && performance.now() - start < 30) {}
|
|
37
|
+
completed++; finish();
|
|
38
|
+
});
|
|
39
|
+
setTimeout(() => { whenTimer = completed; finish(); }, 20);
|
|
40
|
+
})`);
|
|
41
|
+
expect(result.completed).toBe(16);
|
|
42
|
+
// Check task ordering, rather than imposing a real-clock upper bound on a
|
|
43
|
+
// slow CI machine. The old separate 1ms timers drain all sixteen first.
|
|
44
|
+
expect(result.whenTimer).toBeLessThan(16);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("an expired timeout has no idle budget", async () => {
|
|
48
|
+
const result = await evaluate(`new Promise(resolve => {
|
|
49
|
+
requestIdleCallback(deadline => resolve({
|
|
50
|
+
didTimeout: deadline.didTimeout, remaining: deadline.timeRemaining()
|
|
51
|
+
}), {timeout: 1});
|
|
52
|
+
const start = performance.now();
|
|
53
|
+
while (performance.now() - start < 20) {}
|
|
54
|
+
})`);
|
|
55
|
+
expect(result).toEqual({ didTimeout: true, remaining: 0 });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("zero timeout does not expire and cancellation prevents delivery", async () => {
|
|
59
|
+
const result = await evaluate(`new Promise(resolve => {
|
|
60
|
+
const calls = [];
|
|
61
|
+
const id = requestIdleCallback(() => calls.push("cancelled"), {timeout: 1});
|
|
62
|
+
cancelIdleCallback(id);
|
|
63
|
+
requestIdleCallback(deadline => {
|
|
64
|
+
calls.push(deadline.didTimeout);
|
|
65
|
+
setTimeout(() => resolve(calls), 10);
|
|
66
|
+
}, {timeout: 0});
|
|
67
|
+
const start = performance.now();
|
|
68
|
+
while (performance.now() - start < 10) {}
|
|
69
|
+
})`);
|
|
70
|
+
expect(result).toEqual([false]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("a timeout above the host timer limit does not expire immediately", async () => {
|
|
74
|
+
const result = await evaluate(`new Promise(resolve => {
|
|
75
|
+
requestIdleCallback(deadline => resolve({didTimeout: deadline.didTimeout}), {timeout: 2147483648});
|
|
76
|
+
})`);
|
|
77
|
+
expect(result).toEqual({ didTimeout: false });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("idle callbacks validate their Web IDL inputs", async () => {
|
|
81
|
+
expect(await evaluate(`(() => {
|
|
82
|
+
const failures = [];
|
|
83
|
+
for (const args of [[null], ["code"], [() => {}, 1], [() => {}, {timeout: Symbol()}]]) {
|
|
84
|
+
try { requestIdleCallback(...args); failures.push(false); }
|
|
85
|
+
catch (error) { failures.push(error.name === "TypeError"); }
|
|
86
|
+
}
|
|
87
|
+
return failures;
|
|
88
|
+
})()`)).toEqual([true, true, true, true]);
|
|
89
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
import { createIdleCallbacks } from '../lib/renderer/idle-callbacks.js';
|
|
3
|
+
|
|
4
|
+
function fixture() {
|
|
5
|
+
let time = 0, id = 0;
|
|
6
|
+
const tasks = new Map(), errors = [];
|
|
7
|
+
const clock = {
|
|
8
|
+
now: () => time,
|
|
9
|
+
consume: ms => { time += ms; },
|
|
10
|
+
setTimer(callback, delay) {
|
|
11
|
+
const handle = ++id;
|
|
12
|
+
tasks.set(handle, { callback, at: time + Math.max(1, delay) });
|
|
13
|
+
return handle;
|
|
14
|
+
},
|
|
15
|
+
clearTimer: handle => tasks.delete(handle),
|
|
16
|
+
run() {
|
|
17
|
+
let iterations = 0;
|
|
18
|
+
while (tasks.size) {
|
|
19
|
+
if (++iterations > 1000) throw new Error('Idle scheduler did not finish');
|
|
20
|
+
const [handle, task] = [...tasks].sort((a, b) => a[1].at - b[1].at || a[0] - b[0])[0];
|
|
21
|
+
tasks.delete(handle);
|
|
22
|
+
time = Math.max(time, task.at);
|
|
23
|
+
task.callback();
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const idle = createIdleCallbacks({ ...clock, invoke(callback, deadline) {
|
|
28
|
+
try { callback(deadline); } catch (error) { errors.push(error.message); }
|
|
29
|
+
} });
|
|
30
|
+
return { clock, idle, tasks, errors };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('a burst of cooperative idle work gives an already due timer a turn', () => {
|
|
34
|
+
const { clock, idle, tasks } = fixture();
|
|
35
|
+
let completed = 0, timer;
|
|
36
|
+
for (let i = 0; i < 16; i++) idle.request(i, deadline => {
|
|
37
|
+
expect(deadline.didTimeout).toBe(false);
|
|
38
|
+
clock.consume(deadline.timeRemaining());
|
|
39
|
+
expect(deadline.timeRemaining()).toBe(0);
|
|
40
|
+
completed++;
|
|
41
|
+
}, 0);
|
|
42
|
+
clock.setTimer(() => { timer = { at: clock.now(), completed }; }, 20);
|
|
43
|
+
// Sixteen requests share one dispatch task; they are not sixteen overdue timers.
|
|
44
|
+
expect(tasks.size).toBe(2);
|
|
45
|
+
clock.run();
|
|
46
|
+
expect(timer).toEqual({ at: 22, completed: 2 });
|
|
47
|
+
expect(completed).toBe(16);
|
|
48
|
+
expect(tasks.size).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('callbacks share their period deadline and nested requests wait for the next period', () => {
|
|
52
|
+
const { clock, idle } = fixture();
|
|
53
|
+
const seen = [];
|
|
54
|
+
idle.request(1, deadline => {
|
|
55
|
+
seen.push({ id: 1, at: clock.now(), budget: deadline.timeRemaining() });
|
|
56
|
+
idle.request(3, nested => seen.push({ id: 3, at: clock.now(), budget: nested.timeRemaining() }), 0);
|
|
57
|
+
clock.consume(3);
|
|
58
|
+
}, 0);
|
|
59
|
+
idle.request(2, deadline => seen.push({ id: 2, at: clock.now(), budget: deadline.timeRemaining() }), 0);
|
|
60
|
+
clock.run();
|
|
61
|
+
expect(seen).toEqual([{ id: 1, at: 1, budget: 10 }, { id: 2, at: 5, budget: 6 }, { id: 3, at: 11, budget: 10 }]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('positive timeout interrupts an idle backlog once, with no budget or retained timeout', () => {
|
|
65
|
+
const { clock, idle, tasks } = fixture();
|
|
66
|
+
let completed = 0;
|
|
67
|
+
const seen = [];
|
|
68
|
+
for (let i = 0; i < 16; i++) idle.request(i, deadline => {
|
|
69
|
+
clock.consume(deadline.timeRemaining()); completed++;
|
|
70
|
+
}, 0);
|
|
71
|
+
idle.request(20, deadline => seen.push({ at: clock.now(), completed, didTimeout: deadline.didTimeout, remaining: deadline.timeRemaining() }), 5);
|
|
72
|
+
clock.run();
|
|
73
|
+
expect(seen).toEqual([{ at: 11, completed: 1, didTimeout: true, remaining: 0 }]);
|
|
74
|
+
expect(completed).toBe(16);
|
|
75
|
+
expect(tasks.size).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('unsigned-long timeouts are chunked and rechecked before expiring', () => {
|
|
79
|
+
let time = 0, next = 0;
|
|
80
|
+
const timers = new Map(), seen = [];
|
|
81
|
+
const idle = createIdleCallbacks({
|
|
82
|
+
now: () => time,
|
|
83
|
+
setTimer(callback, delay) {
|
|
84
|
+
expect(delay).toBeGreaterThanOrEqual(1);
|
|
85
|
+
expect(delay).toBeLessThanOrEqual(2147483647);
|
|
86
|
+
timers.set(++next, { callback, delay });
|
|
87
|
+
return next;
|
|
88
|
+
},
|
|
89
|
+
clearTimer: id => timers.delete(id),
|
|
90
|
+
invoke: (callback, deadline) => callback(deadline),
|
|
91
|
+
});
|
|
92
|
+
idle.request(1, deadline => seen.push([deadline.didTimeout, deadline.timeRemaining()]), 4294967295);
|
|
93
|
+
const fire = (id, at) => {
|
|
94
|
+
time = at;
|
|
95
|
+
const task = timers.get(id);
|
|
96
|
+
timers.delete(id);
|
|
97
|
+
task.callback();
|
|
98
|
+
};
|
|
99
|
+
expect(timers.get(1).delay).toBe(2147483647);
|
|
100
|
+
// Exercise timeout tasks without running the idle-dispatch task: reaching a
|
|
101
|
+
// host timer's maximum delay is not the idle callback's expiration.
|
|
102
|
+
fire(1, 2147483647);
|
|
103
|
+
expect(seen).toEqual([]);
|
|
104
|
+
expect(timers.get(3).delay).toBe(2147483647);
|
|
105
|
+
fire(3, 4294967294);
|
|
106
|
+
expect(seen).toEqual([]);
|
|
107
|
+
expect(timers.get(4).delay).toBe(1);
|
|
108
|
+
fire(4, 4294967295);
|
|
109
|
+
expect(seen).toEqual([[true, 0]]);
|
|
110
|
+
expect(timers.size).toBe(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('cancellation works before dispatch, inside a period and for timeout tasks', () => {
|
|
114
|
+
const { clock, idle, tasks } = fixture();
|
|
115
|
+
const seen = [];
|
|
116
|
+
idle.request(1, () => seen.push(1), 1);
|
|
117
|
+
idle.cancel(1);
|
|
118
|
+
expect(tasks.size).toBe(0);
|
|
119
|
+
idle.request(2, () => { seen.push(2); idle.cancel(3); }, 0);
|
|
120
|
+
idle.request(3, () => seen.push(3), 3);
|
|
121
|
+
idle.request(4, () => seen.push(4), 50);
|
|
122
|
+
clock.run();
|
|
123
|
+
expect(seen).toEqual([2, 4]);
|
|
124
|
+
expect(clock.now()).toBeLessThan(50);
|
|
125
|
+
expect(tasks.size).toBe(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('disposal inside a callback clears remaining dispatch and timeout work', () => {
|
|
129
|
+
const { clock, idle, tasks } = fixture();
|
|
130
|
+
const seen = [];
|
|
131
|
+
idle.request(1, () => { seen.push(1); idle.dispose(); idle.request(3, () => seen.push(3), 1); }, 0);
|
|
132
|
+
idle.request(2, () => seen.push(2), 50);
|
|
133
|
+
clock.run();
|
|
134
|
+
expect(seen).toEqual([1]);
|
|
135
|
+
expect(tasks.size).toBe(0);
|
|
136
|
+
idle.dispose();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('reported callback errors leave subsequent idle work runnable', () => {
|
|
140
|
+
const { clock, idle, tasks, errors } = fixture();
|
|
141
|
+
let complete = false;
|
|
142
|
+
idle.request(1, () => { throw Error('page exception'); }, 0);
|
|
143
|
+
idle.request(2, () => { complete = true; }, 0);
|
|
144
|
+
clock.run();
|
|
145
|
+
expect(errors).toEqual(['page exception']);
|
|
146
|
+
expect(complete).toBe(true);
|
|
147
|
+
expect(tasks.size).toBe(0);
|
|
148
|
+
});
|
package/test/index.test.js
CHANGED
|
@@ -4,6 +4,7 @@ import { castyCommand, chooseFrontend } from "../index.js";
|
|
|
4
4
|
test("the first argument picks the frontend; anything else goes to casty whole", () => {
|
|
5
5
|
expect(chooseFrontend(["--casty", "example.com", "--", "--mobile"])).toEqual({ frontend: "casty", args: ["example.com", "--", "--mobile"] });
|
|
6
6
|
expect(chooseFrontend(["--headless", "--remote-debugging-port=9222"])).toEqual({ frontend: "headless", args: ["--remote-debugging-port=9222"] });
|
|
7
|
+
expect(chooseFrontend(["--win32", "example.com"])).toEqual({ frontend: "win32", args: ["example.com"] });
|
|
7
8
|
expect(chooseFrontend(["--gtk", "x"])).toEqual({ frontend: "gtk", args: ["x"], reserved: true });
|
|
8
9
|
expect(chooseFrontend(["github.com", "--", "--mobile"])).toEqual({ frontend: "casty", args: ["github.com", "--", "--mobile"] });
|
|
9
10
|
expect(chooseFrontend(["--mobile"])).toEqual({ frontend: "casty", args: ["--mobile"] });
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
import { parseHTMLDocument } from '../lib/happy-dom/parser.js';
|
|
3
|
+
import { computeElementStyle, parseDeclarations, StyleEngine } from '../lib/style/computed-style.js';
|
|
4
|
+
|
|
5
|
+
test('inline importance and variables reach principal styles without leaking into pseudos', () => {
|
|
6
|
+
const { document, window } = parseHTMLDocument('<div id="target" style="--accent:blue!important;--accent:orange;color:var(--accent);margin:2px;margin-left:7px!important;content:\'principal\'">text</div>');
|
|
7
|
+
const sheets = [
|
|
8
|
+
'#target {--accent:red!important;color:purple!important;margin-left:9px!important;background-color:var(--accent)}',
|
|
9
|
+
'#target::before {--accent:gold;content:"before";color:var(--accent);margin-left:inherit}',
|
|
10
|
+
];
|
|
11
|
+
try {
|
|
12
|
+
const target = document.getElementById('target');
|
|
13
|
+
const engine = new StyleEngine().compute(document, sheets);
|
|
14
|
+
const original = engine.get(target), originalCustom = engine.customPropertiesOf(target);
|
|
15
|
+
expect(original).toMatchObject({
|
|
16
|
+
color:'rgba(128, 0, 128, 1)', backgroundColor:'rgba(0, 0, 255, 1)', margin:[2,2,2,7], content:'principal',
|
|
17
|
+
});
|
|
18
|
+
expect(originalCustom.get('--accent')).toBe('blue');
|
|
19
|
+
expect(engine.getPseudo(target, 'before')).toMatchObject({ content:'before', color:'rgba(255, 215, 0, 1)', margin:[0,0,0,7] });
|
|
20
|
+
|
|
21
|
+
target.setAttribute('style', '--accent:lime!important;color:var(--accent)!important;margin:4px;margin-left:8px!important');
|
|
22
|
+
engine.compute(document, sheets, {}, new Set([target]));
|
|
23
|
+
expect(engine.get(target)).toMatchObject({
|
|
24
|
+
color:'rgba(0, 255, 0, 1)', backgroundColor:'rgba(0, 255, 0, 1)', margin:[4,4,4,8], content:null,
|
|
25
|
+
});
|
|
26
|
+
expect(engine.customPropertiesOf(target).get('--accent')).toBe('lime');
|
|
27
|
+
expect(engine.getPseudo(target, 'before')).toMatchObject({ content:'before', color:'rgba(255, 215, 0, 1)', margin:[0,0,0,8] });
|
|
28
|
+
expect(original.backgroundColor).toBe('rgba(0, 0, 255, 1)');
|
|
29
|
+
expect(originalCustom.get('--accent')).toBe('blue');
|
|
30
|
+
} finally { window.happyDOM.abort(); }
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('inline shorthands retain their priority over UA defaults and dimension hints', () => {
|
|
34
|
+
const { document, window } = parseHTMLDocument('<h1 id="heading" style="font:italic 20px serif;margin:3px">heading</h1><img id="picture" width="17" height="19" style="width:33px;height:44px">');
|
|
35
|
+
try {
|
|
36
|
+
const engine = new StyleEngine().compute(document, ['#picture {width:80px!important}']);
|
|
37
|
+
expect(engine.get(document.getElementById('heading'))).toMatchObject({
|
|
38
|
+
fontSize:20, fontWeight:400, fontStyle:'italic', fontFamily:['serif'], margin:[3,3,3,3],
|
|
39
|
+
});
|
|
40
|
+
expect(engine.get(document.getElementById('picture'))).toMatchObject({ width:80, height:44 });
|
|
41
|
+
} finally { window.happyDOM.abort(); }
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('direct computed-style calls still cascade inline values without mutating supplied declarations', () => {
|
|
45
|
+
const { document, window } = parseHTMLDocument('<div style="color:blue;width:31px;padding:2px">text</div>');
|
|
46
|
+
try {
|
|
47
|
+
const element = document.querySelector('div');
|
|
48
|
+
const declarations = parseDeclarations('color:purple!important;width:10px');
|
|
49
|
+
const originalWidth = declarations.get('width');
|
|
50
|
+
const first = computeElementStyle(element, null, declarations);
|
|
51
|
+
expect(first).toMatchObject({ color:'rgba(128, 0, 128, 1)', width:31, padding:[2,2,2,2] });
|
|
52
|
+
expect(declarations.size).toBe(2);
|
|
53
|
+
expect(declarations.get('width')).toBe(originalWidth);
|
|
54
|
+
expect(originalWidth.value).toBe('10px');
|
|
55
|
+
|
|
56
|
+
declarations.set('color', { ...declarations.get('color'), value:'green' });
|
|
57
|
+
element.setAttribute('style', 'color:blue;width:41px;padding:4px');
|
|
58
|
+
const second = computeElementStyle(element, null, declarations);
|
|
59
|
+
expect(second).toMatchObject({ color:'rgba(0, 128, 0, 1)', width:41, padding:[4,4,4,4] });
|
|
60
|
+
expect(first.width).toBe(31);
|
|
61
|
+
expect(first.color).toBe('rgba(128, 0, 128, 1)');
|
|
62
|
+
} finally { window.happyDOM.abort(); }
|
|
63
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
test('paint-only geometry reads skip layout and match full-layout pixels', async () => {
|
|
4
|
+
const child = Bun.spawn([process.execPath, new URL('./fixtures/layout-repaint.js', import.meta.url).pathname],
|
|
5
|
+
{ stdout: 'pipe', stderr: 'pipe' });
|
|
6
|
+
const [exit, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]);
|
|
7
|
+
expect(stderr).toBe('');
|
|
8
|
+
expect(exit).toBe(0);
|
|
9
|
+
}, 15_000);
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import {test, expect} from 'bun:test';
|
|
2
|
+
import {parseHTMLDocument} from '../lib/happy-dom/parser.js';
|
|
3
|
+
import {StyleEngine} from '../lib/style/computed-style.js';
|
|
4
|
+
import {RenderTreeBuilder} from '../lib/render-tree/index.js';
|
|
5
|
+
import {LayoutEngine} from '../lib/layout/text-layout.js';
|
|
6
|
+
import {buildDisplayList} from '../lib/paint/display-list.js';
|
|
7
|
+
import {repaintLayout} from '../lib/layout/repaint.js';
|
|
8
|
+
|
|
9
|
+
function fixture(source='<body><div id="a" style="width:100px;height:30px;transform:scaleX(0.3);background-image:linear-gradient(red, blue);background-size:20px 20px">hello <span>world</span></div><p>after</p></body>') {
|
|
10
|
+
const parsed=parseHTMLDocument(source);
|
|
11
|
+
const builder=new RenderTreeBuilder(),engine=new LayoutEngine();
|
|
12
|
+
const first=builder.build(parsed.document,new StyleEngine().compute(parsed.document));
|
|
13
|
+
const options={width:300,viewportHeight:200};
|
|
14
|
+
const layout=engine.layout(first,options);
|
|
15
|
+
return {...parsed,builder,engine,first,layout,options,element:parsed.document.getElementById('a')};
|
|
16
|
+
}
|
|
17
|
+
for (const [name,value] of [['transform','scaleX(0.8)'],['backgroundSize','40px 15px'],['transformOrigin','left top'],['opacity','0.4'],['backgroundColor','blue']]) {
|
|
18
|
+
test(`retained ${name} paint matches a complete layout, leaving prior geometry immutable`,()=>{
|
|
19
|
+
const f=fixture();
|
|
20
|
+
try {
|
|
21
|
+
const before=JSON.stringify(f.layout);
|
|
22
|
+
f.element.style[name]=value;
|
|
23
|
+
const next=f.builder.build(f.document,new StyleEngine().compute(f.document)),retained=repaintLayout(f.layout,f.first,next);
|
|
24
|
+
expect(retained).not.toBeNull();
|
|
25
|
+
expect(buildDisplayList(retained,next)).toEqual(buildDisplayList(f.engine.layout(next,f.options),next));
|
|
26
|
+
expect(JSON.stringify(f.layout)).toBe(before);
|
|
27
|
+
const unchanged=f.layout.fragments.findIndex(fragment=>fragment.runs.some(run=>run.text==='after'));
|
|
28
|
+
expect(unchanged).toBeGreaterThanOrEqual(0);
|
|
29
|
+
expect(retained.fragments[unchanged]).toBe(f.layout.fragments[unchanged]);
|
|
30
|
+
expect(retained.fragments[unchanged].runs).toBe(f.layout.fragments[unchanged].runs);
|
|
31
|
+
} finally {f.window.happyDOM.abort();}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
for (const [name,value] of [['transform','translateX(10px) scaleX(0.8)'],['transform','none'],['width','120px'],['color','red'],['display','none']]) {
|
|
35
|
+
test(`changing ${name} outside the paint-only subset retains full layout`,()=>{
|
|
36
|
+
const f=fixture();try {f.element.style[name]=value;expect(repaintLayout(f.layout,f.first,f.builder.build(f.document,new StyleEngine().compute(f.document)))).toBeNull();}
|
|
37
|
+
finally{f.window.happyDOM.abort();}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
test('text changes cannot reuse old line breaking',()=>{
|
|
41
|
+
const f=fixture();try{f.element.firstChild.data='a much longer paragraph';expect(repaintLayout(f.layout,f.first,f.builder.build(f.document,new StyleEngine().compute(f.document)))).toBeNull();}
|
|
42
|
+
finally{f.window.happyDOM.abort();}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function expectEquivalent(f, next, retained) {
|
|
46
|
+
expect(retained).not.toBeNull();
|
|
47
|
+
const full = new LayoutEngine().layout(next, f.options);
|
|
48
|
+
// Fragment IDs retain the generation in which their geometry was created.
|
|
49
|
+
const comparable = layout => ({ ...layout, fragments: layout.fragments.map(({ id, ...fragment }) => fragment) });
|
|
50
|
+
expect(comparable(retained)).toEqual(comparable(full));
|
|
51
|
+
const painted = buildDisplayList(retained, next), reference = buildDisplayList(full, next);
|
|
52
|
+
expect(painted).toEqual(reference);
|
|
53
|
+
// Hit testing, text selection, and scroll extents are non-enumerable.
|
|
54
|
+
expect(painted.hits).toEqual(reference.hits);
|
|
55
|
+
expect(painted.scrollers).toEqual(reference.scrollers);
|
|
56
|
+
expect(painted.extent).toEqual(reference.extent);
|
|
57
|
+
return painted;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test('ancestor opacity updates nested positioned content and survives zero opacity', () => {
|
|
61
|
+
const f = fixture(`<body><div id="a" style="position:relative;z-index:2;width:100px;height:70px;background:red">
|
|
62
|
+
text <span style="opacity:.5;background:yellow">faint</span>
|
|
63
|
+
<span style="position:absolute;z-index:-1;left:2px;top:4px;background:blue">behind</span>
|
|
64
|
+
<span style="position:absolute;z-index:3;left:8px;top:15px;background:green">above</span>
|
|
65
|
+
</div><div style="position:relative;z-index:1">outside</div></body>`);
|
|
66
|
+
try {
|
|
67
|
+
let previous = f.first, layout = f.layout;
|
|
68
|
+
for (const opacity of ['0.5', '0', '1']) {
|
|
69
|
+
f.element.style.opacity = opacity;
|
|
70
|
+
const next = f.builder.build(f.document, new StyleEngine().compute(f.document));
|
|
71
|
+
const retained = repaintLayout(layout, previous, next);
|
|
72
|
+
const list = expectEquivalent(f, next, retained);
|
|
73
|
+
const faint = list.items.find(item => item.text === 'faint');
|
|
74
|
+
if (opacity === '0') expect(faint).toBeUndefined();
|
|
75
|
+
else expect(faint.opacity).toBe(Number(opacity) * 0.5);
|
|
76
|
+
for (let index = 0; index < retained.boxes.length; index++) {
|
|
77
|
+
expect(retained.boxes[index].layer).toBe(layout.boxes[index].layer);
|
|
78
|
+
}
|
|
79
|
+
previous = next;
|
|
80
|
+
layout = retained;
|
|
81
|
+
}
|
|
82
|
+
} finally { f.window.happyDOM.abort(); }
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
for (const [description, content] of [
|
|
86
|
+
['wrapped inline', '<div style="width:90px">before <span id="a" style="background:red">several wrapped words</span> after</div>'],
|
|
87
|
+
['bordered inline', '<div><span id="a" style="border:1px solid currentColor">text</span></div>'],
|
|
88
|
+
['bordered image', '<div><img id="a" width="30" height="20" style="border:1px solid currentColor"></div>'],
|
|
89
|
+
['table cell with resolved padding', '<table style="width:140px"><tr><td id="a" style="padding:10%;background:red">cell</td><td>other</td></tr></table>'],
|
|
90
|
+
['block with anonymous lines', '<div id="a" style="padding:5%">before<p>middle</p>after</div>'],
|
|
91
|
+
]) {
|
|
92
|
+
test(`background color retains ${description} geometry and paint`, () => {
|
|
93
|
+
const f = fixture(`<body>${content}</body>`);
|
|
94
|
+
try {
|
|
95
|
+
f.element.style.backgroundColor = 'blue';
|
|
96
|
+
const next = f.builder.build(f.document, new StyleEngine().compute(f.document));
|
|
97
|
+
expectEquivalent(f, next, repaintLayout(f.layout, f.first, next));
|
|
98
|
+
} finally { f.window.happyDOM.abort(); }
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const [description, content, color] of [
|
|
103
|
+
['new inline background', '<span id="a">text</span>', 'red'],
|
|
104
|
+
['removed inline background', '<span id="a" style="background:red">text</span>', 'transparent'],
|
|
105
|
+
['new image background', '<img id="a" width="30" height="20">', 'red'],
|
|
106
|
+
['removed image background', '<img id="a" width="30" height="20" style="background:red">', 'transparent'],
|
|
107
|
+
]) {
|
|
108
|
+
test(`${description} regenerates inline decoration boxes`, () => {
|
|
109
|
+
const f = fixture(`<body><div>${content}</div></body>`);
|
|
110
|
+
try {
|
|
111
|
+
f.element.style.backgroundColor = color;
|
|
112
|
+
const next = f.builder.build(f.document, new StyleEngine().compute(f.document));
|
|
113
|
+
expect(repaintLayout(f.layout, f.first, next)).toBeNull();
|
|
114
|
+
} finally { f.window.happyDOM.abort(); }
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
test('retained styles preserve gradient text identity across repeated paint changes', () => {
|
|
119
|
+
const f = fixture('<body><div id="a" style="width:100px;background-image:linear-gradient(red,blue);background-clip:text">gradient text</div></body>');
|
|
120
|
+
try {
|
|
121
|
+
let previous = f.first, layout = f.layout;
|
|
122
|
+
for (const [property, value] of [['opacity', '.5'], ['background-size', '40px 20px'], ['background-color', 'green']]) {
|
|
123
|
+
f.element.setAttribute('style', `${f.element.getAttribute('style')};${property}:${value}`);
|
|
124
|
+
const next = f.builder.build(f.document, new StyleEngine().compute(f.document));
|
|
125
|
+
const retained = repaintLayout(layout, previous, next);
|
|
126
|
+
const list = expectEquivalent(f, next, retained);
|
|
127
|
+
expect(list.items.filter(item => item.op === 'drawText').every(item => item.gradient)).toBe(true);
|
|
128
|
+
previous = next;
|
|
129
|
+
layout = retained;
|
|
130
|
+
}
|
|
131
|
+
} finally { f.window.happyDOM.abort(); }
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('color changes with inherited currentColor borders, decorations and SVG keep full layout', () => {
|
|
135
|
+
const f = fixture(`<body><div id="a" style="color:red;text-decoration:underline">
|
|
136
|
+
<span style="border:1px solid currentColor">text</span>
|
|
137
|
+
<svg width="20" height="20"><rect width="20" height="20" fill="currentColor"/></svg>
|
|
138
|
+
</div></body>`);
|
|
139
|
+
try {
|
|
140
|
+
f.element.style.color = 'blue';
|
|
141
|
+
const next = f.builder.build(f.document, new StyleEngine().compute(f.document));
|
|
142
|
+
expect(repaintLayout(f.layout, f.first, next)).toBeNull();
|
|
143
|
+
} finally { f.window.happyDOM.abort(); }
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
for (const otherOpacity of [null, '0.7', '0.3']) {
|
|
147
|
+
test(`a shared old style cannot leak into an alias with opacity ${otherOpacity ?? 'unchanged'}`, () => {
|
|
148
|
+
const f = fixture('<body><div id="a" style="width:100px">first</div><div id="b" style="width:100px">second</div></body>');
|
|
149
|
+
try {
|
|
150
|
+
const a = [...f.first.nodesById.values()].find(node => node.domNode === f.element);
|
|
151
|
+
const other = f.document.getElementById('b');
|
|
152
|
+
const b = [...f.first.nodesById.values()].find(node => node.domNode === other);
|
|
153
|
+
expect(a.style).toEqual(b.style);
|
|
154
|
+
// Render trees may share immutable styles. Give both elements and their
|
|
155
|
+
// text nodes that same style, then split it in the next generation.
|
|
156
|
+
const nodesById = new Map();
|
|
157
|
+
const share = node => {
|
|
158
|
+
const shared = Object.freeze({ ...node,
|
|
159
|
+
style: node.style === b.style ? a.style : node.style,
|
|
160
|
+
children: Object.freeze(node.children.map(share)),
|
|
161
|
+
});
|
|
162
|
+
nodesById.set(shared.id, shared);
|
|
163
|
+
return shared;
|
|
164
|
+
};
|
|
165
|
+
const previous = { ...f.first, root: share(f.first.root), nodesById };
|
|
166
|
+
const layout = new LayoutEngine().layout(previous, f.options);
|
|
167
|
+
f.element.style.opacity = '0.3';
|
|
168
|
+
if (otherOpacity != null) other.style.opacity = otherOpacity;
|
|
169
|
+
const next = f.builder.build(f.document, new StyleEngine().compute(f.document));
|
|
170
|
+
const retained = repaintLayout(layout, previous, next);
|
|
171
|
+
if (otherOpacity === '0.3') expectEquivalent(f, next, retained);
|
|
172
|
+
else expect(retained).toBeNull();
|
|
173
|
+
} finally { f.window.happyDOM.abort(); }
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
test('a changed run only replaces its own immutable fragment and preserves sibling runs',()=>{
|
|
177
|
+
const f=fixture('<body><div style="width:280px">before <span id="a" style="background-size:20px 20px">target</span> after</div><p>outside</p></body>');
|
|
178
|
+
try {
|
|
179
|
+
const before=JSON.stringify(f.layout);
|
|
180
|
+
const index=f.layout.fragments.findIndex(fragment=>fragment.runs.some(run=>run.text==='target'));
|
|
181
|
+
const original=f.layout.fragments[index];
|
|
182
|
+
expect(original.runs.length).toBeGreaterThan(1);
|
|
183
|
+
f.element.style.backgroundSize='40px 15px';
|
|
184
|
+
const next=f.builder.build(f.document,new StyleEngine().compute(f.document)),retained=repaintLayout(f.layout,f.first,next);
|
|
185
|
+
expect(retained).not.toBeNull();
|
|
186
|
+
const updated=retained.fragments[index];
|
|
187
|
+
expect(updated).not.toBe(original);
|
|
188
|
+
expect(updated.runs).not.toBe(original.runs);
|
|
189
|
+
for(let runIndex=0;runIndex<original.runs.length;runIndex++) {
|
|
190
|
+
const run=original.runs[runIndex],replacement=updated.runs[runIndex];
|
|
191
|
+
if(run.text==='target') {
|
|
192
|
+
expect(replacement).not.toBe(run);
|
|
193
|
+
expect(Object.isFrozen(replacement)).toBe(true);
|
|
194
|
+
expect(Object.isFrozen(replacement.style)).toBe(true);
|
|
195
|
+
} else expect(replacement).toBe(run);
|
|
196
|
+
}
|
|
197
|
+
expect(Object.isFrozen(updated)).toBe(true);
|
|
198
|
+
expect(Object.isFrozen(updated.runs)).toBe(true);
|
|
199
|
+
expect(Object.isFrozen(retained.fragments)).toBe(true);
|
|
200
|
+
expect(buildDisplayList(retained,next)).toEqual(buildDisplayList(f.engine.layout(next,f.options),next));
|
|
201
|
+
expect(JSON.stringify(f.layout)).toBe(before);
|
|
202
|
+
} finally {f.window.happyDOM.abort();}
|
|
203
|
+
});
|
|
204
|
+
test('box-only and no-op paint updates keep unchanged fragment and record arrays',()=>{
|
|
205
|
+
const f=fixture('<body><div id="a" style="width:100px;height:30px;background-size:20px 20px"></div><p>after</p></body>');
|
|
206
|
+
try {
|
|
207
|
+
const before=JSON.stringify(f.layout);
|
|
208
|
+
f.element.style.backgroundSize='40px 15px';
|
|
209
|
+
const next=f.builder.build(f.document,new StyleEngine().compute(f.document)),retained=repaintLayout(f.layout,f.first,next);
|
|
210
|
+
expect(retained).not.toBeNull();
|
|
211
|
+
expect(retained.boxes).not.toBe(f.layout.boxes);
|
|
212
|
+
expect(Object.isFrozen(retained.boxes)).toBe(true);
|
|
213
|
+
expect(retained.fragments).toBe(f.layout.fragments);
|
|
214
|
+
expect(buildDisplayList(retained,next)).toEqual(buildDisplayList(f.engine.layout(next,f.options),next));
|
|
215
|
+
expect(JSON.stringify(f.layout)).toBe(before);
|
|
216
|
+
const unchanged=f.builder.build(f.document,new StyleEngine().compute(f.document));
|
|
217
|
+
const again=repaintLayout(retained,next,unchanged);
|
|
218
|
+
expect(again.boxes).toBe(retained.boxes);
|
|
219
|
+
expect(again.fragments).toBe(retained.fragments);
|
|
220
|
+
expect(again.generation).toBe(unchanged.generation);
|
|
221
|
+
} finally {f.window.happyDOM.abort();}
|
|
222
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
for (const scenario of ['success', 'pause', 'controls', 'seek', 'source', 'format', 'worker-failure', 'epoch', 'no-growth']) {
|
|
4
|
+
test(`presentation continuation: ${scenario}`, async () => {
|
|
5
|
+
const child = Bun.spawn([process.execPath,
|
|
6
|
+
new URL('./fixtures/media-presentation-continuation.js', import.meta.url).pathname, scenario],
|
|
7
|
+
{ stdout: 'pipe', stderr: 'pipe' });
|
|
8
|
+
const [out, error, code] = await Promise.all([
|
|
9
|
+
new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited,
|
|
10
|
+
]);
|
|
11
|
+
expect({ code, error }).toEqual({ code: 0, error: '' });
|
|
12
|
+
expect(out).toContain(`continuation ${scenario} passed`);
|
|
13
|
+
}, 15000);
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
|
|
3
|
+
test('presentation refill avoids duplicate page decoding and retains local fallbacks',async()=>{
|
|
4
|
+
const child=Bun.spawn([process.execPath,new URL('./fixtures/media-presentation-refill.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('presentation refill avoids duplicate page decode and preserves fallbacks');
|
|
8
|
+
},15000);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
for (const scenario of ['initial-shared', 'initial-growth', 'initial-source', 'initial-rejected', 'audio-only',
|
|
4
|
+
'refresh-growth', 'pipe-change', 'failed-retry', 'mime-change', 'epoch', 'epoch-inflight', 'moving-snapshot']) {
|
|
5
|
+
test(`media metadata probe: ${scenario}`, async () => {
|
|
6
|
+
const child = Bun.spawn([process.execPath, new URL('./fixtures/media-probe.js', import.meta.url).pathname, scenario],
|
|
7
|
+
{ stdout: 'pipe', stderr: 'pipe' });
|
|
8
|
+
try {
|
|
9
|
+
const [out, error, code] = await Promise.all([
|
|
10
|
+
new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited,
|
|
11
|
+
]);
|
|
12
|
+
expect({ code, error }).toEqual({ code: 0, error: '' });
|
|
13
|
+
expect(out).toContain(`media probe ${scenario} passed`);
|
|
14
|
+
} finally { child.kill(); }
|
|
15
|
+
}, 10000);
|
|
16
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
|
|
3
|
+
test('independent media coalesces page UI painting while pause, seek and native controls remain live',async()=>{
|
|
4
|
+
const child=Bun.spawn([process.execPath,new URL('./fixtures/media-repaint.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('media repaint integration passed');
|
|
8
|
+
},15000);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {expect, test} from 'bun:test';
|
|
2
|
+
|
|
3
|
+
test('only the currently attached MediaSource can change media state across src, srcObject and documents', async () => {
|
|
4
|
+
const child=Bun.spawn([process.execPath,new URL('./fixtures/media-source-attachment.js',import.meta.url).pathname],{
|
|
5
|
+
stdout:'pipe',stderr:'pipe',
|
|
6
|
+
});
|
|
7
|
+
const [out,error,code]=await Promise.all([
|
|
8
|
+
new Response(child.stdout).text(),new Response(child.stderr).text(),child.exited,
|
|
9
|
+
]);
|
|
10
|
+
expect({code,error}).toEqual({code:0,error:''});
|
|
11
|
+
expect(out).toContain('MediaSource attachment isolation passed');
|
|
12
|
+
},15000);
|
|
13
|
+
|
|
14
|
+
for(const action of ['src','attribute','load'])test(`explicit ${action} reselects media while passive base changes preserve it`,async()=>{
|
|
15
|
+
const child=Bun.spawn([process.execPath,new URL('./fixtures/media-resource-reselection.js',import.meta.url).pathname,action],{
|
|
16
|
+
stdout:'pipe',stderr:'pipe',
|
|
17
|
+
});
|
|
18
|
+
const [out,error,code]=await Promise.all([
|
|
19
|
+
new Response(child.stdout).text(),new Response(child.stderr).text(),child.exited,
|
|
20
|
+
]);
|
|
21
|
+
expect({code,error}).toEqual({code:0,error:''});
|
|
22
|
+
expect(out).toContain(`Media resource reselection ${action} passed`);
|
|
23
|
+
},15000);
|
|
24
|
+
|
|
25
|
+
test('MediaSource attachment queues sourceopen without a playback decoder', async () => {
|
|
26
|
+
const child=Bun.spawn([process.execPath,new URL('./fixtures/media-source-no-decoder.js',import.meta.url).pathname],{
|
|
27
|
+
stdout:'pipe',stderr:'pipe',
|
|
28
|
+
});
|
|
29
|
+
const [out,error,code]=await Promise.all([
|
|
30
|
+
new Response(child.stdout).text(),new Response(child.stderr).text(),child.exited,
|
|
31
|
+
]);
|
|
32
|
+
expect({code,error}).toEqual({code:0,error:''});
|
|
33
|
+
expect(out).toContain('Decoder-less MediaSource attachment passed');
|
|
34
|
+
},15000);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {expect, test} from 'bun:test';
|
|
2
|
+
|
|
3
|
+
test('video-only refills retain audio until its own EOF or epoch boundary, including pending output requests', async () => {
|
|
4
|
+
const child = Bun.spawn([process.execPath, new URL('./fixtures/media-track-refill.js', import.meta.url).pathname], {
|
|
5
|
+
stdout:'pipe', stderr:'pipe',
|
|
6
|
+
});
|
|
7
|
+
const [out, error, code] = await Promise.all([
|
|
8
|
+
new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited,
|
|
9
|
+
]);
|
|
10
|
+
expect({code, error}).toEqual({code:0, error:''});
|
|
11
|
+
expect(out).toContain('track-independent refill integration passed');
|
|
12
|
+
}, 15_000);
|
|
13
|
+
|
|
14
|
+
for (const action of ['seek', 'pause', 'navigate']) {
|
|
15
|
+
test(`a failed audio refill cancelled by ${action} does not fail the current playback`, async () => {
|
|
16
|
+
const child = Bun.spawn([process.execPath,
|
|
17
|
+
new URL('./fixtures/media-refill-cancel.js', import.meta.url).pathname, action], {
|
|
18
|
+
stdout:'pipe', stderr:'pipe',
|
|
19
|
+
});
|
|
20
|
+
const [out, error, code] = await Promise.all([
|
|
21
|
+
new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited,
|
|
22
|
+
]);
|
|
23
|
+
expect({code, error}).toEqual({code:0, error:''});
|
|
24
|
+
expect(out).toContain(`${action} ignores an obsolete refill rejection`);
|
|
25
|
+
}, 15_000);
|
|
26
|
+
}
|