buninu-browser 0.7.0 → 0.9.1
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/AGENTS.md +5 -0
- package/README.md +90 -1
- package/buninu-browser.js +1 -1
- package/frontends/wayland/README.md +35 -2
- package/frontends/wayland/client.js +8 -6
- package/frontends/wayland/clipboard.js +1 -1
- package/frontends/wayland/index.js +1 -1
- package/frontends/wayland/input.js +1 -1
- package/frontends/wayland/resource-io.js +6 -0
- package/frontends/wayland/transport-posix.js +120 -0
- package/frontends/wayland/transport-win32.js +9 -0
- package/frontends/wayland/transport.js +3 -120
- package/frontends/wayland/win32-resources.js +80 -0
- package/frontends/wayland/win32-wire.js +62 -0
- package/lib/audio/biquad.js +25 -0
- package/lib/audio/piano.js +6 -3
- package/lib/audio/playback-buffer.js +5 -5
- package/lib/audio/pulse-client.js +24 -5
- package/lib/audio/web-audio.js +570 -0
- package/lib/browser-dialog.js +126 -0
- package/lib/cdp-server.js +43 -11
- package/lib/file-picker.js +48 -0
- package/lib/headless-shell.js +34 -12
- package/lib/layout/text-layout.js +112 -19
- package/lib/media/mp4-frame-times.js +93 -0
- package/lib/paint/direct-canvas.ts +39 -0
- package/lib/paint/display-list.js +37 -12
- package/lib/paint/raster-readback.js +16 -7
- package/lib/renderer/animation-frames.js +10 -5
- package/lib/renderer/bindings.js +69 -16
- package/lib/renderer/canvas-2d.js +265 -15
- package/lib/renderer/canvas-round-rect.js +36 -0
- package/lib/renderer/css-animations.js +173 -0
- package/lib/renderer/dialog-client.js +30 -0
- package/lib/renderer/dialog-worker.js +35 -0
- package/lib/renderer/eval-environment.js +28 -0
- package/lib/renderer/file-input.js +49 -0
- package/lib/renderer/host.js +33 -4
- package/lib/renderer/html-drag.js +77 -0
- package/lib/renderer/image-loading.js +45 -2
- package/lib/renderer/main.js +4 -0
- package/lib/renderer/module-transform.js +5 -3
- package/lib/renderer/page-builtins.js +59 -0
- package/lib/renderer/page-realm.js +177 -40
- package/lib/renderer/page-renderer.js +397 -49
- package/lib/renderer/piano-input.js +3 -1
- package/lib/renderer/raster-surface.js +18 -0
- package/lib/renderer/script-rewrite.js +165 -9
- package/lib/renderer/video-presentation.js +5 -4
- package/lib/renderer/webgl/clip.js +24 -0
- package/lib/renderer/webgl/context.js +158 -0
- package/lib/renderer/webgl/cpu-fragment.js +211 -0
- package/lib/renderer/webgl/draw-cache.js +22 -0
- package/lib/renderer/webgl/pipeline.js +70 -0
- package/lib/renderer/webgl/shaders.js +58 -0
- package/lib/renderer/webgl/textures.js +43 -0
- package/lib/renderer/window-named-properties.js +46 -0
- package/lib/screenshot-cli.js +4 -7
- package/lib/style/animation-values.js +29 -0
- package/lib/style/computed-style.js +109 -13
- package/lib/style/transform.js +44 -0
- package/lib/window-size.js +10 -0
- package/package.json +1 -1
- package/patches/happy-dom/0007-scope-content-models-to-html-namespace.patch +29 -0
- package/patches/happy-dom/README.md +16 -0
- package/test/animation-frames.test.js +15 -0
- package/test/canvas-2d.test.js +252 -4
- package/test/canvas-frame-pacing.test.js +7 -0
- package/test/cdp-viewers.test.js +36 -0
- package/test/classic-games-patches/0001-fix-game-initialization.patch +177 -0
- package/test/classic-games-patches/README.md +156 -0
- package/test/classic-games-scenarios.js +133 -0
- package/test/clipboard.test.js +16 -0
- package/test/computed-style.test.js +18 -0
- package/test/css-animation-lifecycle.test.js +28 -0
- package/test/css-animations.test.js +83 -0
- package/test/direct-canvas.test.js +39 -0
- package/test/dynamic-inline-handlers.test.js +25 -0
- package/test/eval-declarations.test.js +22 -0
- package/test/file-picker.test.js +34 -0
- package/test/fixtures/canvas-damage.js +23 -0
- package/test/fixtures/canvas-frame-pacing.js +35 -0
- package/test/fixtures/direct-canvas.js +96 -0
- package/test/fixtures/javascript-dialogs.html +8 -0
- package/test/fixtures/media-audio-lifetime.js +65 -0
- package/test/fixtures/renderer-memory.js +7 -5
- package/test/fixtures/video-black-replay.js +8 -6
- package/test/fixtures/video-slow-end.js +39 -0
- package/test/fixtures/web-audio-renderer.js +45 -0
- package/test/flex.test.js +26 -0
- package/test/grid.test.js +13 -0
- package/test/headless-shell.test.js +9 -0
- package/test/headless-window-size.test.js +28 -0
- package/test/html-rcdata.test.js +35 -0
- package/test/image-decode.test.js +18 -0
- package/test/javascript-dialog.test.js +83 -0
- package/test/manual-breakout-paddle.js +42 -0
- package/test/manual-breakout-static.js +35 -0
- package/test/manual-classic-games.js +41 -0
- package/test/manual-clumsy-bird-three-pipes.js +81 -0
- package/test/manual-clumsy-bird.js +52 -0
- package/test/manual-hextris.js +30 -0
- package/test/manual-numbered-music.js +24 -0
- package/test/manual-t-rex-runner.js +49 -0
- package/test/manual-threejs-scene.js +64 -0
- package/test/manual-webgl-benchmark.js +27 -0
- package/test/manual-webgl-cpu.html +26 -0
- package/test/media-audio-lifetime.test.js +8 -0
- package/test/mp4-frame-times.test.js +33 -0
- package/test/page-scripts.test.js +96 -1
- package/test/piano-input.test.js +15 -2
- package/test/piano.test.js +11 -2
- package/test/playback-buffer.test.js +11 -0
- package/test/pointer-drag.test.js +33 -0
- package/test/positioning.test.js +82 -0
- package/test/pulse-client.test.js +51 -2
- package/test/raster-readback.test.js +14 -0
- package/test/script-rewrite.test.js +61 -3
- package/test/video-slow-end.test.js +6 -0
- package/test/viewport-keyboard-overflow.test.js +25 -0
- package/test/wayland-win32-resources.test.js +47 -0
- package/test/wayland-win32-wire.test.js +59 -0
- package/test/web-audio-renderer.test.js +8 -0
- package/test/web-audio.test.js +264 -0
- package/test/webgl-clip.test.js +9 -0
- package/test/webgl-cpu-fragment.test.js +43 -0
- package/test/window-named-properties.test.js +41 -0
- package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfig.ts +2 -2
- package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfigContentModelEnum.ts +1 -0
- package/vendor/happy-dom/packages/happy-dom/src/html-parser/HTMLParser.ts +23 -3
- package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +1 -1
- package/vendor/land-renderer/LICENSE +21 -0
- package/vendor/land-renderer/README.md +14 -0
- package/vendor/land-renderer/direct-compositor.ts +443 -0
- package/vendor/land-renderer/frame.js +88 -0
- package/vendor/land-renderer/png-fast.js +85 -0
- package/vendor/land-renderer/raw-frames.js +66 -0
- package/vendor/land-renderer/render-types.ts +7 -0
- package/vendor/land-renderer/renderer.ts +134 -0
- package/vendor/land-renderer/snapshot.js +37 -0
- package/vendor/land-renderer/viewport.js +18 -0
- package/vendor/ses/dist/ses.mjs +1 -1
|
@@ -102,3 +102,36 @@ test('pointer bursts preserve drawing points while batching raster work',async()
|
|
|
102
102
|
const [exit,stderr]=await Promise.all([child.exited,new Response(child.stderr).text()]);
|
|
103
103
|
expect(stderr).toBe('');expect(exit).toBe(0);
|
|
104
104
|
},15000);
|
|
105
|
+
|
|
106
|
+
test('HTML draggable canvas emits offsets, protected text data and dragend without a click',async()=>{
|
|
107
|
+
const b=await createBrowser({spareRenderer:false}),server=CdpServer.create(b.context).listen(0),c=new CDP(`ws://127.0.0.1:${server.port}/devtools/browser/cdp-server`);
|
|
108
|
+
try{
|
|
109
|
+
await c.send('Page.navigate',{url:'data:text/html,'+encodeURIComponent(`<body style="margin:0"><canvas id="cv" draggable="true" width="300" height="300"></canvas><script>
|
|
110
|
+
window.events=[];window.cancelDrag=false;
|
|
111
|
+
cv.ondragstart=e=>{events.push([e.type,e.offsetX,e.offsetY,e instanceof DragEvent]);e.dataTransfer.setData('text/plain','drag');if(cancelDrag)e.preventDefault()};
|
|
112
|
+
cv.ondragover=e=>{events.push([e.type,e.dataTransfer.getData('text/plain')]);e.preventDefault()};
|
|
113
|
+
cv.ondrop=e=>events.push([e.type,e.dataTransfer.getData('text/plain')]);
|
|
114
|
+
cv.ondragend=e=>events.push([e.type,e.offsetX,e.offsetY]);
|
|
115
|
+
cv.onclick=()=>events.push(['click']);</script>`)});
|
|
116
|
+
const send=(type,x,y)=>c.send('Input.dispatchMouseEvent',{type,x,y,button:'left',clickCount:1});
|
|
117
|
+
await send('mousePressed',30,40);await send('mouseMoved',160,170);await send('mouseReleased',160,170);
|
|
118
|
+
expect(await c.evaluate('events')).toEqual([['dragstart',30,40,true],['dragover',''],['drop','drag'],['dragend',160,170]]);
|
|
119
|
+
await c.evaluate('events=[];cancelDrag=true');
|
|
120
|
+
await send('mousePressed',30,40);await send('mouseMoved',160,170);await send('mouseReleased',160,170);
|
|
121
|
+
expect((await c.evaluate('events')).some(e=>e[0]==='dragend')).toBe(false);
|
|
122
|
+
await c.evaluate('events=[];cancelDrag=false');
|
|
123
|
+
await send('mousePressed',30,40);await send('mouseMoved',160,170);
|
|
124
|
+
await c.send('Input.dispatchKeyEvent',{type:'keyDown',key:'Escape',code:'Escape',windowsVirtualKeyCode:27});
|
|
125
|
+
expect((await c.evaluate('events')).map(e=>e[0])).toEqual(['dragstart','dragover','dragend']);
|
|
126
|
+
}finally{c.close();server.stop(true);b.close()}
|
|
127
|
+
},30000);
|
|
128
|
+
|
|
129
|
+
test('CSS rotation transforms painting bounds and inversely maps pointer offsets',async()=>{
|
|
130
|
+
const b=await createBrowser({spareRenderer:false}),server=CdpServer.create(b.context).listen(0),c=new CDP(`ws://127.0.0.1:${server.port}/devtools/browser/cdp-server`);
|
|
131
|
+
try{
|
|
132
|
+
await c.send('Page.navigate',{url:'data:text/html,'+encodeURIComponent(`<style>body{margin:0}canvas{position:absolute;left:100px;top:100px;width:200px;height:100px;transform:rotate(90deg)}</style><canvas id="cv"></canvas><script>window.clicks=[];cv.onclick=e=>clicks.push([e.offsetX,e.offsetY]);</script>`)});
|
|
133
|
+
const r=await c.evaluate('cv.getBoundingClientRect().toJSON()');expect(r.left).toBeCloseTo(150);expect(r.top).toBeCloseTo(50);expect(r.width).toBeCloseTo(100);expect(r.height).toBeCloseTo(200);
|
|
134
|
+
for(const type of ['mousePressed','mouseReleased'])await c.send('Input.dispatchMouseEvent',{type,x:175,y:90,button:'left',clickCount:1});
|
|
135
|
+
const hits=await c.evaluate('clicks');expect(hits.length).toBe(1);expect(hits[0][0]).toBeCloseTo(40);expect(hits[0][1]).toBeCloseTo(75);
|
|
136
|
+
}finally{c.close();server.stop(true);b.close()}
|
|
137
|
+
},30000);
|
package/test/positioning.test.js
CHANGED
|
@@ -237,3 +237,85 @@ test("an svg without width and height fills its container and keeps its viewBox'
|
|
|
237
237
|
expect(run("#sized")).toMatchObject({ width: 10, height: 5 });
|
|
238
238
|
expect(run("#bare")).toMatchObject({ width: 24, height: 150 });
|
|
239
239
|
});
|
|
240
|
+
|
|
241
|
+
test("aspect-ratio sizes bottom-anchored controls and centers a percentage-width grid child", () => {
|
|
242
|
+
const { box } = render(`<div class="stage"><div class="pad"><div class="knob"></div></div><button class="fire">!</button></div>`,
|
|
243
|
+
`* { box-sizing:border-box } .stage { position:relative; width:400px; height:300px }
|
|
244
|
+
.pad { position:absolute; left:20px; bottom:20px; width:150px; aspect-ratio:1; border:2px solid; display:grid; place-items:center }
|
|
245
|
+
.knob { width:42%; aspect-ratio:1 }
|
|
246
|
+
.fire { position:absolute; right:20px; bottom:20px; width:100px; aspect-ratio:1; border:2px solid; padding:4px }`);
|
|
247
|
+
expect(box('.pad')).toEqual({ x:20, y:130, width:150, height:150 });
|
|
248
|
+
expect(box('.knob').width).toBeCloseTo(61.32);
|
|
249
|
+
expect(box('.knob').height).toBeCloseTo(61.32);
|
|
250
|
+
expect(box('.knob').x).toBeCloseTo(64.34);
|
|
251
|
+
expect(box('.knob').y).toBeCloseTo(174.34);
|
|
252
|
+
expect(box('.fire')).toEqual({ x:280, y:180, width:100, height:100 });
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("aspect-ratio respects box-sizing, definite dimensions and height constraints", () => {
|
|
256
|
+
const { box } = render(`<div id="content"></div><div id="border"></div><div id="height"></div>
|
|
257
|
+
<div id="both"></div><div id="clamped"><div id="percent"></div></div>`,
|
|
258
|
+
`#content,#border { width:100px; aspect-ratio:2; padding:10px; border:2px solid }
|
|
259
|
+
#border { box-sizing:border-box }
|
|
260
|
+
#height { height:40px; aspect-ratio:2 }
|
|
261
|
+
#both { width:100px; height:30px; aspect-ratio:1 }
|
|
262
|
+
#clamped { width:100px; aspect-ratio:1; max-height:40px }
|
|
263
|
+
#percent { height:50% }`);
|
|
264
|
+
expect(box('#content')).toMatchObject({ width:124, height:74 });
|
|
265
|
+
expect(box('#border')).toMatchObject({ width:100, height:50 });
|
|
266
|
+
expect(box('#height')).toMatchObject({ width:80, height:40 });
|
|
267
|
+
expect(box('#both')).toMatchObject({ width:100, height:30 });
|
|
268
|
+
expect(box('#clamped').height).toBe(40);
|
|
269
|
+
expect(box('#percent').height).toBe(20);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("grid normal alignment preserves an item's preferred ratio while explicit stretch can override it", () => {
|
|
273
|
+
const { box } = render(`<div class="grid"><div id="normal"></div></div><div class="grid"><div id="stretch"></div></div>`,
|
|
274
|
+
`.grid { display:grid; width:100px; height:200px } .grid>div { width:100px; aspect-ratio:1 }
|
|
275
|
+
#stretch { align-self:stretch }`);
|
|
276
|
+
expect(box('#normal')).toMatchObject({ width:100, height:100 });
|
|
277
|
+
expect(box('#stretch')).toMatchObject({ width:100, height:200 });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("aspect-ratio preserves the automatic content minimum unless min-height allows overflow", () => {
|
|
281
|
+
const { box } = render(`<div class="ratio" id="auto"><div></div></div><div class="ratio" id="zero"><div></div></div>`,
|
|
282
|
+
`.ratio { width:100px; aspect-ratio:2 } .ratio>div { height:80px } #zero { min-height:0 }`);
|
|
283
|
+
expect(box('#auto').height).toBe(80);
|
|
284
|
+
expect(box('#zero').height).toBe(50);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("replaced elements use an explicit aspect-ratio or their natural ratio with auto", () => {
|
|
288
|
+
const { box } = render(`<canvas id="override" width="200" height="100"></canvas>
|
|
289
|
+
<canvas id="natural" width="200" height="100"></canvas><canvas id="degenerate" width="200" height="100"></canvas>`,
|
|
290
|
+
`canvas { display:block; width:80px } #override { aspect-ratio:1 } #natural { aspect-ratio:auto 1 } #degenerate { aspect-ratio:0 / 1 }`);
|
|
291
|
+
expect(box('#override')).toMatchObject({ width:80, height:80 });
|
|
292
|
+
expect(box('#natural')).toMatchObject({ width:80, height:40 });
|
|
293
|
+
expect(box('#degenerate')).toMatchObject({ width:80, height:40 });
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('absolute left/right and explicit width resolve auto margins before CSS transforms',()=>{
|
|
297
|
+
const {box}=render('<div class="both"></div><div class="left"></div><div class="right"></div><div class="large"></div>',`
|
|
298
|
+
.both,.left,.right,.large{position:absolute;left:10px;right:30px;width:100px;height:20px}
|
|
299
|
+
.both{margin:0 auto}.left{margin-left:auto;margin-right:20px}.right{margin-left:20px;margin-right:auto}
|
|
300
|
+
.large{width:500px;margin:0 auto}
|
|
301
|
+
`);
|
|
302
|
+
expect(box('.both').x).toBe(140);
|
|
303
|
+
expect(box('.left').x).toBe(250);
|
|
304
|
+
expect(box('.right').x).toBe(30);
|
|
305
|
+
expect(box('.large').x).toBe(10);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('backface-hidden card faces respect preserve-3d rotations and remain correct after a flip',()=>{
|
|
309
|
+
for(const flipped of [false,true]){
|
|
310
|
+
const {layout,tree}=render('<div class="card"><div class="back">back</div><div class="front">front</div></div>',`
|
|
311
|
+
.card{position:relative;width:100px;height:100px;transform-style:preserve-3d;${flipped?'transform:rotateY(180deg)':''}}
|
|
312
|
+
.back,.front{position:absolute;inset:0;backface-visibility:hidden}
|
|
313
|
+
.back{background:blue}.front{background:red;transform:rotateY(180deg)}
|
|
314
|
+
`);
|
|
315
|
+
const list=buildDisplayList(layout,tree);
|
|
316
|
+
const colors=list.items.filter(item=>item.op==='fillRect').map(item=>item.color);
|
|
317
|
+
expect(colors).toContain(flipped?'rgba(255, 0, 0, 1)':'rgba(0, 0, 255, 1)');
|
|
318
|
+
expect(colors).not.toContain(flipped?'rgba(0, 0, 255, 1)':'rgba(255, 0, 0, 1)');
|
|
319
|
+
expect(list.items.filter(item=>item.op==='drawText').map(item=>item.text)).toEqual([flipped?'front':'back']);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
@@ -223,7 +223,7 @@ test("measured device progress governs the media clock and bounded audio queue t
|
|
|
223
223
|
// Keep the real connection/open/close path, but drive only the playback
|
|
224
224
|
// intervals with a virtual monotonic clock. Latency responses are controlled
|
|
225
225
|
// at the client boundary; the test above covers their wire representation.
|
|
226
|
-
async function playbackTimers(run) {
|
|
226
|
+
async function playbackTimers(run, {credit = 1e7} = {}) {
|
|
227
227
|
const { createServer } = await import("node:net");
|
|
228
228
|
const server = createServer(socket => {
|
|
229
229
|
let input = Buffer.alloc(0);
|
|
@@ -235,7 +235,7 @@ async function playbackTimers(run) {
|
|
|
235
235
|
input = input.subarray(20 + length);
|
|
236
236
|
if (channel !== 0xffffffff) continue;
|
|
237
237
|
const command = body.readUInt32BE(1), tag = body.readUInt32BE(6);
|
|
238
|
-
const extra = command === 3 ? [0x4c, ...u32(1), 0x4c, ...u32(1), 0x4c, ...u32(
|
|
238
|
+
const extra = command === 3 ? [0x4c, ...u32(1), 0x4c, ...u32(1), 0x4c, ...u32(credit)] : [];
|
|
239
239
|
const reply = Buffer.from([0x4c, ...u32(2), 0x4c, ...u32(tag), ...extra]);
|
|
240
240
|
socket.write(Buffer.concat([header(reply.length, 0xffffffff), reply]));
|
|
241
241
|
}
|
|
@@ -384,3 +384,52 @@ test("timing rejection permits a retry and a late rejection after kill stays sil
|
|
|
384
384
|
expect(times).toEqual([]);
|
|
385
385
|
});
|
|
386
386
|
});
|
|
387
|
+
|
|
388
|
+
test('live audio adapts to slow renderer replies with a bounded live reserve and device queue',async()=>{
|
|
389
|
+
await playbackTimers(async({client,sent,advance})=>{
|
|
390
|
+
const requests=[];
|
|
391
|
+
client.playbackTiming=()=>({writeIndex:0,readIndex:0,sinkSeconds:0,playing:false});
|
|
392
|
+
const stream=await client.openPlayback({rate:500,channels:1,leadSeconds:.12,adaptiveLead:true,
|
|
393
|
+
onRequest:bytes=>requests.push(bytes),onTiming:()=>{}});
|
|
394
|
+
// Even a renderer that has not replied gets only a bounded live request.
|
|
395
|
+
expect(requests.reduce((a,b)=>a+b,0)).toBeLessThanOrEqual(320);
|
|
396
|
+
await advance(250);
|
|
397
|
+
stream.write(new Uint8Array(1000));
|
|
398
|
+
const total=()=>sent.reduce((a,p)=>a+p.bytes,0);
|
|
399
|
+
expect(total()).toBe(350); // 250 ms observed response + 100 ms headroom
|
|
400
|
+
await advance(2000);
|
|
401
|
+
expect(total()).toBe(350); // stale playback clock cannot flood the sink
|
|
402
|
+
expect(requests.reduce((a,b)=>a+b,0)).toBeLessThanOrEqual(500);
|
|
403
|
+
stream.kill();expect(stream.queued).toBe(0);
|
|
404
|
+
sent.length=0;
|
|
405
|
+
const slow=await client.openPlayback({rate:500,channels:1,leadSeconds:.12,adaptiveLead:true,
|
|
406
|
+
onRequest:()=>{},onTiming:()=>{}});
|
|
407
|
+
await advance(1500);slow.write(new Uint8Array(2000));
|
|
408
|
+
expect(total()).toBe(500); // even extreme stalls stop at half a second
|
|
409
|
+
slow.kill();sent.length=0;
|
|
410
|
+
const fast=await client.openPlayback({rate:500,channels:1,leadSeconds:.12,adaptiveLead:true,
|
|
411
|
+
onRequest:()=>{},onTiming:()=>{}});
|
|
412
|
+
fast.write(new Uint8Array(1000));expect(total()).toBe(120);
|
|
413
|
+
fast.kill();
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
test('live synthesis prepares PCM even after server credits are exhausted',async()=>{
|
|
419
|
+
await playbackTimers(async({client,sent,advance})=>{
|
|
420
|
+
const requests=[];
|
|
421
|
+
client.playbackTiming=()=>({writeIndex:100,readIndex:100,sinkSeconds:0,playing:false});
|
|
422
|
+
const stream=await client.openPlayback({rate:500,channels:1,leadSeconds:.12,adaptiveLead:true,
|
|
423
|
+
onRequest:bytes=>requests.push(bytes),onTiming:()=>{}});
|
|
424
|
+
expect(requests).toEqual([320]);
|
|
425
|
+
stream.write(new Uint8Array(320));
|
|
426
|
+
expect(sent.reduce((n,p)=>n+p.bytes,0)).toBe(100);
|
|
427
|
+
await advance(50);
|
|
428
|
+
// No new server credits, but device consumption leaves room for the live
|
|
429
|
+
// producer to refill its reserve before those credits eventually arrive.
|
|
430
|
+
expect(requests).toEqual([320,100]);
|
|
431
|
+
await advance(200);
|
|
432
|
+
expect(requests).toEqual([320,100]); // outstanding demand is not duplicated
|
|
433
|
+
stream.kill();
|
|
434
|
+
},{credit:100});
|
|
435
|
+
});
|
|
@@ -38,3 +38,17 @@ test("raster readback reuses native scratch while published shared frames stay i
|
|
|
38
38
|
}
|
|
39
39
|
expect(releases).toBe(allocations);
|
|
40
40
|
});
|
|
41
|
+
|
|
42
|
+
test('partial raster readback patches an immutable previous frame and rejects invalid regions',async()=>{
|
|
43
|
+
const {CanvasKit:kit}=await loadGraphics();const surface=kit.MakeSurface(8,6);
|
|
44
|
+
const capture=new RasterReadback(kit,{shared:true});
|
|
45
|
+
const read=options=>{const image=surface.makeImageSnapshot();try{return capture.read(image,8,6,options)}finally{image.delete()}};
|
|
46
|
+
try{
|
|
47
|
+
surface.getCanvas().clear(kit.Color(255,0,0,1));const first=read(),saved=first.slice();
|
|
48
|
+
const paint=new kit.Paint();try{paint.setColor(kit.Color(0,0,255,0.5));paint.setBlendMode(kit.BlendMode.Src);surface.getCanvas().drawRect(kit.XYWHRect(2,1,3,2),paint)}finally{paint.delete()}
|
|
49
|
+
const full=read(),partial=read({previous:first,region:{x:2,y:1,width:3,height:2}});
|
|
50
|
+
expect(partial).toEqual(full);expect(first).toEqual(saved);expect(partial.buffer).not.toBe(first.buffer);
|
|
51
|
+
for(const region of [{x:-1,y:0,width:2,height:2},{x:0,y:0,width:0,height:2},{x:7,y:0,width:2,height:2},{x:0.5,y:0,width:2,height:2}])
|
|
52
|
+
expect(read({previous:first,region})).toEqual(full);
|
|
53
|
+
}finally{capture.dispose();surface.dispose()}
|
|
54
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
-
escapeSesHtmlComments, prepareDirectEvalSource, restoreSourceText, rewriteClassicScript, rewriteConstructorReads,
|
|
4
|
-
referenceHelper, rewriteClassicScriptSource, rewriteDirectEval,
|
|
3
|
+
escapeSesHtmlComments, prepareDirectEvalSource, restoreSourceText, rewriteClassicScript, rewriteMemberReads, rewriteConstructorReads,
|
|
4
|
+
referenceHelper, rewriteClassicScriptSource, rewriteModuleSource, rewriteDirectEval,
|
|
5
5
|
} from "../lib/renderer/script-rewrite.js";
|
|
6
6
|
|
|
7
7
|
// The comment each constructor rewrite leaves for restoreSourceText.
|
|
@@ -134,7 +134,7 @@ test("the classic-script pipeline parses once when earlier rewrites change nothi
|
|
|
134
134
|
"return 1;",
|
|
135
135
|
];
|
|
136
136
|
for (const source of sources) {
|
|
137
|
-
const stepwise = rewriteClassicScript(rewriteConstructorReads(rewriteDirectEval(source).code).code);
|
|
137
|
+
const stepwise = rewriteClassicScript(rewriteMemberReads(rewriteConstructorReads(rewriteDirectEval(source).code).code).code);
|
|
138
138
|
expect(rewriteClassicScriptSource(source)).toEqual(stepwise);
|
|
139
139
|
}
|
|
140
140
|
const direct = rewriteDirectEval("var s = 'eval(1)';");
|
|
@@ -169,3 +169,61 @@ test("global variable reads go through the script's global binding, keeping thei
|
|
|
169
169
|
// A block-level function outside functions (Annex B) leaves the script's identifiers as written.
|
|
170
170
|
expect(rewriteClassicScript("{ function b() {} } b(Q);").code).not.toContain("$buninu$g");
|
|
171
171
|
});
|
|
172
|
+
|
|
173
|
+
test('global-read rewrites preserve automatic semicolons and original function source', () => {
|
|
174
|
+
const source = `function convert() {
|
|
175
|
+
let a = input.value
|
|
176
|
+
output.value = a
|
|
177
|
+
if (a) output.extra = input.value
|
|
178
|
+
output.done = true
|
|
179
|
+
for (let i=0;i<1;i++) output.count = i
|
|
180
|
+
return
|
|
181
|
+
output.unreachable = true
|
|
182
|
+
}`;
|
|
183
|
+
const rewritten = rewriteClassicScriptSource(source);
|
|
184
|
+
const g = {input:{value:'text'},output:{},$buninu$self:x=>x,$buninu$member:(o,k,c)=>c&&typeof o[k]==='function'?o[k].bind(o):o[k]};
|
|
185
|
+
g.$buninu$ref = referenceHelper(g);
|
|
186
|
+
Function('globalThis', 'input', 'output', '$buninu$member', `let convert; ${rewritten.code}; globalThis.convert=convert;`)(g,g.input,g.output,g.$buninu$member);
|
|
187
|
+
g.convert();
|
|
188
|
+
expect(g.output).toEqual({value:'text',extra:'text',done:true,count:0});
|
|
189
|
+
expect(restoreSourceText(g.convert.toString())).toBe(source);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('member adaptation preserves source, call order, computed keys, parentheses and assignment patterns',()=>{
|
|
193
|
+
const samples=[
|
|
194
|
+
'function f(a){return(a.x).y(a.z)}',
|
|
195
|
+
'function f(a){return a /* property comment */ .\\u0062(a[/* index comment */ a.key])}',
|
|
196
|
+
'function f(a){return(a,b).x}',
|
|
197
|
+
'function f(a){return new (a.Ctor)(a.x)}',
|
|
198
|
+
'function f(a){return a[a.key](a.x)}',
|
|
199
|
+
'function f(a){({x:a.x=2,...a.rest}=a.source);[a.y]=a.values;return a.x}',
|
|
200
|
+
'function f(a){a.x++;delete a.x;for(a.x of a.values){};return a?.x}',
|
|
201
|
+
'class C extends B{#x=1;f(){return super.f(this.#x)}}',
|
|
202
|
+
];
|
|
203
|
+
for(const source of samples){const result=rewriteMemberReads(source);expect(restoreSourceText(result.code)).toBe(source);expect(()=>Function('$buninu$member','B',result.code)).not.toThrow();}
|
|
204
|
+
const calls=[];
|
|
205
|
+
const receiver={get fn(){calls.push('get');return function(x){calls.push(this===receiver?'this':'wrong');return x;};}};
|
|
206
|
+
const key={ [Symbol.toPrimitive](hint){calls.push(hint);return 'fn';}};
|
|
207
|
+
const read=(o,k,call)=>{const f=o[k];return call?(...args)=>Reflect.apply(f,o,args):f;};
|
|
208
|
+
const source='return receiver[key]((calls.push("arg"),42))';
|
|
209
|
+
expect(Function('$buninu$member','receiver','key','calls',rewriteMemberReads(source.replace('return ','')).code.replace(/^/,'return '))(read,receiver,key,calls)).toBe(42);
|
|
210
|
+
expect(calls).toEqual(['string','get','arg','this']);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('member adaptation preserves ASI before reparsing local calls in scripts and modules',()=>{
|
|
214
|
+
const source=`function local($) {
|
|
215
|
+
var diff = (7-2)
|
|
216
|
+
$('box').css('value',diff)
|
|
217
|
+
var receiver = {value:3}
|
|
218
|
+
receiver.read = function(){return this.value}
|
|
219
|
+
return receiver.read()
|
|
220
|
+
}`;
|
|
221
|
+
for(const rewrite of [rewriteMemberReads,source=>({code:rewriteModuleSource(source)})]){
|
|
222
|
+
const code=rewrite(source).code;
|
|
223
|
+
const values=[],$=()=>({css:(key,value)=>values.push([key,value])});
|
|
224
|
+
const member=(object,key,call)=>call&&typeof object[key]==='function'?object[key].bind(object):object[key];
|
|
225
|
+
const fn=Function('$buninu$member',`${code}; return local;`)(member);
|
|
226
|
+
expect(fn($)).toBe(3);expect(values).toEqual([['value',5]]);
|
|
227
|
+
expect(restoreSourceText(fn.toString())).toBe(source);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
test('silent video waits for a slow decoder and retains its final frame',async()=>{
|
|
3
|
+
const child=Bun.spawn([process.execPath,new URL('./fixtures/video-slow-end.js',import.meta.url).pathname],{stdout:'pipe',stderr:'pipe'});
|
|
4
|
+
const [out,err,code]=await Promise.all([new Response(child.stdout).text(),new Response(child.stderr).text(),child.exited]);
|
|
5
|
+
expect({code,err}).toEqual({code:0,err:''});expect(out).toContain('slow video end integration passed');
|
|
6
|
+
},15000);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {createBrowser} from '../lib/headless-shell.js';
|
|
3
|
+
|
|
4
|
+
test('root/body overflow blocks keyboard scrolling without blocking script scrolling for hidden overflow',async()=>{
|
|
5
|
+
const browser=await createBrowser({spareRenderer:false}),ctx=browser.context;
|
|
6
|
+
try{
|
|
7
|
+
await ctx.resize(400,300);
|
|
8
|
+
for(const rule of ['body{overflow:hidden}','html{overflow:hidden}','html{overflow:clip}','body{overflow-y:hidden}']){
|
|
9
|
+
await ctx.navigate('data:text/html,'+encodeURIComponent(`<style>body{margin:0}${rule}</style><div style="height:1500px">content</div>`));
|
|
10
|
+
for(const key of [' ','ArrowDown','PageDown','End']){
|
|
11
|
+
await ctx.press(key);expect(await ctx.evaluate('scrollY')).toBe(0);
|
|
12
|
+
}
|
|
13
|
+
if(!rule.includes('clip')){
|
|
14
|
+
await ctx.evaluate('scrollTo(0,100)');expect(await ctx.evaluate('scrollY')).toBe(100);
|
|
15
|
+
await ctx.press('Home');expect(await ctx.evaluate('scrollY')).toBe(100);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
await ctx.navigate('data:text/html,'+encodeURIComponent('<style>body{margin:0}</style><div style="height:1500px">content</div>'));
|
|
19
|
+
await ctx.press(' ');expect(await ctx.evaluate('scrollY')).toBeGreaterThan(0);
|
|
20
|
+
await ctx.evaluate('scrollTo(0,0); document.body.style.overflow="hidden"');
|
|
21
|
+
await ctx.press(' ');expect(await ctx.evaluate('scrollY')).toBe(0);
|
|
22
|
+
await ctx.evaluate('document.body.style.overflow="auto"');
|
|
23
|
+
await ctx.press(' ');expect(await ctx.evaluate('scrollY')).toBeGreaterThan(0);
|
|
24
|
+
}finally{browser.close();}
|
|
25
|
+
},30000);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {mkdtempSync,rmSync} from 'node:fs';
|
|
3
|
+
import {tmpdir} from 'node:os';
|
|
4
|
+
import {join} from 'node:path';
|
|
5
|
+
const windows=process.platform==='win32';
|
|
6
|
+
test.skipIf(!windows)('Windows mapping shares pixels between independent Bun processes',async()=>{
|
|
7
|
+
const r=await import('../frontends/wayland/win32-resources.js');
|
|
8
|
+
const fd=r.createMapping(16),held=r.sendResource(fd);
|
|
9
|
+
let child;
|
|
10
|
+
try{
|
|
11
|
+
expect(held.descriptor.name).toMatch(/^Local\\buninu-wayland-/);
|
|
12
|
+
r.writeResource(fd,Buffer.from([1,2,3,4]),0,4,0);
|
|
13
|
+
const module=new URL('../frontends/wayland/win32-resources.js',import.meta.url).href;
|
|
14
|
+
child=Bun.spawn([process.execPath,'-e',`
|
|
15
|
+
const r=await import(${JSON.stringify(module)}),fd=r.receiveResource(JSON.parse(Bun.argv.at(-1)));
|
|
16
|
+
try{const b=Buffer.alloc(4);r.readResource(fd,b,0,4,0);if(b[0]!==1)throw Error('wrong pixels');
|
|
17
|
+
r.writeResource(fd,Buffer.from([9,8,7,6]),0,4,0);
|
|
18
|
+
}finally{r.closeResource(fd)}
|
|
19
|
+
`,JSON.stringify(held.descriptor)],{stdout:'pipe',stderr:'pipe'});
|
|
20
|
+
const timer=setTimeout(()=>child.kill(),15000);
|
|
21
|
+
try{const error=await new Response(child.stderr).text();expect({code:await child.exited,error}).toEqual({code:0,error:''})}finally{clearTimeout(timer)}
|
|
22
|
+
const b=Buffer.alloc(4);r.readResource(fd,b,0,4,0);expect([...b]).toEqual([9,8,7,6]);
|
|
23
|
+
}finally{child?.kill();held.release();r.closeResource(fd)}
|
|
24
|
+
},20000);
|
|
25
|
+
test.skipIf(!windows)('Windows pipe resource preserves pending reads and EOF across FD transfer',async()=>{
|
|
26
|
+
const r=await import('../frontends/wayland/win32-resources.js'),fds=new Int32Array(2);r.libc.pipe2(fds);
|
|
27
|
+
const held=r.sendResource(fds[1]);r.closeResource(fds[1]);const remote=r.receiveResource(held.descriptor);held.release();
|
|
28
|
+
try{
|
|
29
|
+
const b=Buffer.alloc(32);expect(()=>r.readResource(fds[0],b)).toThrow('pending');
|
|
30
|
+
r.writeResource(remote,Buffer.from('中文 clipboard'));r.closeResource(remote);
|
|
31
|
+
const n=r.readResource(fds[0],b);expect(b.subarray(0,n).toString()).toBe('中文 clipboard');expect(r.readResource(fds[0],b)).toBe(0);
|
|
32
|
+
}finally{r.closeResource(remote);r.closeResource(fds[0])}
|
|
33
|
+
});
|
|
34
|
+
test.skipIf(!windows)('Windows ordinary socket delivers a named mapping with resource ACK',async()=>{
|
|
35
|
+
const r=await import('../frontends/wayland/win32-resources.js');
|
|
36
|
+
const {MappingConnection}=await import('../frontends/wayland/win32-wire.js');
|
|
37
|
+
const {connectUnix}=await import('../frontends/wayland/transport.js');
|
|
38
|
+
const dir=mkdtempSync(join(tmpdir(),'wayland-mapping-')),path=join(dir,'wayland-buninu');
|
|
39
|
+
let server,client,peer;const fd=r.createMapping(4);
|
|
40
|
+
try{
|
|
41
|
+
r.writeResource(fd,Buffer.from([12,34,56,78]),0,4,0);
|
|
42
|
+
server=Bun.listen({unix:path,socket:{open(s){peer=new MappingConnection(r);s.data=peer;peer.attach(s);peer.onData=()=>{const b=Buffer.alloc(4);const received=peer.fds.shift();try{r.readResource(received,b)}finally{r.closeResource(received)}peer.send(b)}},data:(s,b)=>s.data.receive(b),drain:s=>s.data.flush(),close:s=>s.data.close()}});
|
|
43
|
+
client=connectUnix(path);client.send(Buffer.from('commit'),[fd]);
|
|
44
|
+
const deadline=Date.now()+5000;while(client.input.length<4&&Date.now()<deadline)await Bun.sleep(10);
|
|
45
|
+
expect([...client.input]).toEqual([12,34,56,78]);expect(client.pending.size).toBe(0);
|
|
46
|
+
}finally{client?.close();peer?.close();server?.stop(true);r.closeResource(fd);rmSync(dir,{recursive:true,force:true})}
|
|
47
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {existsSync} from 'node:fs';
|
|
3
|
+
const sibling=existsSync(new URL('../../buninu-land/lib/win32-wire.js',import.meta.url));
|
|
4
|
+
import {MappingConnection} from '../frontends/wayland/win32-wire.js';
|
|
5
|
+
import {encodeRequest,decode,interfaces} from '../frontends/wayland/protocol.js';
|
|
6
|
+
function resources(){
|
|
7
|
+
const objects=new Map(),fds=new Map();let serial=0;
|
|
8
|
+
const put=o=>{const fd=++serial;fds.set(fd,o);o.refs++;return fd};
|
|
9
|
+
const api={
|
|
10
|
+
create(bytes){const o={name:crypto.randomUUID(),bytes,refs:0};objects.set(o.name,o);return put(o)},
|
|
11
|
+
sendResource(fd){const o=fds.get(fd);if(!o)throw Error('closed');o.refs++;let released=false;
|
|
12
|
+
return {descriptor:{name:o.name},release(){if(released)throw Error('double release');released=true;o.refs--}}},
|
|
13
|
+
receiveResource(d){const o=objects.get(d.name);if(!o||!o.refs)throw Error('mapping gone');return put(o)},
|
|
14
|
+
closeResource(fd){const o=fds.get(fd);if(o){o.refs--;fds.delete(fd)}},
|
|
15
|
+
get:fd=>fds.get(fd),objects,
|
|
16
|
+
};return api;
|
|
17
|
+
}
|
|
18
|
+
function pair(r,partial=false){
|
|
19
|
+
const a=new MappingConnection(r),b=new MappingConnection(r),toA=[],toB=[];
|
|
20
|
+
const socket=queue=>({write(bytes){const n=partial?Math.min(7,bytes.length):bytes.length;queue.push(Buffer.from(bytes.subarray(0,n)));return n},end(){}});
|
|
21
|
+
a.attach(socket(toB));b.attach(socket(toA));
|
|
22
|
+
const pump=()=>{for(let i=0;i<1000;i++){
|
|
23
|
+
a.flush();b.flush();while(toA.length)a.receive(toA.shift());while(toB.length)b.receive(toB.shift());
|
|
24
|
+
if(!a.output.length&&!b.output.length&&!toA.length&&!toB.length)return;
|
|
25
|
+
}throw Error('pump did not drain')};
|
|
26
|
+
return {a,b,pump};
|
|
27
|
+
}
|
|
28
|
+
test('mapping envelope preserves Wayland FD order, fragmented bytes, ACK lifetime and live pixels',()=>{
|
|
29
|
+
const r=resources(),{a,b,pump}=pair(r,true),pixels=Buffer.from([1,2,3,4]),fd=r.create(pixels),o=r.get(fd);
|
|
30
|
+
const request=encodeRequest(4,'wl_shm','create_pool',[8,fd,4]);a.send(request.data,request.fds);r.closeResource(fd);
|
|
31
|
+
expect(o.refs).toBe(1);expect(a.pending.size).toBe(1);pump();
|
|
32
|
+
expect(a.pending.size).toBe(0);expect(o.refs).toBe(1);
|
|
33
|
+
const args=decode(interfaces.get('wl_shm').requests[0],b.input,b.fds);
|
|
34
|
+
expect(args.slice(0,1)).toEqual([8]);expect(args[2]).toBe(4);
|
|
35
|
+
expect(r.get(args[1]).bytes).toBe(pixels);pixels[0]=99;expect(r.get(args[1]).bytes[0]).toBe(99);
|
|
36
|
+
r.closeResource(args[1]);a.close();b.close();expect(o.refs).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
test('disconnect releases unacknowledged mapping handles and queued descriptors',()=>{
|
|
39
|
+
const r=resources(),{a,b}=pair(r),fd=r.create(Buffer.alloc(4)),o=r.get(fd);
|
|
40
|
+
a.send(Buffer.alloc(8),[fd]);r.closeResource(fd);expect(o.refs).toBe(1);a.close();b.close();expect(o.refs).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
test('malformed envelope and unsolicited ACK close the connection',()=>{
|
|
43
|
+
const r=resources(),{a,b,pump}=pair(r);
|
|
44
|
+
a.packet({version:1,ack:123});pump();expect(b.closed).toBe(true);expect(b.error.message).toContain('ACK');a.close();
|
|
45
|
+
const c=new MappingConnection(r),header=Buffer.alloc(8);header.writeUInt32LE(2*1024*1024);c.receive(header);expect(c.closed).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
test.skipIf(process.platform==='win32')('non-Windows mapping modules reject before any DLL call',async()=>{
|
|
48
|
+
for(const path of ['../frontends/wayland/win32-resources.js',...(sibling?['../../buninu-land/lib/win32-resources.js']:[])]){
|
|
49
|
+
try{await import(path);throw Error('unexpected import success')}catch(e){expect(e.message).toBe('Named mappings require Windows')}
|
|
50
|
+
}
|
|
51
|
+
const browser=await import('../frontends/wayland/transport.js'),posix=await import('../frontends/wayland/transport-posix.js');
|
|
52
|
+
expect(browser.libc).toBe(posix.libc);
|
|
53
|
+
});
|
|
54
|
+
test.skipIf(!sibling)('both projects use identical Windows resource and framing implementations',async()=>{
|
|
55
|
+
for(const file of ['win32-resources.js','win32-wire.js']){
|
|
56
|
+
const a=await Bun.file(new URL('../frontends/wayland/'+file,import.meta.url)).text();
|
|
57
|
+
const b=await Bun.file(new URL('../../buninu-land/lib/'+file,import.meta.url)).text();expect(a).toBe(b);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
|
|
3
|
+
for(const mode of ['ses','host'])test(`${mode} pages synthesize audio through the existing output and release it on navigation`,async()=>{
|
|
4
|
+
const child=Bun.spawn([process.execPath,new URL('./fixtures/web-audio-renderer.js',import.meta.url).pathname,mode],{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(`${mode} Web Audio page PCM and disposal passed`);
|
|
8
|
+
},15000);
|