buninu-browser 0.2.4 → 0.4.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/LICENSES/jsmdcui.txt +22 -0
- package/README.md +102 -2
- package/frontends/wayland/LICENSE-land +21 -0
- package/frontends/wayland/README.md +123 -0
- package/frontends/wayland/casty/LICENSE +21 -0
- package/frontends/wayland/casty/bookmarks.js +32 -0
- package/frontends/wayland/casty/click.js +96 -0
- package/frontends/wayland/casty/commands.js +61 -0
- package/frontends/wayland/casty/config.js +45 -0
- package/frontends/wayland/casty/download-directory.js +28 -0
- package/frontends/wayland/casty/download.js +49 -0
- package/frontends/wayland/casty/hints.js +334 -0
- package/frontends/wayland/casty/keys.js +56 -0
- package/frontends/wayland/casty/page-commands.js +80 -0
- package/frontends/wayland/casty/prompt-width.js +148 -0
- package/frontends/wayland/casty/urlbar.js +606 -0
- package/frontends/wayland/client.js +88 -0
- package/frontends/wayland/clipboard.js +80 -0
- package/frontends/wayland/index.js +116 -0
- package/frontends/wayland/input.js +56 -0
- package/frontends/wayland/protocol.js +55 -0
- package/frontends/wayland/protocols/wayland.xml +3248 -0
- package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
- package/frontends/wayland/transport.js +120 -0
- package/frontends/wayland/urlbar.js +158 -0
- package/index.js +28 -1
- package/lib/clipboard-command.js +10 -0
- package/lib/clipboard-powershell.js +22 -0
- package/lib/clipboard.js +57 -0
- package/lib/document-types.js +11 -0
- package/lib/headless-shell.js +78 -13
- package/lib/layout/text-layout.js +17 -9
- package/lib/paint/display-list.js +5 -1
- package/lib/paint/frame.js +35 -3
- package/lib/paint/raw-frames.js +47 -0
- package/lib/qml/README.md +971 -0
- package/lib/qml/builtins.js +700 -0
- package/lib/qml/canvas-dom.js +115 -0
- package/lib/qml/document.js +1176 -0
- package/lib/qml/format.js +9 -0
- package/lib/qml/host.js +324 -0
- package/lib/qml/images.js +199 -0
- package/lib/qml/launcher.js +132 -0
- package/lib/qml/loader.js +517 -0
- package/lib/qml/media-renderer.js +49 -0
- package/lib/qml/media.js +86 -0
- package/lib/qml/models.js +622 -0
- package/lib/qml/parser.js +381 -0
- package/lib/qml/qs +5 -0
- package/lib/qml/qscasty +3 -0
- package/lib/qml/qsheadless +3 -0
- package/lib/qml/qswayland +3 -0
- package/lib/qml/reactive.js +159 -0
- package/lib/qml/runtime.js +3381 -0
- package/lib/qml/shapes.js +243 -0
- package/lib/qml/text.js +109 -0
- package/lib/qml/textures.js +123 -0
- package/lib/qml/wayland-clipboard.js +32 -0
- package/lib/qml/worker-thread.js +70 -0
- package/lib/render-tree/index.js +16 -1
- package/lib/renderer/bindings.js +11 -0
- package/lib/renderer/canvas-2d.js +13 -0
- package/lib/renderer/compositor-channel.js +103 -0
- package/lib/renderer/compositor.js +25 -33
- package/lib/renderer/flags.js +1 -1
- package/lib/renderer/host.js +128 -92
- package/lib/renderer/main.js +38 -4
- package/lib/renderer/page-realm.js +32 -1
- package/lib/renderer/page-renderer.js +250 -21
- package/lib/renderer/text-input-paint.js +54 -0
- package/lib/renderer/text-input.js +57 -0
- package/lib/style/computed-style.js +2 -2
- package/package.json +1 -1
- package/test/clipboard.test.js +54 -0
- package/test/fixtures/browser-raw-demo.html +36 -0
- package/test/fixtures/flea-helpers/Dockerfile +36 -0
- package/test/fixtures/flea-helpers/bat.mjs +43 -0
- package/test/fixtures/flea-helpers/bin/bat +3 -0
- package/test/fixtures/flea-helpers/bin/bwrap +10 -0
- package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
- package/test/fixtures/flea-helpers/bin/glow +3 -0
- package/test/fixtures/flea-helpers/bin/qs +3 -0
- package/test/fixtures/flea-helpers/bin/unsupported +3 -0
- package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
- package/test/fixtures/flea-helpers/check-media.mjs +29 -0
- package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
- package/test/fixtures/flea-helpers/check.mjs +36 -0
- package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
- package/test/fixtures/flea-helpers/source-mime.xml +28 -0
- package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
- package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
- package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
- package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea/LICENSE +21 -0
- package/test/fixtures/qml/flea/README.md +25 -0
- package/test/fixtures/qml/flea/Theme.qml +8 -0
- package/test/fixtures/qml/flea/demo.qml +24 -0
- package/test/fixtures/qml/flea/qmldir +3 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
- package/test/fixtures/qml/flea-theme/README.md +70 -0
- package/test/fixtures/qml/flea-theme/button.qml +17 -0
- package/test/fixtures/qml/flea-theme/demo.qml +27 -0
- package/test/fixtures/qml/flea-theme/field.qml +21 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
- package/test/fixtures/qml/flea-theme/segment.qml +17 -0
- package/test/fixtures/qml/flea-theme/settings.qml +58 -0
- package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
- package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
- package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
- package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
- package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
- package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
- package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
- package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
- package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
- package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
- package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
- package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
- package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
- package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
- package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
- package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
- package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
- package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
- package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
- package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
- package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
- package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
- package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
- package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
- package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
- package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
- package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
- package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
- package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
- package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
- package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
- package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
- package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
- package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
- package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
- package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
- package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
- package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
- package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
- package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
- package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
- package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
- package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
- package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
- package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
- package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
- package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
- package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
- package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
- package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
- package/test/fixtures/qml/hello.qml +26 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
- package/test/fixtures/qml/imports/components/Badge.qml +15 -0
- package/test/fixtures/qml/imports/logic/base.js +2 -0
- package/test/fixtures/qml/imports/logic/counter.js +4 -0
- package/test/fixtures/qml/imports/logic/math.js +3 -0
- package/test/fixtures/qml/imports/logic/shared.js +3 -0
- package/test/fixtures/qml/imports/main.qml +21 -0
- package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
- package/test/fixtures/qml/imports/theme/qmldir +2 -0
- package/test/fixtures/qml/multiply.qml +61 -0
- package/test/fixtures/qml-canvas-dom.js +90 -0
- package/test/fixtures/qml-media-audio.js +39 -0
- package/test/fixtures/raw-capture-benchmark.json +76 -0
- package/test/helpers/flea-sandbox.js +48 -0
- package/test/helpers/quickshell-fixture.js +125 -0
- package/test/html-clipboard-wayland.test.js +47 -0
- package/test/index.test.js +13 -0
- package/test/manual-casty-without-qml.js +87 -0
- package/test/manual-flea-application.js +49 -0
- package/test/manual-flea-binary.js +62 -0
- package/test/manual-flea-qml.js +9 -0
- package/test/manual-flea-settings.js +40 -0
- package/test/manual-flea-theme.js +40 -0
- package/test/manual-qml-removal.js +46 -0
- package/test/manual-without-qml.js +45 -0
- package/test/qml-canvas-dom.test.js +37 -0
- package/test/qml-clipboard-bridge.test.js +25 -0
- package/test/qml-clipboard-wayland.test.js +40 -0
- package/test/qml-connections-timing.test.js +73 -0
- package/test/qml-dynamic.test.js +401 -0
- package/test/qml-flea-app.test.js +146 -0
- package/test/qml-flea-backend.test.js +78 -0
- package/test/qml-flea-columns-backend.test.js +38 -0
- package/test/qml-flea-compile.test.js +78 -0
- package/test/qml-flea-launcher.test.js +88 -0
- package/test/qml-flea-markdown-backend.test.js +31 -0
- package/test/qml-flea-media.test.js +28 -0
- package/test/qml-flea-navigation-backend.test.js +35 -0
- package/test/qml-flea-navigation-timing.test.js +147 -0
- package/test/qml-flea-preview-backend.test.js +53 -0
- package/test/qml-flea-previews.test.js +163 -0
- package/test/qml-flea-rows.test.js +101 -0
- package/test/qml-flea-textures.test.js +59 -0
- package/test/qml-flea-view-switch.test.js +33 -0
- package/test/qml-flea-wayland.test.js +68 -0
- package/test/qml-flea.test.js +34 -0
- package/test/qml-host.test.js +238 -0
- package/test/qml-imports.test.js +102 -0
- package/test/qml-inherited-handlers.test.js +51 -0
- package/test/qml-input-selection.test.js +66 -0
- package/test/qml-interactions.test.js +309 -0
- package/test/qml-language.test.js +167 -0
- package/test/qml-launcher.test.js +54 -0
- package/test/qml-layout-text.test.js +205 -0
- package/test/qml-media.test.js +63 -0
- package/test/qml-persisted-delegates.test.js +45 -0
- package/test/qml-quickshell-controls.test.js +65 -0
- package/test/qml-quickshell-settings.test.js +62 -0
- package/test/qml-repeater.test.js +72 -0
- package/test/qml-style-updates.test.js +124 -0
- package/test/qml-textures.test.js +66 -0
- package/test/qml-views.test.js +332 -0
- package/test/qml-worker.test.js +69 -0
- package/test/qml.test.js +83 -0
- package/test/raw-capture.test.js +152 -0
- package/test/renderer-host.test.js +11 -0
- package/test/text-input-pointer.test.js +31 -0
- package/test/text-input-scroll.test.js +29 -0
- package/test/text-input.test.js +47 -0
- package/test/wayland-frontend.test.js +83 -0
- package/test/wayland-urlbar.test.js +38 -0
- package/test/window-resize.test.js +36 -0
- package/tools/bench-raw-capture.js +32 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {readFileSync,existsSync,unlinkSync} from 'node:fs';
|
|
3
|
+
import {dirname} from 'node:path';
|
|
4
|
+
import {RendererHost} from '../lib/renderer/host.js';
|
|
5
|
+
import {PacketReader,encodePacket,listenCompositor,connectCompositor} from '../lib/renderer/compositor-channel.js';
|
|
6
|
+
import {RawFrames} from '../lib/paint/raw-frames.js';
|
|
7
|
+
import CanvasKitInit from '../usr/lib/canvaskit/canvaskit.js';
|
|
8
|
+
const kit=CanvasKitInit({wasmBinary:readFileSync(new URL('../usr/lib/canvaskit/canvaskit.wasm',import.meta.url))});
|
|
9
|
+
|
|
10
|
+
test('binary channel handles fragmented packets, rejects oversized input and authenticates peers',async()=>{
|
|
11
|
+
const results=[];const reader=new PacketReader((message,pixels)=>results.push([message,[...pixels]]));
|
|
12
|
+
const bytes=Buffer.concat([encodePacket({id:1},Uint8Array.of(1,2,3)),encodePacket({id:2})]);
|
|
13
|
+
for(let i=0;i<bytes.length;i++)reader.push(bytes.subarray(i,i+1));
|
|
14
|
+
expect(results).toEqual([[{id:1},[1,2,3]],[{id:2},[]]]);
|
|
15
|
+
const header=Buffer.alloc(8);header.writeUInt32LE(0xffffffff,0);
|
|
16
|
+
expect(()=>new PacketReader(()=>{}).push(header)).toThrow('size');
|
|
17
|
+
let reply;const got=new Promise(resolve=>reply=resolve);
|
|
18
|
+
const server=await listenCompositor((message,pixels)=>{server.send({id:message.id,length:pixels.length});},()=>{});
|
|
19
|
+
let peer;
|
|
20
|
+
try{
|
|
21
|
+
await expect(connectCompositor({...server.endpoint,token:'wrong'},()=>{},()=>{})).rejects.toThrow();
|
|
22
|
+
peer=await connectCompositor(server.endpoint,message=>reply(message),()=>{});
|
|
23
|
+
peer.send({id:8},new Uint8Array(1024));expect(await got).toEqual({id:8,length:1024});
|
|
24
|
+
}finally{peer?.close();server.close();}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('raw file owner rejects malformed input and bounds unconsumed frames',()=>{
|
|
28
|
+
const store=new RawFrames();let path;
|
|
29
|
+
try{
|
|
30
|
+
expect(()=>store.write(new Uint8Array(3),1,1,'rgba')).toThrow('Invalid');
|
|
31
|
+
for(let i=0;i<16;i++)path=JSON.parse(store.write(new Uint8Array(4),1,1,'rgba')).path;
|
|
32
|
+
expect(()=>store.write(new Uint8Array(4),1,1,'rgba')).toThrow('stalled');
|
|
33
|
+
}finally{store.close();}
|
|
34
|
+
expect(existsSync(dirname(path))).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
for(const compositor of [true,false])test(`raw screenshots match PNG and stay fresh after consumer deletion (compositor=${compositor})`,async()=>{
|
|
38
|
+
const host=new RendererHost({compositor});let directory;
|
|
39
|
+
try{
|
|
40
|
+
await host.start();await host.call('resize',32,24,1);
|
|
41
|
+
await host.call('loadDocument',{url:'https://page.test/',source:'<body style="margin:0;background:rgb(211,73,19)">',contentType:'text/html'});
|
|
42
|
+
await host.call('whenLoaded');
|
|
43
|
+
const png=await host.composite('screenshot',{format:'png'});
|
|
44
|
+
const CK=await kit;const image=CK.MakeImageFromEncoded(Buffer.from(png,'base64'));
|
|
45
|
+
const reference=image.readPixels(0,0,{width:32,height:24,colorType:CK.ColorType.RGBA_8888,alphaType:CK.AlphaType.Unpremul,colorSpace:CK.ColorSpace.SRGB});image.delete();
|
|
46
|
+
for(const format of ['rgba','bgra']){
|
|
47
|
+
const frame=JSON.parse(await host.composite('screenshot',{format}));directory=dirname(frame.path);
|
|
48
|
+
const pixels=readFileSync(frame.path);expect(pixels.length).toBe(32*24*4);
|
|
49
|
+
if(format==='bgra')for(let i=0;i<pixels.length;i+=4){const r=pixels[i];pixels[i]=pixels[i+2];pixels[i+2]=r;}
|
|
50
|
+
expect([...pixels]).toEqual([...reference]);unlinkSync(frame.path);
|
|
51
|
+
const next=JSON.parse(await host.composite('screenshot',{format}));expect(next.path).not.toBe(frame.path);expect(existsSync(next.path)).toBe(true);unlinkSync(next.path);
|
|
52
|
+
}
|
|
53
|
+
if(compositor){
|
|
54
|
+
const busy=host.call('evaluate',"(()=>{const end=Date.now()+1800;while(Date.now()<end);return 1})()");await Bun.sleep(150);
|
|
55
|
+
const start=performance.now();const raw=JSON.parse(await host.composite('screenshot',{format:'bgra'}));
|
|
56
|
+
expect(performance.now()-start).toBeLessThan(1400);unlinkSync(raw.path);await busy;
|
|
57
|
+
}
|
|
58
|
+
}finally{host.close();}
|
|
59
|
+
expect(existsSync(directory)).toBe(false);
|
|
60
|
+
},20000);
|
|
61
|
+
|
|
62
|
+
test('normal CDP routes raw requests to the controller without changing standard screenshots',async()=>{
|
|
63
|
+
const {createBrowser}=await import('../lib/headless-shell.js');
|
|
64
|
+
const {CdpServer}=await import('../lib/cdp-server.js');
|
|
65
|
+
const {CDP}=await import('../tools/browser-lab.js');
|
|
66
|
+
const browser=await createBrowser({spareRenderer:false});
|
|
67
|
+
const server=CdpServer.create(browser.context).listen(0,'127.0.0.1');
|
|
68
|
+
const cdp=new CDP(`ws://127.0.0.1:${server.port}/devtools/browser/cdp-server`);
|
|
69
|
+
let directory;
|
|
70
|
+
try{
|
|
71
|
+
await cdp.ready;const {targetId}=await cdp.send('Target.createTarget',{url:'about:blank'});
|
|
72
|
+
const {sessionId}=await cdp.send('Target.attachToTarget',{targetId,flatten:true});cdp.sessionId=sessionId;
|
|
73
|
+
await cdp.send('Emulation.setDeviceMetricsOverride',{width:16,height:9,deviceScaleFactor:1.75,mobile:false});
|
|
74
|
+
const metrics=await cdp.send('Page.getLayoutMetrics');
|
|
75
|
+
expect(metrics.cssVisualViewport).toMatchObject({pageX:0,pageY:0});
|
|
76
|
+
const {data}=await cdp.send('Page.captureScreenshot',{format:'bgra',clip:{x:0,y:0,width:16,height:15/1.75,scale:1}});
|
|
77
|
+
const frame=JSON.parse(data);directory=dirname(frame.path);
|
|
78
|
+
expect(frame).toMatchObject({version:1,transport:'file',format:'bgra',width:28,height:15});expect(existsSync(frame.path)).toBe(true);
|
|
79
|
+
cdp.events.length=0;
|
|
80
|
+
await cdp.send('Page.startScreencast',{format:'png',maxWidth:4,maxHeight:2,clip:{x:0,y:0,width:4,height:2,scale:1},everyNthFrame:1});
|
|
81
|
+
let event;for(let i=0;i<100;i++){event=cdp.events.find(e=>e.method==='Page.screencastFrame');if(event)break;await Bun.sleep(20);}
|
|
82
|
+
expect(event).toBeDefined();
|
|
83
|
+
const crop=Buffer.from(event.params.data,'base64');
|
|
84
|
+
// Native 1.75 density is preserved, rather than rescaling to maxWidth/maxHeight.
|
|
85
|
+
expect([crop.readUInt32BE(16),crop.readUInt32BE(20)]).toEqual([7,4]);
|
|
86
|
+
await cdp.send('Page.screencastFrameAck',{sessionId:event.params.sessionId});
|
|
87
|
+
await cdp.send('Page.stopScreencast');
|
|
88
|
+
const png=await cdp.send('Page.captureScreenshot',{format:'png'});
|
|
89
|
+
expect([...Buffer.from(png.data,'base64').subarray(0,4)]).toEqual([137,80,78,71]);
|
|
90
|
+
}finally{cdp.close();server.stop(true);browser.close();}
|
|
91
|
+
expect(existsSync(directory)).toBe(false);
|
|
92
|
+
},20000);
|
|
93
|
+
|
|
94
|
+
test('closing during listener startup rejects promptly and a new start remains usable',async()=>{
|
|
95
|
+
const host=new RendererHost();
|
|
96
|
+
const pending=host.start();host.close();
|
|
97
|
+
await expect(pending).rejects.toThrow('closed');
|
|
98
|
+
try{await host.start();expect(host.pid).toBeGreaterThan(0);}finally{host.close();}
|
|
99
|
+
},15000);
|
|
100
|
+
|
|
101
|
+
for(const compositor of [true,false])test(`fractional CDP clips preserve exact physical dimensions and raw pixels (compositor=${compositor})`,async()=>{
|
|
102
|
+
const host=new RendererHost({compositor});
|
|
103
|
+
try {
|
|
104
|
+
await host.start();await host.call('resize',16,9,1.75);
|
|
105
|
+
await host.call('loadDocument',{url:'https://clip.test/',contentType:'text/html',source:'<body style="margin:0;background:red"><div style="height:3px;background:blue"></div><div style="height:100px"></div>'});
|
|
106
|
+
await host.call('whenLoaded');
|
|
107
|
+
const full=JSON.parse(await host.composite('screenshot',{format:'rgba'}));
|
|
108
|
+
expect([full.width,full.height]).toEqual([28,16]);
|
|
109
|
+
const clip={x:0,y:0,width:16,height:15/1.75,scale:1};
|
|
110
|
+
const cropped=JSON.parse(await host.composite('screenshot',{format:'rgba',clip}));
|
|
111
|
+
expect([cropped.width,cropped.height]).toEqual([28,15]);
|
|
112
|
+
expect(readFileSync(cropped.path)).toEqual(readFileSync(full.path).subarray(0,28*15*4));
|
|
113
|
+
const CK=await kit;const png=CK.MakeImageFromEncoded(Buffer.from(await host.composite('screenshot',{format:'png',clip}),'base64'));
|
|
114
|
+
expect([png.width(),png.height()]).toEqual([28,15]);png.delete();
|
|
115
|
+
await host.composite('scroll',0,4);
|
|
116
|
+
const offset=await host.composite('scrollOffset');
|
|
117
|
+
const scrolled=JSON.parse(await host.composite('screenshot',{format:'bgra',clip:{...clip,y:offset.y}}));
|
|
118
|
+
expect([...readFileSync(scrolled.path).subarray(0,4)]).toEqual([0,0,255,255]);
|
|
119
|
+
await expect(host.composite('screenshot',{format:'rgba',clip:{...clip,width:Infinity}})).rejects.toThrow();
|
|
120
|
+
}finally{host.close();}
|
|
121
|
+
},20000);
|
|
122
|
+
|
|
123
|
+
test('scale-one full-width fractional clip borrows contiguous raster bytes',async()=>{
|
|
124
|
+
const {captureViewport}=await import('../lib/paint/frame.js');
|
|
125
|
+
const tile={x:0,y:0,scale:1.75,pixelWidth:28,pixelHeight:16,pixels:new Uint8Array(28*16*4)};
|
|
126
|
+
const frame=captureViewport(tile,0,0,16,9,{x:0,y:0,width:16,height:15/1.75,scale:1});
|
|
127
|
+
expect(frame.pixels.buffer).toBe(tile.pixels.buffer);
|
|
128
|
+
expect(frame.pixels.byteLength).toBe(28*15*4);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('in-flight raw capture follows an origin renderer swap without exposing compositor closed',async()=>{
|
|
132
|
+
const {createBrowser}=await import('../lib/headless-shell.js');
|
|
133
|
+
const server=Bun.serve({port:0,fetch:()=>new Response('<body style="margin:0;background:rgb(211,73,19)">',{headers:{'content-type':'text/html'}})});
|
|
134
|
+
const browser=await createBrowser({spareRenderer:false});
|
|
135
|
+
const original=RendererHost.prototype.composite;let rejectCapture,armed=true;
|
|
136
|
+
const reached=Promise.withResolvers();
|
|
137
|
+
RendererHost.prototype.composite=function(method,...params){
|
|
138
|
+
if(armed&&method==='screenshot'){
|
|
139
|
+
armed=false;reached.resolve();return new Promise((_,reject)=>rejectCapture=reject);
|
|
140
|
+
}
|
|
141
|
+
return original.call(this,method,...params);
|
|
142
|
+
};
|
|
143
|
+
try{
|
|
144
|
+
const pending=browser.context.screenshot({format:'bgra'});
|
|
145
|
+
await reached.promise;
|
|
146
|
+
await browser.context.navigate(`http://127.0.0.1:${server.port}/`);
|
|
147
|
+
rejectCapture(Error('compositor closed'));
|
|
148
|
+
const frame=JSON.parse(await pending);const bytes=readFileSync(frame.path);
|
|
149
|
+
expect(frame.format).toBe('bgra');expect([...bytes.subarray(0,4)]).toEqual([19,73,211,255]);
|
|
150
|
+
unlinkSync(frame.path);
|
|
151
|
+
}finally{RendererHost.prototype.composite=original;browser.close();server.stop(true);}
|
|
152
|
+
},20000);
|
|
@@ -70,3 +70,14 @@ test("fetches only http(s)/data resources on the renderer's behalf", async () =>
|
|
|
70
70
|
.toEqual({ error: "blocked script-with-cookies request for https:" });
|
|
71
71
|
expect(requested).toEqual(["https://example.test/a.css"]);
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
test("explicit close prevents queued calls and captures from restarting the renderer", async () => {
|
|
76
|
+
const renderer = host();
|
|
77
|
+
const queued = renderer.call("title");
|
|
78
|
+
renderer.close();
|
|
79
|
+
await expect(queued).rejects.toThrow("Renderer is closed");
|
|
80
|
+
await expect(renderer.start()).rejects.toThrow("Renderer is closed");
|
|
81
|
+
await expect(renderer.composite("screenshot", { format: "bgra" })).rejects.toThrow("Renderer is closed");
|
|
82
|
+
expect(renderer.pid).toBeNull();
|
|
83
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {createBrowser} from '../lib/headless-shell.js';
|
|
3
|
+
test('pointer places the caret at shaped text, including selected and scrolled controls',async()=>{
|
|
4
|
+
const b=await createBrowser({spareRenderer:false});const run=s=>b.context.evaluate(s);
|
|
5
|
+
try{
|
|
6
|
+
await b.context.resize(500,400);
|
|
7
|
+
await b.context.navigate('data:text/html,'+encodeURIComponent(`<style>body{margin:0}input,textarea{display:block;margin:10px;padding:0;border:0;font:20px monospace;line-height:24px;width:160px}input{height:24px}textarea{height:72px}</style><input id="i" value="abcdefghij"><textarea id="t">abcde\nfghij\nklmno</textarea><input id="p" type="password" value="a😀bcd">`));
|
|
8
|
+
const pos=()=>run('({start:document.activeElement.selectionStart,end:document.activeElement.selectionEnd})');
|
|
9
|
+
// 20px monospace advances approximately 12px. Click the boundary after b.
|
|
10
|
+
await b.context.click(34,20);expect(await pos()).toEqual({start:2,end:2});
|
|
11
|
+
await b.context.press('a',{modifiers:2});await b.context.click(46,20);expect(await pos()).toEqual({start:3,end:3});
|
|
12
|
+
await b.context.type('X');expect(await run('document.getElementById("i").value')).toBe('abcXdefghij');
|
|
13
|
+
await run('document.getElementById("i").value="abcdefghijklmnopqrstuvwxyz"');
|
|
14
|
+
await b.context.press('End');
|
|
15
|
+
expect(await run('document.getElementById("i").scrollLeft')).toBeGreaterThan(0);
|
|
16
|
+
await b.context.click(45,20);const scrolled=await pos();expect(scrolled.start).toBeGreaterThan(10);expect(scrolled.start).toBeLessThan(26);
|
|
17
|
+
const top=await run('document.getElementById("t").getBoundingClientRect().top');
|
|
18
|
+
await b.context.click(34,top+36);expect(await pos()).toEqual({start:8,end:8});
|
|
19
|
+
await b.context.click(400,top+36); // outside must not reposition
|
|
20
|
+
expect(await pos()).toEqual({start:8,end:8});
|
|
21
|
+
await run('document.getElementById("t").value="abcde ".repeat(20);document.getElementById("t").focus();document.getElementById("t").setSelectionRange(0,0)');
|
|
22
|
+
await b.context.click(34,top+36);const wrapped=await pos();expect(wrapped.start).toBeGreaterThan(10);expect(wrapped.start).toBeLessThan(30);
|
|
23
|
+
await b.context.press('End',{modifiers:2});
|
|
24
|
+
expect(await run('document.getElementById("t").scrollTop')).toBeGreaterThan(0);
|
|
25
|
+
await b.context.click(34,top+12);expect((await pos()).start).toBeGreaterThan(60);expect((await pos()).start).toBeLessThan(120);
|
|
26
|
+
const passTop=await run('document.getElementById("p").getBoundingClientRect().top');
|
|
27
|
+
await b.context.click(34,passTop+12);expect(await pos()).toEqual({start:3,end:3});
|
|
28
|
+
await run('document.getElementById("i").onmousedown=e=>e.preventDefault();document.getElementById("i").focus();document.getElementById("i").setSelectionRange(5,5)');
|
|
29
|
+
await b.context.click(12,20);expect(await pos()).toEqual({start:5,end:5});
|
|
30
|
+
}finally{b.close();}
|
|
31
|
+
},30000);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {createBrowser} from '../lib/headless-shell.js';
|
|
3
|
+
import {loadGraphics} from '../lib/renderer/page-renderer.js';
|
|
4
|
+
test('text controls clip overflow, keep long inputs on one line and reveal the caret',async()=>{
|
|
5
|
+
const b=await createBrowser({spareRenderer:false});
|
|
6
|
+
const run=s=>b.context.evaluate(s);
|
|
7
|
+
try {
|
|
8
|
+
await b.context.resize(400,300);
|
|
9
|
+
await b.context.navigate('data:text/html,'+encodeURIComponent(`<style>body{margin:0;background:white}input,textarea{display:block;width:200px;box-sizing:border-box;border:1px solid black;padding:4px;font:18px monospace;margin:10px}input{height:36px}textarea{height:100px}</style><input id="i" value="${'abcdef'.repeat(20)}"><textarea id="t">${Array.from({length:12},(_,i)=>'第'+i+'行 abcdef').join('\n')}</textarea><button id="b">after</button>`));
|
|
10
|
+
const state=id=>run(`(()=>{const e=document.getElementById('${id}');return {x:e.scrollLeft,y:e.scrollTop,w:e.scrollWidth,h:e.scrollHeight,rect:e.getBoundingClientRect().toJSON()}})()`);
|
|
11
|
+
const initial=await state('i'),area=await state('t');
|
|
12
|
+
expect(area.rect.y).toBe(56);
|
|
13
|
+
expect((await run(`document.getElementById('b').getBoundingClientRect().top`))).toBeLessThan(180);
|
|
14
|
+
const {CanvasKit:k}=await loadGraphics();
|
|
15
|
+
const ink=async(x,y,w,h)=>{
|
|
16
|
+
const bytes=Buffer.from(await b.context.screenshot({format:'png'}),'base64'),im=k.MakeImageFromEncoded(bytes);
|
|
17
|
+
try{const p=im.readPixels(x,y,{width:w,height:h,colorType:k.ColorType.RGBA_8888,alphaType:k.AlphaType.Unpremul,colorSpace:k.ColorSpace.SRGB});let count=0;for(let i=0;i<p.length;i+=4)if(p[i]<180&&p[i+1]<180&&p[i+2]<180)count++;return count;}finally{im.delete();}
|
|
18
|
+
};
|
|
19
|
+
expect(await ink(212,10,180,145)).toBe(0);
|
|
20
|
+
for(const id of ['i','t']) {
|
|
21
|
+
await run(`document.getElementById('${id}').focus();document.getElementById('${id}').setSelectionRange(0,0)`);
|
|
22
|
+
await b.context.press('End',{modifiers:2});
|
|
23
|
+
const end=await state(id);expect(id==='i'?end.x:end.y).toBeGreaterThan(0);
|
|
24
|
+
if(id==='i'){expect(end.h).toBe(initial.h);expect(await ink(16,16,180,20)).toBeGreaterThan(100);}
|
|
25
|
+
await b.context.press('Home',{modifiers:2});const home=await state(id);expect(id==='i'?home.x:home.y).toBe(0);
|
|
26
|
+
await b.context.press('End',{modifiers:10});const selected=await state(id);expect(id==='i'?selected.x:selected.y).toBeGreaterThan(0);
|
|
27
|
+
}
|
|
28
|
+
}finally{b.close();}
|
|
29
|
+
},30000);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {RendererHost} from '../lib/renderer/host.js';
|
|
3
|
+
|
|
4
|
+
test('HTML controls share selection, clipboard and blinking; API reads require user activation',async()=>{
|
|
5
|
+
let clipboard='貼上!',reads=0;
|
|
6
|
+
const host=new RendererHost({clipboardRead:async()=>{reads++;return clipboard;},clipboardWrite:async text=>{clipboard=text;}});
|
|
7
|
+
const run=s=>host.call('evaluate',s);
|
|
8
|
+
const wait=async predicate=>{for(let i=0;i<60;i++){if(await predicate())return;await Bun.sleep(25);}throw Error('Timed out');};
|
|
9
|
+
try {
|
|
10
|
+
await host.start();await host.call('resize',400,200,1);
|
|
11
|
+
await host.call('loadDocument',{url:'https://input.test/',contentType:'text/html',source:`<input id="i" value="abc😀xyz" style="width:300px;height:30px"><textarea id="t">abcd\nxy\n12345</textarea><button id="b">read</button><script>document.getElementById('b').onclick=async()=>{try{window.result=await navigator.clipboard.readText()}catch(e){window.result=e.name}}</script>`});
|
|
12
|
+
await host.call('whenLoaded');
|
|
13
|
+
await run(`document.getElementById('i').focus()`);
|
|
14
|
+
const state=()=>run(`(()=>{let e=document.getElementById('i');return [e.selectionStart,e.selectionEnd,e.value]})()`);
|
|
15
|
+
await host.call('press','a',{modifiers:2});expect((await state()).slice(0,2)).toEqual([0,8]);
|
|
16
|
+
await host.call('press','ArrowRight');expect((await state()).slice(0,2)).toEqual([8,8]);
|
|
17
|
+
await host.call('press','ArrowLeft',{modifiers:8});expect((await state()).slice(0,2)).toEqual([7,8]);
|
|
18
|
+
await host.call('press','c',{modifiers:2});await wait(()=>clipboard==='z');
|
|
19
|
+
await host.call('press','x',{modifiers:2});await wait(async()=>(await state())[2]==='abc😀xy');
|
|
20
|
+
clipboard='漢';await host.call('press','v',{modifiers:2});await wait(async()=>(await state())[2]==='abc😀xy漢');
|
|
21
|
+
await run(`document.getElementById('i').onkeydown=e=>{if(e.ctrlKey)e.preventDefault()}`);
|
|
22
|
+
await host.call('press','a',{modifiers:2});expect((await state()).slice(0,2)).toEqual([8,8]);
|
|
23
|
+
await run(`document.getElementById('t').focus();document.getElementById('t').setSelectionRange(3,3)`);
|
|
24
|
+
await host.call('press','ArrowDown',{modifiers:8});
|
|
25
|
+
expect(await run(`[document.getElementById('t').selectionStart,document.getElementById('t').selectionEnd]`)).toEqual([3,7]);
|
|
26
|
+
await host.call('press','ArrowDown',{modifiers:8});
|
|
27
|
+
expect(await run(`[document.getElementById('t').selectionStart,document.getElementById('t').selectionEnd]`)).toEqual([3,11]);
|
|
28
|
+
await run(`window.result='';document.getElementById('b').click()`);await wait(async()=>(await run('window.result'))!=='');
|
|
29
|
+
// The prior user action has already been consumed by paste, but arrow presses
|
|
30
|
+
// are also user activation. Consume once, then a synthetic click must fail.
|
|
31
|
+
await run(`window.result='';document.getElementById('b').click()`);await wait(async()=>(await run('window.result'))!=='');
|
|
32
|
+
expect(await run('window.result')).toBe('NotAllowedError');
|
|
33
|
+
const b=await run(`document.getElementById('b').getBoundingClientRect().toJSON()`);
|
|
34
|
+
await host.call('click',b.x+b.width/2,b.y+b.height/2);
|
|
35
|
+
await wait(async()=>(await run('window.result'))==='漢');
|
|
36
|
+
expect(reads).toBeGreaterThan(1);
|
|
37
|
+
await run(`document.getElementById('i').onkeydown=null;document.getElementById('i').focus();document.getElementById('i').select();document.getElementById('i').onbeforeinput=e=>e.preventDefault()`);
|
|
38
|
+
clipboard='blocked';await host.call('press','v',{modifiers:2});await Bun.sleep(100);
|
|
39
|
+
expect((await state())[2]).toBe('abc😀xy漢');
|
|
40
|
+
await run(`document.getElementById('i').type='password'`);
|
|
41
|
+
await host.call('press','c',{modifiers:2});await Bun.sleep(100);expect(clipboard).toBe('blocked');
|
|
42
|
+
await run(`document.getElementById('i').type='text'`);
|
|
43
|
+
await run(`document.getElementById('i').focus();document.getElementById('i').setSelectionRange(1,1)`);
|
|
44
|
+
const on=await host.call('screenshot',{format:'png'});await Bun.sleep(550);
|
|
45
|
+
const off=await host.call('screenshot',{format:'png'});expect(off).not.toBe(on);
|
|
46
|
+
} finally {host.close();}
|
|
47
|
+
},30000);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {encodeRequest,decode,interfaces} from '../frontends/wayland/protocol.js';
|
|
3
|
+
import {parseKeymap,keysym} from '../frontends/wayland/input.js';
|
|
4
|
+
import {chooseFrontend} from '../index.js';
|
|
5
|
+
|
|
6
|
+
test('Wayland launcher remains opt-in',()=>{
|
|
7
|
+
expect(chooseFrontend(['--wayland','about:blank'])).toEqual({frontend:'wayland',args:['about:blank']});
|
|
8
|
+
expect(chooseFrontend(['about:blank']).frontend).toBe('casty');
|
|
9
|
+
});
|
|
10
|
+
test('registry bind encodes dynamic new_id and SHM fd stays out of payload',()=>{
|
|
11
|
+
const packet=encodeRequest(2,'wl_registry','bind',[7,'wl_compositor',4,3]);
|
|
12
|
+
expect(decode(interfaces.get('wl_registry').requests[0],packet.data,[])).toEqual([7,'wl_compositor',4,3]);
|
|
13
|
+
const pool=encodeRequest(4,'wl_shm','create_pool',[8,91,4096]);
|
|
14
|
+
expect(pool.fds).toEqual([91]);expect(pool.data.length).toBe(16);
|
|
15
|
+
expect(decode(interfaces.get('wl_shm').requests[0],pool.data,[91])).toEqual([8,91,4096]);
|
|
16
|
+
expect(decode(interfaces.get('wl_shm').requests[0],pool.data,[])).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
test('bounded keymap subset understands named and Unicode keysyms',()=>{
|
|
19
|
+
const map=parseKeymap('<K30> = 38; key <K30> { type="ALPHABETIC", [ 0x61, 0x41 ] };');
|
|
20
|
+
expect(map.get(30).map(keysym)).toEqual(['a','A']);expect(keysym('0x01004e2d')).toBe('中');
|
|
21
|
+
expect(keysym('0xff0d')).toBe('Enter');expect(keysym('0xffe1')).toBeNull();expect(keysym('Left')).toBe('ArrowLeft');expect(keysym('dead_acute')).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Optional sibling integration; never installs prerequisites or touches a running compositor.
|
|
25
|
+
test.skipIf(process.env.BUNINU_TEST_WAYLAND!=='1')('Browser SHM, pointer, keyboard and resize in Land',async()=>{
|
|
26
|
+
const {mkdtempSync,rmSync}=await import('node:fs');
|
|
27
|
+
const {start}=await import('../../buninu-land/compositor.js');
|
|
28
|
+
const {launch}=await import('../frontends/wayland/index.js');
|
|
29
|
+
const dir=mkdtempSync('/tmp/buninu-wayland-test-'),old=process.env.XDG_RUNTIME_DIR;
|
|
30
|
+
process.env.XDG_RUNTIME_DIR=dir;let land,app;
|
|
31
|
+
const wait=async(fn)=>{for(let i=0;i<500;i++){if(await fn())return;await Bun.sleep(20);}throw Error('Timed out waiting for Wayland state');};
|
|
32
|
+
try{
|
|
33
|
+
land=await start(['--remote-debugging-port=0','--window-size=640,480']);
|
|
34
|
+
app=await launch({socket:dir+'/wayland-buninu',url:'data:text/html,'+encodeURIComponent('<title>Ready</title><body style="margin:0;background:rgb(255,0,255)"><input id="text" style="width:200px;height:60px" oninput="document.title=this.value"><button style="position:absolute;left:0;top:100px;width:200px;height:60px" onclick="document.title=\'Clicked\'">Click</button>')});
|
|
35
|
+
await wait(()=>land.compositor.wm.visible[0]?.surface.xdg.toplevel.title==='Ready');
|
|
36
|
+
const surface=()=>land.compositor.wm.renderSurfaces().find(s=>s.pixels);
|
|
37
|
+
await wait(()=>surface());expect(surface().pixels.width).toBe(app.window.width);
|
|
38
|
+
const click=async(x,y)=>{const o=land.compositor.wm.origin(surface());for(const type of ['mouseMoved','mousePressed','mouseReleased'])land.context.dispatchMouseEvent({type,x:o.x+x,y:o.y+y,button:'left'});};
|
|
39
|
+
await click(30,157);await wait(()=>land.compositor.wm.visible[0].surface.xdg.toplevel.title==='Clicked');
|
|
40
|
+
await click(30,57);land.context.type('hello');
|
|
41
|
+
await wait(()=>land.compositor.wm.visible[0].surface.xdg.toplevel.title==='hello');
|
|
42
|
+
// Editing browser chrome must not suspend page frames. Conversely, blinking
|
|
43
|
+
// chrome alone must reuse the last raw page frame rather than recapture it.
|
|
44
|
+
let captures=0,presents=0;
|
|
45
|
+
const screenshot=app.browser.context.screenshot.bind(app.browser.context);
|
|
46
|
+
app.browser.context.screenshot=(...args)=>{captures++;return screenshot(...args);};
|
|
47
|
+
const present=app.window.present.bind(app.window);
|
|
48
|
+
app.window.present=(...args)=>{presents++;return present(...args);};
|
|
49
|
+
await app.browser.context.evaluate('document.activeElement.blur()');
|
|
50
|
+
app.toolbar.open();
|
|
51
|
+
await Bun.sleep(800);
|
|
52
|
+
const idleCaptures=captures,idlePresents=presents;
|
|
53
|
+
await Bun.sleep(1100);
|
|
54
|
+
expect(captures).toBe(idleCaptures);
|
|
55
|
+
expect(presents).toBeGreaterThan(idlePresents);
|
|
56
|
+
await app.browser.context.evaluate(`globalThis.animationTick=0;globalThis.animationTimer=setInterval(()=>{document.body.style.background=++animationTick%2?'red':'blue'},80)`);
|
|
57
|
+
const animatedCaptures=captures,animatedPresents=presents;
|
|
58
|
+
await wait(()=>captures>=animatedCaptures+3&&presents>=animatedPresents+3);
|
|
59
|
+
expect(app.toolbar.editor.editing).toBe(true);
|
|
60
|
+
expect(Number(await app.browser.context.evaluate('animationTick'))).toBeGreaterThan(2);
|
|
61
|
+
await app.browser.context.evaluate('clearInterval(animationTimer)');
|
|
62
|
+
app.toolbar.editor.cancelEditing();
|
|
63
|
+
const before=app.window.width;land.context.resize(900,700,1);
|
|
64
|
+
await wait(()=>app.window.width!==before&&surface().pixels.width===app.window.width);
|
|
65
|
+
expect(JSON.parse(await app.browser.context.evaluate('JSON.stringify([innerWidth,innerHeight])'))).toEqual([app.window.width,app.window.height-32]);
|
|
66
|
+
for(const type of ['keyDown','keyUp'])land.context.dispatchKeyEvent({type,key:'w',code:'KeyW',modifiers:4});
|
|
67
|
+
await wait(()=>app.client.connection.closed&&land.compositor.wm.visible.length===0);
|
|
68
|
+
await app.done;
|
|
69
|
+
expect(app.window.closed).toBe(true);
|
|
70
|
+
}finally{app?.close();land?.close();if(old===undefined)delete process.env.XDG_RUNTIME_DIR;else process.env.XDG_RUNTIME_DIR=old;rmSync(dir,{recursive:true,force:true});}
|
|
71
|
+
},30000);
|
|
72
|
+
|
|
73
|
+
test('Wayland input queued around close never calls the disposed renderer',async()=>{
|
|
74
|
+
const {attachInput}=await import('../frontends/wayland/input.js');
|
|
75
|
+
let capabilities,pointer,calls=0,errors=0,reject;
|
|
76
|
+
const client={globals:new Map([['wl_seat',{}]]),connection:{closed:false},bind(_i,_v,fn){capabilities=fn;return 2;},object(iface,fn){if(iface==='wl_pointer')pointer=fn;return 3;},request(){}};
|
|
77
|
+
const window={surface:4,closed:false};
|
|
78
|
+
attachInput(client,window,{move(){calls++;return new Promise((_r,no)=>reject=no);}},()=>errors++);
|
|
79
|
+
capabilities('capabilities',[1]);pointer('enter',[1,4,0,0]);pointer('motion',[1,3,4]);
|
|
80
|
+
await Promise.resolve();expect(calls).toBe(1);
|
|
81
|
+
window.closed=true;reject(Error('Renderer closed'));pointer('motion',[2,4,5]);
|
|
82
|
+
await Bun.sleep(0);expect(calls).toBe(1);expect(errors).toBe(0);
|
|
83
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {WaylandUrlBar,WAYLAND_COMMANDS} from '../frontends/wayland/urlbar.js';
|
|
3
|
+
const history={list:()=>[],add(){}};
|
|
4
|
+
test('Wayland chrome retains Casty selection, insert-paste, mode switch and commands',async()=>{
|
|
5
|
+
let copied='',quit=false;const navigated=[];
|
|
6
|
+
const context={url:'https://example.com/',navigate:async url=>navigated.push(url)};
|
|
7
|
+
const bar=new WaylandUrlBar(context,{history,close:()=>quit=true,clipboard:{read:async()=>'/x',write:async text=>copied=text}});
|
|
8
|
+
try{
|
|
9
|
+
bar.updateURL(context.url);bar.open();
|
|
10
|
+
expect(bar.editor.selectedText).toBe(context.url);
|
|
11
|
+
await bar.press('ArrowRight');expect(bar.editor.selectedText).toBe('');
|
|
12
|
+
await bar.press('ArrowLeft',{modifiers:8});expect(bar.editor.selectedText).toBe('/');
|
|
13
|
+
await bar.press('c',{modifiers:2});expect(copied).toBe('/');
|
|
14
|
+
await bar.press('v',{modifiers:2});expect(bar.editor.text).toBe('https://example.com/x');
|
|
15
|
+
await bar.press('ArrowRight');await bar.press('v',{modifiers:2});
|
|
16
|
+
expect(bar.editor.text).toBe('https://example.com/x/x');
|
|
17
|
+
await bar.press('ArrowLeft',{modifiers:8});
|
|
18
|
+
await bar.press('c',{modifiers:1});expect(copied).toBe('x');
|
|
19
|
+
await bar.press('Tab');expect(bar.editor.mode).toBe('cmd');
|
|
20
|
+
await bar.press('Tab');expect(bar.editor.mode).toBe('url');
|
|
21
|
+
await bar.press('Escape');expect(bar.editor.editing).toBe(false);
|
|
22
|
+
await bar.command('open https://example.org/');expect(navigated).toEqual(['https://example.org/']);
|
|
23
|
+
expect(WAYLAND_COMMANDS.some(c=>c.name==='cdp')).toBe(false);
|
|
24
|
+
await bar.command('q');expect(quit).toBe(true);
|
|
25
|
+
}finally{bar.dispose();}
|
|
26
|
+
});
|
|
27
|
+
test('Toolbar raster cache and horizontal scrolling preserve page-independent rendering',async()=>{
|
|
28
|
+
const bar=await new WaylandUrlBar({},{history}).initialize();
|
|
29
|
+
try{
|
|
30
|
+
bar.updateURL('https://example.com/'+'long-path/'.repeat(20));
|
|
31
|
+
const pixels=bar.pixels(320);expect(pixels.length).toBe(320*32*4);expect(bar.pixels(320)).toBe(pixels);
|
|
32
|
+
bar.open();await bar.press('ArrowRight');bar.pixels(320);
|
|
33
|
+
expect(bar.editor._scrollX).toBeGreaterThan(0);
|
|
34
|
+
await bar.press('Home');bar.pixels(320);expect(bar.editor._scrollX).toBe(0);
|
|
35
|
+
bar.editor.pointerDown(5,1000);bar.editor.pointerMove(10);bar.editor.pointerUp();
|
|
36
|
+
expect(bar.editor.selectedText.length).toBeGreaterThan(0);
|
|
37
|
+
}finally{bar.dispose();}
|
|
38
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {test, expect} from 'bun:test';
|
|
2
|
+
import {RendererHost} from '../lib/renderer/host.js';
|
|
3
|
+
|
|
4
|
+
test('viewport resize notifies window listeners with fresh geometry and repaints their changes',async()=>{
|
|
5
|
+
const host=new RendererHost({timeout:20000});
|
|
6
|
+
try {
|
|
7
|
+
await host.start();
|
|
8
|
+
await host.call('resize',320,240,1);
|
|
9
|
+
await host.call('loadDocument',{url:'https://resize.test/',contentType:'text/html',
|
|
10
|
+
source:'<body style="margin:0"><div id="box" style="width:100%;height:10px"></div><canvas id="canvas"></canvas></body>'});
|
|
11
|
+
await host.call('whenLoaded');
|
|
12
|
+
await host.call('evaluate',`window.events=[];window.propertyEvents=0;
|
|
13
|
+
window.onresize=()=>window.propertyEvents++;
|
|
14
|
+
window.addEventListener('resize',e=>{
|
|
15
|
+
const c=document.getElementById('canvas');
|
|
16
|
+
c.width=innerWidth;c.height=innerHeight;
|
|
17
|
+
events.push([innerWidth,innerHeight,devicePixelRatio,
|
|
18
|
+
document.getElementById('box').getBoundingClientRect().width,
|
|
19
|
+
e.target===window,e.bubbles,e.cancelable]);
|
|
20
|
+
document.body.style.backgroundColor='rgb(17,34,51)';
|
|
21
|
+
});`);
|
|
22
|
+
await host.call('resize',400,300,1.5);
|
|
23
|
+
expect(await host.call('evaluate','JSON.stringify(events)')).toBe('[[400,300,1.5,400,true,false,false]]');
|
|
24
|
+
expect(await host.call('evaluate','propertyEvents')).toBe(1);
|
|
25
|
+
expect(await host.call('evaluate',`[document.getElementById('canvas').width,document.getElementById('canvas').height].join(',')`)).toBe('400,300');
|
|
26
|
+
const png=Buffer.from(await host.composite('screenshot',{format:'png'}),'base64');
|
|
27
|
+
expect([png.readUInt32BE(16),png.readUInt32BE(20)]).toEqual([600,450]);
|
|
28
|
+
expect(await host.call('evaluate',"getComputedStyle(document.body).backgroundColor")).toBe('rgba(17, 34, 51, 1)');
|
|
29
|
+
await host.call('resize',400,300,1.5);
|
|
30
|
+
await host.call('resize',400,300,2);
|
|
31
|
+
expect(await host.call('evaluate','events.length')).toBe(1);
|
|
32
|
+
await host.call('resize',400,200,2);
|
|
33
|
+
expect(await host.call('evaluate','JSON.stringify(events[1])')).toBe('[400,200,2,400,true,false,false]');
|
|
34
|
+
expect(await host.call('evaluate','propertyEvents')).toBe(2);
|
|
35
|
+
}finally{host.close();}
|
|
36
|
+
},30000);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Run with Bun on the host to measure capture delivery, not terminal FPS.
|
|
2
|
+
// Uses a local synthetic page, the normal renderer sandbox and changing crops.
|
|
3
|
+
import {RendererHost} from '../lib/renderer/host.js';
|
|
4
|
+
import {statSync,unlinkSync} from 'node:fs';
|
|
5
|
+
const count=Number(process.argv.find(a=>a.startsWith('--samples='))?.split('=')[1] ?? 30);
|
|
6
|
+
if(!Number.isInteger(count)||count<1||count>1000)throw Error('Invalid sample count');
|
|
7
|
+
const host=new RendererHost();
|
|
8
|
+
const reports=[];
|
|
9
|
+
try{
|
|
10
|
+
await host.start();await host.call('resize',1280,690,1.5);
|
|
11
|
+
await host.call('loadDocument',{url:'https://benchmark.test/',contentType:'text/html',source:'<body style="margin:0">'+Array.from({length:80},(_,i)=>`<div style="height:44px;background:rgb(${i*17%256},${i*37%256},${i*71%256})">Frame ${i} 中文 ABCDEFG 0123456789</div>`).join('')});
|
|
12
|
+
await host.call('whenLoaded');await Bun.sleep(500);
|
|
13
|
+
const formats=process.argv.find(a=>a.startsWith('--formats='))?.slice('--formats='.length).split(',') ?? ['png','jpeg','rgba','bgra','bgra','rgba','jpeg','png'];
|
|
14
|
+
for(const format of formats){
|
|
15
|
+
if(!['png','jpeg','rgba','bgra'].includes(format))throw Error('Invalid format');
|
|
16
|
+
const samples=[];let bytes=0;
|
|
17
|
+
for(let i=0;i<count+3;i++){
|
|
18
|
+
await host.composite('scroll',0,i%2?17:-17);
|
|
19
|
+
const start=performance.now();
|
|
20
|
+
const data=await host.composite('screenshot',{format,quality:85,optimizeForSpeed:true});
|
|
21
|
+
const ms=performance.now()-start;
|
|
22
|
+
if(format==='rgba'||format==='bgra'){
|
|
23
|
+
const f=JSON.parse(data);bytes=statSync(f.path).size;unlinkSync(f.path);
|
|
24
|
+
}else bytes=Buffer.byteLength(data,'base64');
|
|
25
|
+
if(i>=3)samples.push(ms);
|
|
26
|
+
}
|
|
27
|
+
samples.sort((a,b)=>a-b);
|
|
28
|
+
const report={format,count,meanMs:samples.reduce((a,b)=>a+b,0)/count,medianMs:samples[Math.floor(count/2)],bytes};
|
|
29
|
+
reports.push(report);console.log(JSON.stringify(report));
|
|
30
|
+
}
|
|
31
|
+
console.log(JSON.stringify({sandbox:host.sandbox,capture:[1920,1035],reports}));
|
|
32
|
+
}finally{host.close();}
|