buninu-browser 0.3.0 → 0.5.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 +40 -4
- package/frontends/wayland/LICENSE-land +21 -0
- package/frontends/wayland/README.md +152 -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 +130 -0
- package/frontends/wayland/input.js +75 -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 +162 -0
- package/index.js +28 -1
- package/lib/audio/shared-connection.js +31 -0
- package/lib/cdp-server.js +15 -2
- package/lib/clipboard-command.js +10 -0
- package/lib/clipboard-powershell.js +22 -0
- package/lib/clipboard.js +37 -2
- package/lib/document-types.js +11 -0
- package/lib/headless-shell.js +54 -12
- package/lib/layout/text-layout.js +17 -9
- package/lib/paint/display-list.js +6 -1
- package/lib/qml/README.md +1427 -0
- package/lib/qml/builtins.js +771 -0
- package/lib/qml/canvas-dom.js +115 -0
- package/lib/qml/canvas.js +76 -0
- package/lib/qml/document.js +1199 -0
- package/lib/qml/fonts.js +69 -0
- package/lib/qml/format.js +9 -0
- package/lib/qml/host.js +352 -0
- package/lib/qml/images.js +199 -0
- package/lib/qml/launcher.js +132 -0
- package/lib/qml/loader.js +521 -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 +182 -0
- package/lib/qml/runtime.js +3603 -0
- package/lib/qml/shader/fragment.js +83 -0
- package/lib/qml/shader/qsb.js +27 -0
- package/lib/qml/shader/renderer.js +39 -0
- package/lib/qml/shader/runtime.js +61 -0
- package/lib/qml/shapes.js +251 -0
- package/lib/qml/shortcuts.js +65 -0
- package/lib/qml/text.js +109 -0
- package/lib/qml/textures.js +126 -0
- package/lib/qml/wayland-clipboard.js +32 -0
- package/lib/qml/window-constraints.js +16 -0
- package/lib/qml/worker-thread.js +70 -0
- package/lib/render-tree/index.js +16 -1
- package/lib/renderer/bindings.js +16 -1
- package/lib/renderer/canvas-2d.js +29 -0
- package/lib/renderer/document-selection.js +19 -0
- package/lib/renderer/host.js +45 -17
- package/lib/renderer/main.js +21 -6
- package/lib/renderer/page-realm.js +20 -10
- package/lib/renderer/page-renderer.js +497 -42
- package/lib/renderer/pointer-move.js +5 -3
- package/lib/renderer/render-interval.js +9 -0
- package/lib/renderer/text-input-paint.js +54 -0
- package/lib/renderer/text-input.js +57 -0
- package/lib/style/computed-style.js +10 -4
- package/package.json +1 -1
- package/test/audio-connection-lifetime.test.js +46 -0
- package/test/canvas-paint.test.js +8 -0
- package/test/clipboard.test.js +19 -0
- package/test/computed-style.test.js +12 -0
- package/test/document-selection.test.js +27 -0
- package/test/double-click.test.js +85 -0
- package/test/fixtures/canvas-damage.js +40 -0
- package/test/fixtures/fake-renderer.js +1 -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/omastorm-helpers/Dockerfile +35 -0
- package/test/fixtures/omastorm-helpers/check.mjs +56 -0
- package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
- package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
- package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
- package/test/fixtures/pointer-paint-batching.js +23 -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-fonts/main.qml +10 -0
- package/test/fixtures/qml-locale/check.mjs +6 -0
- package/test/fixtures/qml-locale/main.qml +8 -0
- package/test/fixtures/qml-media-audio.js +39 -0
- package/test/fixtures/qml-shader/grab.qml +14 -0
- package/test/fixtures/qml-shader/gradient.frag +5 -0
- package/test/fixtures/qml-shader/main.qml +14 -0
- package/test/fixtures/qml-shader/texture-grab.qml +25 -0
- package/test/fixtures/qml-shader/texture-size.frag +5 -0
- package/test/fixtures/qml-shader-check.mjs +24 -0
- package/test/fixtures/title-no-paint.js +20 -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-double-click.html +21 -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-mouse-buttons.html +18 -0
- package/test/manual-omastorm-engine.mjs +19 -0
- package/test/manual-omastorm-shaders.mjs +28 -0
- package/test/manual-omastorm-software.mjs +25 -0
- package/test/manual-pointer-scroll.html +43 -0
- package/test/manual-qml-removal.js +46 -0
- package/test/manual-without-qml.js +45 -0
- package/test/pointer-drag.test.js +104 -0
- package/test/qml-canvas-dom.test.js +37 -0
- package/test/qml-canvas.test.js +21 -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 +95 -0
- package/test/qml-controls-window.test.js +182 -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-fonts.test.js +31 -0
- package/test/qml-grid-multiline.test.js +48 -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-locale.test.js +45 -0
- package/test/qml-media.test.js +63 -0
- package/test/qml-nonvisual-visible.test.js +19 -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-shader-runtime.test.js +6 -0
- package/test/qml-shortcuts.test.js +39 -0
- package/test/qml-socket.test.js +60 -0
- package/test/qml-software-shader.test.js +55 -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-window-update.test.js +36 -0
- package/test/qml-worker.test.js +69 -0
- package/test/qml.test.js +83 -0
- package/test/render-interval.test.js +33 -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/title-no-paint.test.js +6 -0
- package/test/wayland-frontend.test.js +83 -0
- package/test/wayland-scroll.test.js +40 -0
- package/test/wayland-urlbar.test.js +61 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {RendererHost} from '../lib/renderer/host.js';
|
|
3
|
+
|
|
4
|
+
test('Window.update coalesces requests and emits frame signals, including reentrant requests',async()=>{
|
|
5
|
+
const host=new RendererHost({timeout:20000});
|
|
6
|
+
try {
|
|
7
|
+
await host.start();
|
|
8
|
+
await host.call('loadDocument',{url:'data:text/x-qml,window-update',contentType:'text/x-qml',source:`import QtQuick
|
|
9
|
+
import Quickshell
|
|
10
|
+
FloatingWindow {width:200;height:100
|
|
11
|
+
Item {id:child;Component.onCompleted:globalThis.testWindow=child.Window.window}
|
|
12
|
+
}`});
|
|
13
|
+
await host.call('whenLoaded');
|
|
14
|
+
const result=await host.call('evaluate',`(()=>{
|
|
15
|
+
const original=requestAnimationFrame, cancel=cancelAnimationFrame;
|
|
16
|
+
const queue=new Map();let next=0,animated=0,synced=0;
|
|
17
|
+
globalThis.requestAnimationFrame=fn=>{queue.set(++next,fn);return next};
|
|
18
|
+
globalThis.cancelAnimationFrame=id=>queue.delete(id);
|
|
19
|
+
const w=testWindow;
|
|
20
|
+
const animate=()=>{animated++;if(animated===1)w.update()};
|
|
21
|
+
const sync=()=>synced++;
|
|
22
|
+
const step=()=>{const [id,fn]=queue.entries().next().value;queue.delete(id);fn(0)};
|
|
23
|
+
try {
|
|
24
|
+
w.update();w.update();const coalesced=queue.size;
|
|
25
|
+
w.afterAnimating.connect(animate);w.afterSynchronizing.connect(sync);
|
|
26
|
+
step();const reentrant=queue.size;
|
|
27
|
+
step();
|
|
28
|
+
w.afterAnimating.disconnect(animate);w.afterSynchronizing.disconnect(sync);
|
|
29
|
+
step();const idle=queue.size;
|
|
30
|
+
w.update();step();step();const idleAfterUnobserved=queue.size;
|
|
31
|
+
return {coalesced,reentrant,animated,synced,idle,idleAfterUnobserved};
|
|
32
|
+
}finally{globalThis.requestAnimationFrame=original;globalThis.cancelAnimationFrame=cancel}
|
|
33
|
+
})()`);
|
|
34
|
+
expect(result).toEqual({coalesced:1,reentrant:1,animated:2,synced:2,idle:0,idleAfterUnobserved:0});
|
|
35
|
+
}finally{host.close()}
|
|
36
|
+
},30000);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { createBrowser } from '../lib/headless-shell.js';
|
|
5
|
+
async function run(files, source, verify) {
|
|
6
|
+
const dir = mkdtempSync('/tmp/qml-worker-');
|
|
7
|
+
let browser;
|
|
8
|
+
try {
|
|
9
|
+
for (const [name, text] of Object.entries({ ...files, 'main.qml': source }))
|
|
10
|
+
await Bun.write(join(dir, name), text);
|
|
11
|
+
browser = await createBrowser({ spareRenderer: false });
|
|
12
|
+
await browser.context.navigate(
|
|
13
|
+
Bun.pathToFileURL(join(dir, 'main.qml')).href,
|
|
14
|
+
);
|
|
15
|
+
const read = () =>
|
|
16
|
+
browser.context.evaluate('document.getElementById("result").textContent');
|
|
17
|
+
let text;
|
|
18
|
+
for (let i = 0; i < 200; i++) {
|
|
19
|
+
text = await read();
|
|
20
|
+
if (text !== 'waiting') break;
|
|
21
|
+
await Bun.sleep(20);
|
|
22
|
+
}
|
|
23
|
+
await verify(text, browser);
|
|
24
|
+
} finally {
|
|
25
|
+
browser?.close();
|
|
26
|
+
rmSync(dir, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
test('WorkerScript includes shared script scope, clones messages and keeps independent worker state', async () => {
|
|
30
|
+
await run(
|
|
31
|
+
{
|
|
32
|
+
'worker.js':
|
|
33
|
+
'Qt.include("helper.js");var count=0;WorkerScript.onMessage=function(m){m.answer=twice(m.n)+ ++count;WorkerScript.sendMessage(m);};',
|
|
34
|
+
'helper.js': '.pragma library\nfunction twice(n){return n*2;}',
|
|
35
|
+
},
|
|
36
|
+
`import QtQuick
|
|
37
|
+
Text {id:result;text:"waiting";property var input:({n:4});property var replies:[]
|
|
38
|
+
WorkerScript {id:a;source:"worker.js";onMessage:function(m){result.replies=result.replies.concat([m.answer]);if(result.replies.length===2)result.text=JSON.stringify(result.replies)+":"+result.input.answer}}
|
|
39
|
+
WorkerScript {id:b;source:"worker.js";onMessage:function(m){result.replies=result.replies.concat([m.answer]);if(result.replies.length===2)result.text=JSON.stringify(result.replies)+":"+result.input.answer}}
|
|
40
|
+
Component.onCompleted:{a.sendMessage(input);b.sendMessage(input)}
|
|
41
|
+
}`,
|
|
42
|
+
(text) => expect(text).toBe('[9,9]:undefined'),
|
|
43
|
+
);
|
|
44
|
+
}, 30000);
|
|
45
|
+
test.skipIf(!process.env.BUNINU_FLEA_ROOT)(
|
|
46
|
+
'unchanged Flea MarkdownWorker parses headings and paragraphs in a real worker',
|
|
47
|
+
async () => {
|
|
48
|
+
const url = Bun.pathToFileURL(
|
|
49
|
+
resolve(process.env.BUNINU_FLEA_ROOT, 'ui/MarkdownWorker.js'),
|
|
50
|
+
).href;
|
|
51
|
+
await run(
|
|
52
|
+
{},
|
|
53
|
+
`import QtQuick
|
|
54
|
+
Text {id:result;text:"waiting"
|
|
55
|
+
WorkerScript {id:worker;source:${JSON.stringify(url)};onMessage:function(m){result.text=JSON.stringify(m)}}
|
|
56
|
+
Component.onCompleted:worker.sendMessage({seq:12,source:"# Hello\\n\\nA **bold** paragraph.",dir:"/tmp",chrome:"#000000",ink:"#ffffff"})
|
|
57
|
+
}`,
|
|
58
|
+
(text) => {
|
|
59
|
+
const result = JSON.parse(text);
|
|
60
|
+
expect(result.error).toBe('');
|
|
61
|
+
expect(result.seq).toBe(12);
|
|
62
|
+
expect(result.blocks.length).toBeGreaterThan(1);
|
|
63
|
+
expect(JSON.stringify(result.blocks)).toContain('Hello');
|
|
64
|
+
expect(JSON.stringify(result.blocks)).toContain('bold');
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
30000,
|
|
69
|
+
);
|
package/test/qml.test.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {parseQml} from '../lib/qml/parser.js';
|
|
3
|
+
import {createQmlDocument} from '../lib/qml/document.js';
|
|
4
|
+
import {isQmlDocument} from '../lib/qml/format.js';
|
|
5
|
+
import {displayDocument} from '../lib/document-types.js';
|
|
6
|
+
import {RendererHost} from '../lib/renderer/host.js';
|
|
7
|
+
const url='https://qml.test/hello.qml';
|
|
8
|
+
const source=await Bun.file(new URL('./fixtures/qml/hello.qml',import.meta.url)).text();
|
|
9
|
+
|
|
10
|
+
test('QML builds DOM directly, preserves source through response classification',()=>{
|
|
11
|
+
const response=displayDocument({url,contentType:'text/plain',body:new TextEncoder().encode(source)});
|
|
12
|
+
expect(response).toEqual({source,contentType:'text/x-qml'});
|
|
13
|
+
expect(isQmlDocument(url,'text/html')).toBe(false);
|
|
14
|
+
const {document}=createQmlDocument(source,url);
|
|
15
|
+
expect(document.querySelectorAll('[data-qml-node]').length).toBe(4);
|
|
16
|
+
expect(document.querySelector('#root')).not.toBeNull();
|
|
17
|
+
expect(document.querySelector('script').textContent).toContain('addEventListener');
|
|
18
|
+
});
|
|
19
|
+
test('unsupported imports/types are visible diagnostics and source is inert',()=>{
|
|
20
|
+
const {document}=createQmlDocument('import Evil\nRectangle { color: "<script>boom</script>" }',url);
|
|
21
|
+
expect(document.body.textContent).toContain('Unsupported QML import');
|
|
22
|
+
expect(document.querySelector('script')).toBeNull();
|
|
23
|
+
expect(()=>parseQml('Rectangle { width: }')).toThrow();
|
|
24
|
+
const disabled=createQmlDocument(source,url,{scripting:false}).document;
|
|
25
|
+
expect(disabled.querySelector('script')).toBeNull();
|
|
26
|
+
expect(disabled.body.textContent).toContain('requires page scripting');
|
|
27
|
+
});
|
|
28
|
+
test('Acorn handles modern JS expressions and braces inside strings',()=>{
|
|
29
|
+
const ast=parseQml('import QtQuick\nText { text: ({a: "}"})?.a ?? "fallback"; onClicked: { console.log("}"); } }');
|
|
30
|
+
expect(ast.root.properties.length).toBe(2);
|
|
31
|
+
});
|
|
32
|
+
test('QML uses normal realm, reactive DOM, input, viewport resize and navigation',async()=>{
|
|
33
|
+
const host=new RendererHost({timeout:20000});
|
|
34
|
+
try{
|
|
35
|
+
await host.start(); await host.call('resize',640,360,1);
|
|
36
|
+
await host.call('loadDocument',{url,source,contentType:'text/x-qml'}); await host.call('whenLoaded');
|
|
37
|
+
expect(await host.call('evaluate','document.title')).toBe('QML DOM experiment');
|
|
38
|
+
expect(await host.call('evaluate','document.getElementById("button").style.width')).toBe('320px');
|
|
39
|
+
expect(await host.call('evaluate','document.body.textContent.includes("Hello World! Clicks: 0")')).toBe(true);
|
|
40
|
+
await host.call('evaluate',`document.querySelector('[data-qml-node="3"]').click()`);
|
|
41
|
+
expect(await host.call('evaluate',`document.querySelector('[data-qml-node="2"]').textContent`)).toBe('Hello World! Clicks: 1');
|
|
42
|
+
await host.call('resize',800,400,1);
|
|
43
|
+
expect(await host.call('evaluate','document.getElementById("button").style.width')).toBe('400px');
|
|
44
|
+
await host.call('loadDocument',{url,contentType:'text/x-qml',source:`import QtQuick
|
|
45
|
+
Rectangle {
|
|
46
|
+
id: root
|
|
47
|
+
implicitWidth: 300
|
|
48
|
+
implicitHeight: 120
|
|
49
|
+
property int count: 1
|
|
50
|
+
Rectangle { id: child; anchors.fill: parent
|
|
51
|
+
Text { id: label; text: root.count + ":" + parent.width }
|
|
52
|
+
}
|
|
53
|
+
onClicked: { root.width = 222; root.implicitWidth = 500; root.count++; }
|
|
54
|
+
}`}); await host.call('whenLoaded');
|
|
55
|
+
expect(await host.call('evaluate','document.getElementById("label").textContent')).toBe('1:300');
|
|
56
|
+
await host.call('evaluate','document.getElementById("root").click()');
|
|
57
|
+
expect(await host.call('evaluate','document.getElementById("label").textContent')).toBe('2:222');
|
|
58
|
+
await host.call('loadDocument',{url:'https://qml.test/plain' ,source:'<p>ordinary HTML</p>',contentType:'text/html'});await host.call('whenLoaded');
|
|
59
|
+
expect(await host.call('evaluate','document.body.textContent')).toBe('ordinary HTML');
|
|
60
|
+
}finally{host.close();}
|
|
61
|
+
},30000);
|
|
62
|
+
|
|
63
|
+
test('Quickshell multiply fixture uses editable DOM controls and click handlers',async()=>{
|
|
64
|
+
const host=new RendererHost({timeout:20000});
|
|
65
|
+
const source=await Bun.file(new URL('./fixtures/qml/multiply.qml',import.meta.url)).text();
|
|
66
|
+
try{
|
|
67
|
+
await host.start();await host.call('resize',640,480,1);
|
|
68
|
+
await host.call('loadDocument',{url:'https://qml.test/multiply.qml',source,contentType:'text/x-qml'});await host.call('whenLoaded');
|
|
69
|
+
expect(await host.call('evaluate',"document.querySelectorAll('input').length")).toBe(3);
|
|
70
|
+
const point=async selector=>JSON.parse(await host.call('evaluate',`JSON.stringify((()=>{const r=document.querySelector(${JSON.stringify(selector)}).getBoundingClientRect();return [r.x+r.width/2,r.y+r.height/2]})())`));
|
|
71
|
+
const click=async selector=>host.call('click',...await point(selector));
|
|
72
|
+
const value=()=>host.call('evaluate',"document.getElementById('result').value");
|
|
73
|
+
await click('button');expect(await value()).toBe('42');
|
|
74
|
+
await click('#first');await host.call('press','a',{modifiers:2});await host.call('type','-2.5');
|
|
75
|
+
await click('#second');await host.call('press','a',{modifiers:2});await host.call('type','4');
|
|
76
|
+
await click('button');expect(await value()).toBe('-10');
|
|
77
|
+
await click('#result');await host.call('type','999');expect(await value()).toBe('-10');
|
|
78
|
+
await click('#first');await host.call('press','a',{modifiers:2});await host.call('press','Backspace');
|
|
79
|
+
await click('button');expect(await value()).toBe('Please enter two valid numbers');
|
|
80
|
+
const rects=JSON.parse(await host.call('evaluate',"JSON.stringify([...document.querySelectorAll('input,button')].map(e=>{const r=e.getBoundingClientRect();return [r.x,r.y,r.width,r.height]}))"));
|
|
81
|
+
for(let i=1;i<rects.length;i++)expect(rects[i][1]).toBeGreaterThanOrEqual(rects[i-1][1]+rects[i-1][3]);
|
|
82
|
+
}finally{host.close();}
|
|
83
|
+
},30000);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {test, expect} from "bun:test";
|
|
2
|
+
import {renderMinInterval, RENDER_INTERVAL_FLAG} from "../lib/renderer/render-interval.js";
|
|
3
|
+
import {RendererHost} from "../lib/renderer/host.js";
|
|
4
|
+
|
|
5
|
+
test("render interval validates boundaries without silently accepting malformed values", () => {
|
|
6
|
+
for (const [value, expected] of [[undefined,16],["0",0],["8",8],["16",16],["60000",60000]]) {
|
|
7
|
+
const warnings=[];expect(renderMinInterval(value,s=>warnings.push(s))).toBe(expected);expect(warnings).toEqual([]);
|
|
8
|
+
}
|
|
9
|
+
for (const value of ["","-1","60001","1.5","8ms","Infinity","NaN"," 8 "]) {
|
|
10
|
+
const warnings=[];expect(renderMinInterval(value,s=>warnings.push(s))).toBe(16);expect(warnings.length).toBe(1);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("renderer receives configured interval on launch without inheriting host environment", async () => {
|
|
15
|
+
const old=process.env.BUNINU_RENDER_MIN_INTERVAL, secret=process.env.BUNINU_TEST_PRIVATE_ENV;
|
|
16
|
+
let host;
|
|
17
|
+
try {
|
|
18
|
+
process.env.BUNINU_TEST_PRIVATE_ENV="must-not-leak";
|
|
19
|
+
for (const interval of ["8","0","60000"]) {
|
|
20
|
+
process.env.BUNINU_RENDER_MIN_INTERVAL=interval;
|
|
21
|
+
host=new RendererHost({entry:new URL("./fixtures/fake-renderer.js",import.meta.url).pathname});
|
|
22
|
+
await host.start();
|
|
23
|
+
const result=await host.call("configuration");
|
|
24
|
+
expect(result.args).toContain(RENDER_INTERVAL_FLAG+interval);
|
|
25
|
+
expect(result.secret).toBeUndefined();
|
|
26
|
+
host.close();
|
|
27
|
+
}
|
|
28
|
+
} finally {
|
|
29
|
+
host?.close();
|
|
30
|
+
if(old===undefined)delete process.env.BUNINU_RENDER_MIN_INTERVAL;else process.env.BUNINU_RENDER_MIN_INTERVAL=old;
|
|
31
|
+
if(secret===undefined)delete process.env.BUNINU_TEST_PRIVATE_ENV;else process.env.BUNINU_TEST_PRIVATE_ENV=secret;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
@@ -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,6 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
test('live title reads do not flush pending layout and paint, including empty titles',async()=>{
|
|
3
|
+
const child=Bun.spawn([process.execPath,new URL('./fixtures/title-no-paint.js',import.meta.url).pathname],{stdout:'pipe',stderr:'pipe'});
|
|
4
|
+
const [exit,stderr]=await Promise.all([child.exited,new Response(child.stderr).text()]);
|
|
5
|
+
expect(stderr).toBe('');expect(exit).toBe(0);
|
|
6
|
+
},15000);
|
|
@@ -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,40 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {mkdtempSync,rmSync} from 'node:fs';
|
|
3
|
+
|
|
4
|
+
// Opt-in sibling integration, with its own socket; never touches the user's Land.
|
|
5
|
+
test.skipIf(process.env.BUNINU_TEST_WAYLAND!=='1')('Casty SGR wheel -> Land -> browser scrolls both DOM axes',async()=>{
|
|
6
|
+
const {start}=await import('../../buninu-land/compositor.js');
|
|
7
|
+
const {mouseWheelEvent}=await import('../../casty/lib/input.js');
|
|
8
|
+
const {launch}=await import('../frontends/wayland/index.js');
|
|
9
|
+
const dir=mkdtempSync('/tmp/browser-wheel-'),old=process.env.XDG_RUNTIME_DIR;
|
|
10
|
+
process.env.XDG_RUNTIME_DIR=dir;let land,app;
|
|
11
|
+
const wait=async(fn)=>{for(let n=0;n<300;n++){if(await fn())return;await Bun.sleep(20)}throw Error('Scroll timed out')};
|
|
12
|
+
try{
|
|
13
|
+
land=await start(['--remote-debugging-port=0','--window-size=640,480']);
|
|
14
|
+
app=await launch({socket:dir+'/wayland-buninu',url:'data:text/html,'+encodeURIComponent('<style>body{margin:0}#box{width:400px;height:250px;overflow:auto}#content{width:1600px;height:1600px;background:linear-gradient(red,blue)}</style><div id="box"><div id="content"></div></div>')});
|
|
15
|
+
const state=async()=>JSON.parse(await app.browser.context.evaluate('JSON.stringify([document.getElementById("box").scrollLeft,document.getElementById("box").scrollTop])'));
|
|
16
|
+
await wait(()=>land.compositor.wm.renderSurfaces().some(s=>s.pixels));
|
|
17
|
+
const surface=land.compositor.wm.renderSurfaces().find(s=>s.pixels),origin=land.compositor.wm.origin(surface);
|
|
18
|
+
const wheel=code=>land.context.dispatchMouseEvent(mouseWheelEvent(code,origin.x+100,origin.y+130));
|
|
19
|
+
wheel(67);await wait(async()=>(await state())[0]>0);
|
|
20
|
+
const right=await state();expect(right).toEqual([100,0]);
|
|
21
|
+
wheel(65);await wait(async()=>(await state())[1]>0);
|
|
22
|
+
const down=await state();expect(down).toEqual([100,100]);
|
|
23
|
+
wheel(66);await wait(async()=>(await state())[0]<right[0]);
|
|
24
|
+
wheel(64);await wait(async()=>(await state())[1]<down[1]);
|
|
25
|
+
expect(await state()).toEqual([0,0]);
|
|
26
|
+
// Touchpad-like fractional deltas must not be rounded into wheel ticks.
|
|
27
|
+
land.context.dispatchMouseEvent({type:'mouseWheel',x:origin.x+100,y:origin.y+130,deltaX:12.5,deltaY:7.5});
|
|
28
|
+
await wait(async()=>(await state())[1]>0);
|
|
29
|
+
const fractional=await state();
|
|
30
|
+
expect(fractional[0]).toBeGreaterThan(0);expect(fractional[1]).toBeGreaterThan(0);
|
|
31
|
+
// Compare the same wheel input with the direct Browser/CDP scroll path.
|
|
32
|
+
await app.browser.context.scroll(-100,-100,{x:100,y:100});
|
|
33
|
+
expect(await state()).toEqual([0,0]);
|
|
34
|
+
await app.browser.context.scroll(12.5,7.5,{x:100,y:100});
|
|
35
|
+
expect(await state()).toEqual(fractional);
|
|
36
|
+
await app.browser.context.scroll(-100,-100,{x:100,y:100});
|
|
37
|
+
await app.browser.context.scroll(100,100,{x:100,y:100});
|
|
38
|
+
expect(await state()).toEqual(down);
|
|
39
|
+
}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})}
|
|
40
|
+
},30000);
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
});
|
|
39
|
+
|
|
40
|
+
test('Wayland chrome forwards page click counts through zoom and bar offset',async()=>{
|
|
41
|
+
const clicks=[];
|
|
42
|
+
const bar=new WaylandUrlBar({url:'about:blank',click:(...args)=>clicks.push(args)},{history,close(){},clipboard:{read:async()=>'',write:async()=>{}}});
|
|
43
|
+
try{
|
|
44
|
+
bar.zoom=2;
|
|
45
|
+
await bar.pointerButton(40,bar.height+60,272,1,{clickCount:2});
|
|
46
|
+
await bar.pointerButton(40,bar.height+60,272,0,{clickCount:2});
|
|
47
|
+
expect(clicks).toEqual([[20,30,{clickCount:2}]]);
|
|
48
|
+
}finally{bar.dispose()}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('Wayland toolbar forwards right/middle page clicks but ignores them on chrome',async()=>{
|
|
52
|
+
const clicks=[];const bar=new WaylandUrlBar({click:(...a)=>clicks.push(a)},{history});
|
|
53
|
+
try{
|
|
54
|
+
for(const code of [273,274]){
|
|
55
|
+
await bar.pointerButton(40,bar.height+60,code,1);
|
|
56
|
+
await bar.pointerButton(40,bar.height+60,code,0);
|
|
57
|
+
await bar.pointerButton(40,5,code,0);
|
|
58
|
+
}
|
|
59
|
+
expect(clicks.map(c=>c[2].button)).toEqual(['right','middle']);
|
|
60
|
+
}finally{bar.dispose()}
|
|
61
|
+
});
|