buninu-browser 0.6.0 → 0.9.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.
Files changed (144) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +94 -4
  3. package/buninu-browser.js +1 -1
  4. package/frontends/wayland/README.md +35 -2
  5. package/frontends/wayland/client.js +8 -6
  6. package/frontends/wayland/clipboard.js +1 -1
  7. package/frontends/wayland/index.js +1 -1
  8. package/frontends/wayland/input.js +1 -1
  9. package/frontends/wayland/resource-io.js +6 -0
  10. package/frontends/wayland/transport-posix.js +120 -0
  11. package/frontends/wayland/transport-win32.js +9 -0
  12. package/frontends/wayland/transport.js +3 -120
  13. package/frontends/wayland/win32-resources.js +80 -0
  14. package/frontends/wayland/win32-wire.js +62 -0
  15. package/index.js +10 -6
  16. package/lib/audio/biquad.js +25 -0
  17. package/lib/audio/piano.js +6 -3
  18. package/lib/audio/playback-buffer.js +5 -5
  19. package/lib/audio/pulse-client.js +24 -5
  20. package/lib/audio/web-audio.js +570 -0
  21. package/lib/browser-dialog.js +116 -0
  22. package/lib/cdp-server.js +128 -72
  23. package/lib/file-picker.js +48 -0
  24. package/lib/headless-shell.js +34 -12
  25. package/lib/layout/text-layout.js +112 -19
  26. package/lib/media/mp4-frame-times.js +93 -0
  27. package/lib/paint/direct-canvas.ts +39 -0
  28. package/lib/paint/display-list.js +37 -12
  29. package/lib/paint/raster-readback.js +16 -7
  30. package/lib/renderer/animation-frames.js +10 -5
  31. package/lib/renderer/bindings.js +69 -16
  32. package/lib/renderer/canvas-2d.js +265 -15
  33. package/lib/renderer/canvas-round-rect.js +36 -0
  34. package/lib/renderer/css-animations.js +173 -0
  35. package/lib/renderer/dialog-client.js +30 -0
  36. package/lib/renderer/dialog-worker.js +35 -0
  37. package/lib/renderer/eval-environment.js +28 -0
  38. package/lib/renderer/file-input.js +49 -0
  39. package/lib/renderer/host.js +33 -4
  40. package/lib/renderer/html-drag.js +77 -0
  41. package/lib/renderer/image-loading.js +45 -2
  42. package/lib/renderer/main.js +4 -0
  43. package/lib/renderer/module-transform.js +5 -3
  44. package/lib/renderer/page-builtins.js +59 -0
  45. package/lib/renderer/page-realm.js +177 -40
  46. package/lib/renderer/page-renderer.js +397 -49
  47. package/lib/renderer/piano-input.js +3 -1
  48. package/lib/renderer/raster-surface.js +18 -0
  49. package/lib/renderer/script-rewrite.js +165 -9
  50. package/lib/renderer/video-presentation.js +5 -4
  51. package/lib/renderer/webgl/clip.js +24 -0
  52. package/lib/renderer/webgl/context.js +158 -0
  53. package/lib/renderer/webgl/cpu-fragment.js +211 -0
  54. package/lib/renderer/webgl/draw-cache.js +22 -0
  55. package/lib/renderer/webgl/pipeline.js +70 -0
  56. package/lib/renderer/webgl/shaders.js +58 -0
  57. package/lib/renderer/webgl/textures.js +43 -0
  58. package/lib/renderer/window-named-properties.js +46 -0
  59. package/lib/screenshot-cli.js +4 -7
  60. package/lib/style/animation-values.js +29 -0
  61. package/lib/style/computed-style.js +109 -13
  62. package/lib/style/transform.js +44 -0
  63. package/lib/window-size.js +10 -0
  64. package/package.json +1 -1
  65. package/patches/happy-dom/0007-scope-content-models-to-html-namespace.patch +29 -0
  66. package/patches/happy-dom/README.md +16 -0
  67. package/test/animation-frames.test.js +15 -0
  68. package/test/canvas-2d.test.js +252 -4
  69. package/test/canvas-frame-pacing.test.js +7 -0
  70. package/test/cdp-viewers.test.js +79 -0
  71. package/test/classic-games-patches/0001-fix-game-initialization.patch +177 -0
  72. package/test/classic-games-patches/README.md +156 -0
  73. package/test/classic-games-scenarios.js +133 -0
  74. package/test/clipboard.test.js +16 -0
  75. package/test/computed-style.test.js +18 -0
  76. package/test/css-animation-lifecycle.test.js +28 -0
  77. package/test/css-animations.test.js +83 -0
  78. package/test/direct-canvas.test.js +39 -0
  79. package/test/dynamic-inline-handlers.test.js +25 -0
  80. package/test/eval-declarations.test.js +22 -0
  81. package/test/file-picker.test.js +26 -0
  82. package/test/fixtures/canvas-damage.js +23 -0
  83. package/test/fixtures/canvas-frame-pacing.js +35 -0
  84. package/test/fixtures/direct-canvas.js +96 -0
  85. package/test/fixtures/javascript-dialogs.html +8 -0
  86. package/test/fixtures/media-audio-lifetime.js +65 -0
  87. package/test/fixtures/renderer-memory.js +7 -5
  88. package/test/fixtures/video-black-replay.js +8 -6
  89. package/test/fixtures/video-slow-end.js +39 -0
  90. package/test/fixtures/web-audio-renderer.js +45 -0
  91. package/test/flex.test.js +26 -0
  92. package/test/grid.test.js +13 -0
  93. package/test/headless-shell.test.js +9 -0
  94. package/test/headless-window-size.test.js +28 -0
  95. package/test/html-rcdata.test.js +35 -0
  96. package/test/image-decode.test.js +18 -0
  97. package/test/index.test.js +1 -0
  98. package/test/javascript-dialog.test.js +83 -0
  99. package/test/manual-breakout-paddle.js +42 -0
  100. package/test/manual-breakout-static.js +35 -0
  101. package/test/manual-classic-games.js +41 -0
  102. package/test/manual-clumsy-bird-three-pipes.js +81 -0
  103. package/test/manual-clumsy-bird.js +52 -0
  104. package/test/manual-hextris.js +30 -0
  105. package/test/manual-numbered-music.js +24 -0
  106. package/test/manual-t-rex-runner.js +49 -0
  107. package/test/manual-threejs-scene.js +64 -0
  108. package/test/manual-webgl-benchmark.js +27 -0
  109. package/test/manual-webgl-cpu.html +26 -0
  110. package/test/media-audio-lifetime.test.js +8 -0
  111. package/test/mp4-frame-times.test.js +33 -0
  112. package/test/page-scripts.test.js +96 -1
  113. package/test/piano-input.test.js +15 -2
  114. package/test/piano.test.js +11 -2
  115. package/test/playback-buffer.test.js +11 -0
  116. package/test/pointer-drag.test.js +33 -0
  117. package/test/positioning.test.js +82 -0
  118. package/test/pulse-client.test.js +51 -2
  119. package/test/raster-readback.test.js +14 -0
  120. package/test/script-rewrite.test.js +61 -3
  121. package/test/video-slow-end.test.js +6 -0
  122. package/test/viewport-keyboard-overflow.test.js +25 -0
  123. package/test/wayland-win32-resources.test.js +47 -0
  124. package/test/wayland-win32-wire.test.js +59 -0
  125. package/test/web-audio-renderer.test.js +8 -0
  126. package/test/web-audio.test.js +264 -0
  127. package/test/webgl-clip.test.js +9 -0
  128. package/test/webgl-cpu-fragment.test.js +43 -0
  129. package/test/window-named-properties.test.js +41 -0
  130. package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfig.ts +2 -2
  131. package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfigContentModelEnum.ts +1 -0
  132. package/vendor/happy-dom/packages/happy-dom/src/html-parser/HTMLParser.ts +23 -3
  133. package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +1 -1
  134. package/vendor/land-renderer/LICENSE +21 -0
  135. package/vendor/land-renderer/README.md +14 -0
  136. package/vendor/land-renderer/direct-compositor.ts +443 -0
  137. package/vendor/land-renderer/frame.js +88 -0
  138. package/vendor/land-renderer/png-fast.js +85 -0
  139. package/vendor/land-renderer/raw-frames.js +66 -0
  140. package/vendor/land-renderer/render-types.ts +7 -0
  141. package/vendor/land-renderer/renderer.ts +134 -0
  142. package/vendor/land-renderer/snapshot.js +37 -0
  143. package/vendor/land-renderer/viewport.js +18 -0
  144. package/vendor/ses/dist/ses.mjs +1 -1
@@ -0,0 +1,33 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {mp4FrameTimes,timeVideoDecoder} from '../lib/media/mp4-frame-times.js';
3
+ const words=(...ns)=>{const b=new Uint8Array(ns.length*4),v=new DataView(b.buffer);ns.forEach((n,i)=>v.setUint32(i*4,n));return b};
4
+ const box=(name,...parts)=>{const body=Buffer.concat(parts),head=Buffer.alloc(8);head.writeUInt32BE(body.length+8);head.write(name,4);return Buffer.concat([head,body])};
5
+ function movie(){return box('moov',box('trak',box('mdia',
6
+ box('hdlr',words(0,0,0x76696465)),box('mdhd',words(0,0,0,1000)),
7
+ box('minf',box('stbl',box('stts',words(0,3,1,50,1,100,2,25)),box('stss',words(0,2,1,3)))))))}
8
+ test('MP4 variable-rate frames use sample times rather than average FPS',()=>{
9
+ const t=mp4FrameTimes(movie());expect([...t.frames]).toEqual([0,.05,.15,.175]);expect(t.end).toBe(.2);
10
+ let seek;const decoder=timeVideoDecoder({width:2,height:2,fps:20,nextFrame:()=>new Uint8Array(16),seek:ms=>seek=ms,close(){}},movie());
11
+ decoder.nextFrame();expect(decoder.frameTime).toBe(0);expect(decoder.nextFrameTime).toBe(.05);
12
+ decoder.nextFrame();expect(decoder.frameTime).toBe(.05);expect(decoder.nextFrameTime).toBe(.15);
13
+ decoder.seek(160);expect(seek).toBe(160);decoder.nextFrame();expect(decoder.frameTime).toBe(.15);
14
+ });
15
+ test('unsupported or truncated containers retain the original decoder',()=>{
16
+ const decoder={};for(const data of [new Uint8Array(),movie().subarray(0,20),box('free',words(0))]){
17
+ expect(mp4FrameTimes(data)).toBeNull();expect(timeVideoDecoder(decoder,data)).toBe(decoder);
18
+ }
19
+ });
20
+
21
+ test('composition offsets and an edit map reordered samples onto presentation time',()=>{
22
+ const data=box('moov',box('trak',
23
+ box('edts',box('elst',words(0,1,200,50,65536))),
24
+ box('mdia',box('hdlr',words(0,0,0x76696465)),box('mdhd',words(0,0,0,1000)),
25
+ box('minf',box('stbl',box('stts',words(0,1,4,50)),
26
+ box('ctts',words(0,4,1,50,1,100,1,0,1,50)),box('stss',words(0,1,1)))))));
27
+ const t=mp4FrameTimes(data);expect([...t.frames]).toEqual([0,.05,.1,.15]);expect(t.end).toBe(.2);
28
+ });
29
+ test('a progressive mdat does not hide already complete sample timing tables',()=>{
30
+ const head=movie(),payload=box('mdat',new Uint8Array(1000));
31
+ const t=mp4FrameTimes(Buffer.concat([head,payload.subarray(0,16)]));
32
+ expect([...t.frames]).toEqual([0,.05,.15,.175]);
33
+ });
@@ -1,5 +1,6 @@
1
1
  import { afterAll, beforeAll, expect, test } from "bun:test";
2
2
  import { RendererHost } from "../lib/renderer/host.js";
3
+ import { supportsFrameCheckpoint } from "../lib/renderer/animation-frames.js";
3
4
 
4
5
  /**
5
6
  * Page JavaScript runs in a real renderer process (seccomp + SES lockdown),
@@ -1221,6 +1222,22 @@ test("import methods keep their names and receivers alongside real dynamic impor
1221
1222
  expect(await evaluate("dynamicImportNames")).toEqual([42, true, "module:e"]);
1222
1223
  });
1223
1224
 
1225
+ test("dynamic module imports preserve grouped specifiers after property rewriting", async () => {
1226
+ scripts.set("https://page.test/grouped-dependency.js", "export const answer=42;");
1227
+ scripts.set("https://page.test/grouped-main.js", `
1228
+ let reads=0;
1229
+ const entry={get module(){reads++;return './grouped-dependency.js'}};
1230
+ const a=await import(entry.module);
1231
+ const b=await import((entry.module));
1232
+ const c=await import /* comment */ (((entry.module)));
1233
+ const d=await import /* comment */ ('./grouped-dependency.js');
1234
+ window.groupedImports=[a.answer,a===b,b===c,c===d,reads];
1235
+ `);
1236
+ await load('<script type="module" src="./grouped-main.js"></script>');
1237
+ await waitFor("typeof groupedImports !== 'undefined'");
1238
+ expect(await evaluate("groupedImports")).toEqual([42,true,true,true,3]);
1239
+ });
1240
+
1224
1241
  test("ES modules: graphs, live exports, JSON, import.meta, dynamic import, TLA, cycles, import maps", async () => {
1225
1242
  const base = "https://page.test/modules/";
1226
1243
  scripts.set(`${base}math.js`, `
@@ -1724,7 +1741,9 @@ test("a video starts from a prefix while the rest of the file is still downloadi
1724
1741
  await evaluate("document.getElementById('v').play()");
1725
1742
  expect(dripFinished).toBe(false);
1726
1743
  expect(await evaluate(`(() => { const v = document.getElementById('v');
1727
- return !v.paused && v.readyState >= 1 && v.currentTime > 0 && v.networkState === 2; })()`)).toBe(true);
1744
+ // The first presented frame has timestamp zero; wall time alone
1745
+ // must not advance a silent video before its next picture.
1746
+ return !v.paused && v.readyState >= 1 && v.videoWidth > 0 && v.currentTime >= 0 && v.networkState === 2; })()`)).toBe(true);
1728
1747
  const deadline = Date.now() + 4000;
1729
1748
  while (!dripFinished && Date.now() < deadline) await Bun.sleep(50);
1730
1749
  expect(dripFinished).toBe(true);
@@ -2304,3 +2323,79 @@ test("NodeIterator validates nullable callback-interface filters at creation", a
2304
2323
  return JSON.stringify(results);
2305
2324
  })()`)).toBe(JSON.stringify([...Array(8).fill('TypeError'),...Array(5).fill(true)]));
2306
2325
  });
2326
+
2327
+
2328
+ test("mouse offsets use target padding edges and CSS pixels, including bubbling and canvas scaling", async () => {
2329
+ await load(`<body style="margin:0"><div id="parent"><canvas id="cv" width="1200" height="1200"
2330
+ style="position:absolute;left:40px;top:30px;width:300px;height:300px;border:5px solid;padding:10px"></canvas></div>
2331
+ <script>
2332
+ window.offsets=[];
2333
+ cv.onclick=e=>{window.lastMouse=e;offsets.push([e.offsetX,e.offsetY]);};
2334
+ document.getElementById('parent').addEventListener('click',e=>offsets.push([e.offsetX,e.offsetY]));
2335
+ </script>`);
2336
+ await renderer.call("click", 75, 85);
2337
+ expect(await evaluate("offsets")).toEqual([[30,50],[30,50]]);
2338
+ expect(await evaluate("[lastMouse.offsetX,lastMouse.offsetY]")).toEqual([75,85]);
2339
+ expect(await evaluate("(()=>{const e=new MouseEvent('click',{clientX:12,clientY:14});return [e.offsetX,e.offsetY]})()")).toEqual([12,14]);
2340
+ });
2341
+
2342
+ (supportsFrameCheckpoint() ? test : test.skip)("classic script cleanup drains promise jobs before restoring currentScript", async () => {
2343
+ await load(`<script id="outer">
2344
+ var checkpointLog = [];
2345
+ const nested = document.createElement('script');
2346
+ nested.id = 'nested';
2347
+ nested.textContent = "checkpointLog.push(document.currentScript.id); Promise.resolve().then(() => checkpointLog.push(document.currentScript?.id ?? null));";
2348
+ document.head.appendChild(nested);
2349
+ checkpointLog.push('after nested');
2350
+ Promise.resolve().then(() => Promise.resolve().then(() => checkpointLog.push(document.currentScript?.id ?? null)));
2351
+ setTimeout(() => checkpointLog.push(document.currentScript?.id ?? null), 0);
2352
+ </script>`);
2353
+ await Bun.sleep(50);
2354
+ expect(await evaluate('checkpointLog')).toEqual(['nested', 'after nested', 'outer', 'outer', null]);
2355
+ expect(await evaluate('document.currentScript')).toBeNull();
2356
+ });
2357
+
2358
+ test('primitive and literal extension methods stay page-local without exposing host capabilities',async()=>{
2359
+ await load(`<script>
2360
+ Number.prototype.pageClamp=function(a,b){return Math.max(a,Math.min(b,this));};
2361
+ Number.prototype.strictReceiver=function(){'use strict';return [typeof this,this];};
2362
+ String.prototype.pageSuffix=function(){return this+'!';};
2363
+ Boolean.prototype.pageToggle=function(){return !this.valueOf();};
2364
+ Array.prototype.pageDrop=function(){return this.pop();};
2365
+ Function.prototype.pageInvoke=function(...args){return this.apply(null,args);};
2366
+ </script><script>
2367
+ window.primitiveResults=[(300).pageClamp(0,255),(-1)['pageClamp'](0,255),'test'.pageSuffix(),true.pageToggle(),[4,5].pageDrop(),(x=>x+1).pageInvoke(3),(2).strictReceiver(),(1).constructor===Number,'a'.constructor===String];
2368
+ window.keyReads=0;const key={[Symbol.toPrimitive](){keyReads++;return 'pageClamp'}};
2369
+ primitiveResults.push((5)[key](1,3),keyReads,eval("(3).pageClamp(0,2)"),Object(1) instanceof Number,new Number(1) instanceof Number);
2370
+ window.escapeResults=[typeof Bun,typeof process,typeof require,Function('return typeof Bun')(),Function('return typeof process')(),(async()=>{}).constructor('return typeof Bun')() instanceof Promise];
2371
+ window.sharedFrozen=[Object.isFrozen(Object.getPrototypeOf(function(){})),Object.isFrozen(Object.getPrototypeOf([]))];
2372
+ </script>`);
2373
+ expect(JSON.parse(await evaluate('JSON.stringify(primitiveResults)'))).toEqual([255,0,'test!',false,5,4,['number',2],true,true,3,1,2,true,true]);
2374
+ expect(JSON.parse(await evaluate('JSON.stringify(escapeResults)'))).toEqual(['undefined','undefined','undefined','undefined','undefined',true]);
2375
+ expect(JSON.parse(await evaluate('JSON.stringify(sharedFrozen)'))).toEqual([true,true]);
2376
+ // A navigation reuses the renderer but must not retain any of these extensions.
2377
+ await load('<script>window.clean=[typeof (1).pageClamp,typeof "a".pageSuffix,typeof true.pageToggle,typeof [].pageDrop,typeof (function(){}).pageInvoke,typeof Bun,typeof process];</script>');
2378
+ expect(JSON.parse(await evaluate('JSON.stringify(clean)'))).toEqual(Array(7).fill('undefined'));
2379
+ });
2380
+
2381
+ test('CanvasRenderingContext2D has a page-local prototype for feature detection and method extensions',async()=>{
2382
+ await load('<canvas id="surface" width="20" height="20"></canvas>');
2383
+ expect(await evaluate(`(()=>{
2384
+ const ctx=document.getElementById('surface').getContext('2d');
2385
+ CanvasRenderingContext2D.prototype.redBox=function(){this.fillStyle='red';this.fillRect(0,0,20,20)};
2386
+ ctx.redBox();
2387
+ return [ctx instanceof CanvasRenderingContext2D,Object.getPrototypeOf(ctx)===CanvasRenderingContext2D.prototype,typeof CanvasRenderingContext2D.prototype.roundRect,ctx.getImageData(5,5,1,1).data[0]].join(',');
2388
+ })()`)).toBe('true,true,function,255');
2389
+ await load('<canvas id="surface"></canvas>');
2390
+ expect(await evaluate('typeof CanvasRenderingContext2D.prototype.redBox')).toBe('undefined');
2391
+ });
2392
+
2393
+ test('decoded video dimensions invalidate retained intrinsic layout without a DOM mutation',async()=>{
2394
+ await load('<video id="intrinsicVideo" src="/video.mp4"></video><div id="afterVideo">after</div>');
2395
+ const before=await evaluate('intrinsicVideo.getBoundingClientRect().width');
2396
+ expect(before).toBe(300);
2397
+ await evaluate('intrinsicVideo.play().then(()=>intrinsicVideo.pause())');
2398
+ const size=await evaluate(`(()=>{const r=intrinsicVideo.getBoundingClientRect();return [intrinsicVideo.videoWidth,intrinsicVideo.videoHeight,r.width,r.height]})()`);
2399
+ expect(size[0]).toBeGreaterThan(0);expect(size[1]).toBeGreaterThan(0);
2400
+ expect(size[2]).toBe(size[0]);expect(size[3]).toBe(size[1]);
2401
+ });
@@ -14,7 +14,9 @@ test("CDP raw keydown uses the physical key even with an empty text field", () =
14
14
  expect(notes.at(-1)).toBe(38);
15
15
  expect(handlePianoInput(input, { key: " ", code: "Space", text: " ", repeat: true }, play)).toBe(true);
16
16
  expect(notes).toHaveLength(4);
17
- expect(handlePianoInput(input, { key: "Tab", code: "Tab", text: "" }, play)).toBe(false);
17
+ expect(handlePianoInput(input, { key: "Tab", code: "Tab", text: "" }, play)).toBe(true);
18
+ expect(notes.at(-1)).toBe(47);
19
+ expect(handlePianoInput(input, { key: "Shift", code: "ShiftRight", text: "" }, play)).toBe(false);
18
20
  });
19
21
 
20
22
  test("piano consumes batched terminal/IME text and plays every mapped note", () => {
@@ -32,10 +34,21 @@ test("piano consumes repeats without retriggering and supports shifted physical
32
34
  expect(handlePianoInput(input, { key: "J", code: "KeyJ", modifiers: 8 }, play)).toBe(true);
33
35
  expect(handlePianoInput(input, { key: "j", modifiers: 2 }, play)).toBe(true);
34
36
  expect(notes).toEqual([71]);
35
- expect(handlePianoInput(input, { key: "Tab" }, play)).toBe(false);
37
+ expect(handlePianoInput(input, { key: "Alt", code: "AltLeft", modifiers: 1 }, play)).toBe(false);
36
38
  expect(handlePianoInput(input, { key: "Backspace" }, play)).toBe(false);
37
39
  });
38
40
 
41
+ test("left-hand control keys and AltGr play even while reporting their own modifier", () => {
42
+ const notes = [];
43
+ const play = n => notes.push(n);
44
+ for (const [key, code] of [["Escape", "Escape"], ["`", "Backquote"], ["Tab", "Tab"], ["CapsLock", "CapsLock"]])
45
+ expect(handlePianoInput(input, { key, code }, play)).toBe(true);
46
+ expect(handlePianoInput(input, { key: "Shift", code: "ShiftLeft", modifiers: 8 }, play)).toBe(true);
47
+ expect(handlePianoInput(input, { key: "AltGraph", code: "AltRight", modifiers: 3 }, play)).toBe(true);
48
+ expect(handlePianoInput(input, { key: "Alt", code: "AltRight", modifiers: 1 }, play)).toBe(true);
49
+ expect(notes).toEqual([35, 42, 47, 50, 54, 64, 64]);
50
+ });
51
+
39
52
  test("ordinary, disabled and readonly editors are unaffected", () => {
40
53
  expect(handlePianoInput({ getAttribute: () => "editor" }, { text: "jk" })).toBe(false);
41
54
  expect(handlePianoInput({ ...input, disabled: true }, { text: "jk" })).toBe(false);
@@ -2,7 +2,7 @@ import { test, expect } from "bun:test";
2
2
  import { PIANO_KEYS, PIANO_DO, pianoNote, pianoTone, PianoMixer } from "../lib/audio/piano.js";
3
3
 
4
4
  test("piano keyboard mapping is editable in one table", () => {
5
- expect(Object.keys(PIANO_KEYS)).toHaveLength(61);
5
+ expect(Object.keys(PIANO_KEYS)).toHaveLength(66);
6
6
  expect(PIANO_DO).toBe(69);
7
7
  expect(PIANO_KEYS[" "]).toBe(60);
8
8
  expect(pianoNote(" ")).toBe(69);
@@ -16,7 +16,16 @@ test("piano keyboard mapping is editable in one table", () => {
16
16
  expect(pianoNote("/", "Slash")).toBe(71);
17
17
  expect(pianoNote("Unidentified", "KeyJ")).toBe(71);
18
18
  expect(pianoNote("!", "Digit1")).toBe(45);
19
- expect(pianoNote("Escape")).toBeNull();
19
+ expect(pianoNote("Escape", "Escape")).toBe(35);
20
+ expect(pianoNote("`", "Backquote")).toBe(42);
21
+ expect(pianoNote("Tab", "Tab")).toBe(47);
22
+ expect(pianoNote("CapsLock", "CapsLock")).toBe(50);
23
+ expect(pianoNote("Shift", "ShiftLeft")).toBe(54);
24
+ expect(pianoNote("Shift", "ShiftRight")).toBeNull();
25
+ expect(pianoNote("Alt", "AltRight")).toBe(64);
26
+ expect(pianoNote("AltGraph")).toBe(64);
27
+ expect(pianoNote("Alt", "AltLeft")).toBeNull();
28
+ expect(pianoNote("Backspace")).toBeNull();
20
29
  });
21
30
 
22
31
  test("piano tone is 48 kHz mono PCM with a nonzero, decaying sound", () => {
@@ -29,3 +29,14 @@ test("short clips drain and piano stays interactive", () => {
29
29
  expect(piano.request(0, 0, 0, 120)).toBe(120);
30
30
  expect(piano.request(0, 120, 0, 120)).toBe(0);
31
31
  });
32
+
33
+
34
+ test('live synthesis prepares a small reserve while server credits are delayed, without startup gating',()=>{
35
+ const live=new PlaybackBuffer(1000,2,.12,.12);
36
+ expect(live.ready(20,0,false)).toBe(true);
37
+ expect(live.request(0,0,0,0)).toBe(120);
38
+ expect(live.request(0,120,0,0)).toBe(0);
39
+ expect(live.request(20,0,40,0)).toBe(60);
40
+ expect(live.request(20,60,40,0)).toBe(0);
41
+ expect(live.request(0,0,200,0)).toBe(0);
42
+ });
@@ -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);
@@ -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(1e7)] : [];
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
+ });