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,60 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {mkdtemp,rm} from 'node:fs/promises';
|
|
3
|
+
import {createServer} from 'node:net';
|
|
4
|
+
import {createBrowser} from '../lib/headless-shell.js';
|
|
5
|
+
|
|
6
|
+
test('local QML Socket connects, splits UTF-8 messages, writes and disconnects',async()=>{
|
|
7
|
+
const dir=await mkdtemp('/tmp/qml-socket-');
|
|
8
|
+
const peers=new Set(); let received='';
|
|
9
|
+
const server=createServer(socket=>{
|
|
10
|
+
peers.add(socket);socket.on('close',()=>peers.delete(socket));
|
|
11
|
+
socket.on('data',data=>{received+=data;});
|
|
12
|
+
const bytes=Buffer.from('你好\nsecond\n');
|
|
13
|
+
socket.write(bytes.subarray(0,2));setTimeout(()=>socket.write(bytes.subarray(2)),20);
|
|
14
|
+
});
|
|
15
|
+
await new Promise(r=>server.listen(dir+'/engine.sock',r));
|
|
16
|
+
const browser=await createBrowser({spareRenderer:false});
|
|
17
|
+
try{
|
|
18
|
+
await Bun.write(dir+'/main.qml',`import QtQuick\nimport QtQuick.Window\nimport Quickshell.Io
|
|
19
|
+
Item { Socket { id: socket; path: ${JSON.stringify(dir+'/engine.sock')}; connected: true
|
|
20
|
+
parser: SplitParser { onRead: data => { globalThis.messages.push(data); socket.write(data + "!"); socket.flush(); } }
|
|
21
|
+
onConnectedChanged: (globalThis.states ??= []).push(connected)
|
|
22
|
+
Component.onCompleted: { globalThis.messages=[]; globalThis.states=[]; globalThis.sock=socket; }
|
|
23
|
+
} }`);
|
|
24
|
+
await browser.context.navigate('file://'+dir+'/main.qml');
|
|
25
|
+
for(let i=0;i<100;i++){if(await browser.context.evaluate('globalThis.messages?.length===2'))break;await Bun.sleep(20);}
|
|
26
|
+
expect(await browser.context.evaluate('messages')).toEqual(['你好','second']);
|
|
27
|
+
expect(await browser.context.evaluate('sock.connected')).toBe(true);
|
|
28
|
+
for(let i=0;i<100&&!received.includes('second!');i++)await Bun.sleep(10);
|
|
29
|
+
expect(received).toBe('你好!second!');
|
|
30
|
+
await browser.context.evaluate('sock.connected=false');
|
|
31
|
+
for(let i=0;i<100&&peers.size;i++)await Bun.sleep(10);
|
|
32
|
+
expect(peers.size).toBe(0);
|
|
33
|
+
expect(await browser.context.evaluate('sock.connected')).toBe(false);
|
|
34
|
+
}finally{browser.close();for(const p of peers)p.destroy();await new Promise(r=>server.close(r));await rm(dir,{recursive:true,force:true});}
|
|
35
|
+
},20000);
|
|
36
|
+
|
|
37
|
+
test('Socket failure can reconnect and navigation closes the live connection',async()=>{
|
|
38
|
+
const dir=await mkdtemp('/tmp/qml-socket-retry-');
|
|
39
|
+
const peers=new Set();
|
|
40
|
+
const server=createServer(s=>{peers.add(s);s.on('close',()=>peers.delete(s));});
|
|
41
|
+
const browser=await createBrowser({spareRenderer:false});
|
|
42
|
+
try{
|
|
43
|
+
await Bun.write(dir+'/main.qml',`import QtQuick\nimport Quickshell.Io
|
|
44
|
+
Item { Socket {id:s; path:${JSON.stringify(dir+'/retry.sock')};connected:true
|
|
45
|
+
onError:error=>{globalThis.socketError=error;}
|
|
46
|
+
Component.onCompleted:globalThis.socketObject=s
|
|
47
|
+
} }`);
|
|
48
|
+
await browser.context.navigate('file://'+dir+'/main.qml');
|
|
49
|
+
for(let i=0;i<100;i++){if(await browser.context.evaluate('!!globalThis.socketError'))break;await Bun.sleep(10);}
|
|
50
|
+
expect(await browser.context.evaluate('socketObject.connected')).toBe(false);
|
|
51
|
+
expect(await browser.context.evaluate('socketError')).toBe('ENOENT');
|
|
52
|
+
await new Promise(r=>server.listen(dir+'/retry.sock',r));
|
|
53
|
+
await browser.context.evaluate('socketObject.connected=true');
|
|
54
|
+
for(let i=0;i<100;i++){if(await browser.context.evaluate('socketObject.connected'))break;await Bun.sleep(10);}
|
|
55
|
+
expect(await browser.context.evaluate('socketObject.connected')).toBe(true);
|
|
56
|
+
await browser.context.navigate('data:text/html,<p>ordinary page</p>');
|
|
57
|
+
for(let i=0;i<100&&peers.size;i++)await Bun.sleep(10);
|
|
58
|
+
expect(peers.size).toBe(0);
|
|
59
|
+
}finally{browser.close();for(const p of peers)p.destroy();await new Promise(r=>server.close(r));await rm(dir,{recursive:true,force:true});}
|
|
60
|
+
},20000);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {test,expect,beforeAll} from 'bun:test';
|
|
2
|
+
import init from '../usr/lib/canvaskit/canvaskit.js';
|
|
3
|
+
import {compileFragment,makeFragmentShader,translateFragment} from '../lib/qml/shader/fragment.js';
|
|
4
|
+
let kit;
|
|
5
|
+
beforeAll(async()=>{kit=await init({wasmBinary:await Bun.file(new URL('../usr/lib/canvaskit/canvaskit.wasm',import.meta.url)).arrayBuffer()});});
|
|
6
|
+
const header=`#version 440
|
|
7
|
+
layout(location=0) in vec2 qt_TexCoord0;
|
|
8
|
+
layout(location=0) out vec4 fragColor;
|
|
9
|
+
layout(std140,binding=0) uniform buf {mat4 qt_Matrix;float qt_Opacity;};`;
|
|
10
|
+
function pixel(compiled,options,x=0,y=0){
|
|
11
|
+
const surface=kit.MakeSurface(options.width,options.height),paint=new kit.Paint();
|
|
12
|
+
const shader=makeFragmentShader(compiled,options);
|
|
13
|
+
try{
|
|
14
|
+
paint.setShader(shader);surface.getCanvas().drawRect(kit.XYWHRect(0,0,options.width,options.height),paint);surface.flush();
|
|
15
|
+
const image=surface.makeImageSnapshot();
|
|
16
|
+
try{return Array.from(image.readPixels(x,y,{width:1,height:1,colorType:kit.ColorType.RGBA_8888,alphaType:kit.AlphaType.Unpremul,colorSpace:kit.ColorSpace.SRGB}));}finally{image.delete();}
|
|
17
|
+
}finally{paint.delete();shader.delete();surface.delete();}
|
|
18
|
+
}
|
|
19
|
+
test('Qt fragment uniform and coordinate mapping executes on CPU raster surface',()=>{
|
|
20
|
+
const c=compileFragment(kit,header+'uniform int mode; void main(){fragColor=mode==2?vec4(qt_TexCoord0,0,1):vec4(0);fragColor*=qt_Opacity;}');
|
|
21
|
+
try{const p=pixel(c,{width:16,height:16,uniforms:{mode:2}},8,8);expect(p).toEqual([135,135,0,255]);}finally{c.effect.delete();}
|
|
22
|
+
});
|
|
23
|
+
test('CPU child texture sampling and textureSize preserve source pixel dimensions',()=>{
|
|
24
|
+
const image=kit.MakeImage({width:2,height:1,colorType:kit.ColorType.RGBA_8888,alphaType:kit.AlphaType.Premul,colorSpace:kit.ColorSpace.SRGB},new Uint8Array([255,0,0,255,0,255,0,255]),8);
|
|
25
|
+
const child=image.makeShaderOptions(kit.TileMode.Clamp,kit.TileMode.Clamp,kit.FilterMode.Nearest,kit.MipmapMode.None);
|
|
26
|
+
const c=compileFragment(kit,header+'layout(binding=1) uniform sampler2D source;void main(){if(textureSize(source,0)!=ivec2(2,1)){fragColor=vec4(0,0,1,1);return;}fragColor=texture(source,qt_TexCoord0);}');
|
|
27
|
+
try{
|
|
28
|
+
const options={width:4,height:2,textures:{source:{shader:child,width:2,height:1}}};
|
|
29
|
+
expect(pixel(c,options,0,0)).toEqual([255,0,0,255]);
|
|
30
|
+
expect(pixel(c,options,3,0)).toEqual([0,255,0,255]);
|
|
31
|
+
expect(()=>makeFragmentShader(c,{width:1,height:1})).toThrow('Missing shader texture');
|
|
32
|
+
}finally{c.effect.delete();child.delete();image.delete();}
|
|
33
|
+
});
|
|
34
|
+
test('unsupported shader interfaces fail rather than rendering a success placeholder',()=>{
|
|
35
|
+
expect(()=>translateFragment('#include "missing"')).toThrow('preprocessor');
|
|
36
|
+
expect(()=>translateFragment(header+'void main(){discard;}')).toThrow('Unsupported');
|
|
37
|
+
expect(()=>compileFragment(kit,header+'void main(){fragColor=unknown();}')).toThrow('compilation failed');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
import {deflateSync} from 'node:zlib';
|
|
41
|
+
import {fragmentSource} from '../lib/qml/shader/qsb.js';
|
|
42
|
+
function qsb(source){
|
|
43
|
+
const text=Buffer.from(source),head=Buffer.alloc(28),tail=Buffer.from([0,0,0,4,109,97,105,110]);
|
|
44
|
+
[9,4,1,150,0,0,text.length].forEach((n,i)=>head.writeUInt32BE(n,i*4));
|
|
45
|
+
const raw=Buffer.concat([head,text,tail]),size=Buffer.alloc(4);size.writeUInt32BE(raw.length);
|
|
46
|
+
return Buffer.concat([size,deflateSync(raw)]);
|
|
47
|
+
}
|
|
48
|
+
test('QSB source records are validated and generated struct uniforms compile',()=>{
|
|
49
|
+
const source='#version 150\nstruct buf {float gain;};uniform buf _12;out vec4 fragColor;void main(){fragColor=vec4(_12.gain,0,0,1);}';
|
|
50
|
+
expect(fragmentSource(qsb(source))).toBe(source);
|
|
51
|
+
const c=compileFragment(kit,fragmentSource(qsb(source)));
|
|
52
|
+
try{expect(pixel(c,{width:1,height:1,uniforms:{gain:1}})).toEqual([255,0,0,255]);}finally{c.effect.delete();}
|
|
53
|
+
expect(()=>fragmentSource(qsb(source).subarray(0,10))).toThrow();
|
|
54
|
+
expect(()=>fragmentSource(qsb(source.replace('#version 150','#version 100')))).toThrow('no supported');
|
|
55
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { RendererHost } from '../lib/renderer/host.js';
|
|
3
|
+
|
|
4
|
+
test('QML style updates preserve final overrides and clear conditional styles', async () => {
|
|
5
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
6
|
+
try {
|
|
7
|
+
await host.start();
|
|
8
|
+
await host.call('loadDocument', {
|
|
9
|
+
url: 'data:text/x-qml,styles',
|
|
10
|
+
contentType: 'text/x-qml',
|
|
11
|
+
source: `import QtQuick
|
|
12
|
+
Rectangle {
|
|
13
|
+
id: root; width: 320; height: 160; property bool changed: false
|
|
14
|
+
onClicked: { changed = !changed; }
|
|
15
|
+
Rectangle { id: box; width: root.changed ? 120 : 80; height: 30; x: 10
|
|
16
|
+
opacity: root.changed ? 0.5 : 1; color: root.changed ? "blue" : "red"
|
|
17
|
+
gradient: root.changed ? null : ramp
|
|
18
|
+
Gradient { id: ramp; GradientStop { position: 0; color: "red" } GradientStop { position: 1; color: "blue" } }
|
|
19
|
+
}
|
|
20
|
+
Text { id: label; y:40; width:80; height:30; text:"long long text"
|
|
21
|
+
elide: root.changed ? Text.ElideNone : Text.ElideRight
|
|
22
|
+
verticalAlignment: Text.AlignVCenter
|
|
23
|
+
}
|
|
24
|
+
}`,
|
|
25
|
+
});
|
|
26
|
+
await host.call('whenLoaded');
|
|
27
|
+
const styles = () =>
|
|
28
|
+
host.call(
|
|
29
|
+
'evaluate',
|
|
30
|
+
`JSON.stringify({
|
|
31
|
+
width:box.style.width,opacity:box.style.opacity,
|
|
32
|
+
gradient:box.style.backgroundImage,overflow:label.style.overflow,
|
|
33
|
+
whiteSpace:label.style.whiteSpace,lineHeight:label.style.lineHeight,
|
|
34
|
+
elide:label.style.textOverflow
|
|
35
|
+
})`
|
|
36
|
+
.replaceAll('box.style', 'document.getElementById("box").style')
|
|
37
|
+
.replaceAll('label.style', 'document.getElementById("label").style'),
|
|
38
|
+
);
|
|
39
|
+
const before = JSON.parse(await styles());
|
|
40
|
+
expect(before.width).toBe('80px');
|
|
41
|
+
expect(before.gradient).toContain('linear-gradient');
|
|
42
|
+
expect(before.elide).toBe('ellipsis');
|
|
43
|
+
expect(before.whiteSpace).toBe('nowrap');
|
|
44
|
+
expect(before.lineHeight).toBe('30px');
|
|
45
|
+
await host.call(
|
|
46
|
+
'evaluate',
|
|
47
|
+
'document.getElementById("box").style.zIndex="99";document.getElementById("root").click()',
|
|
48
|
+
);
|
|
49
|
+
expect(
|
|
50
|
+
await host.call(
|
|
51
|
+
'evaluate',
|
|
52
|
+
'document.getElementById("box").style.zIndex',
|
|
53
|
+
),
|
|
54
|
+
).toBe('0');
|
|
55
|
+
const after = JSON.parse(await styles());
|
|
56
|
+
expect(after.width).toBe('120px');
|
|
57
|
+
expect(after.opacity).toBe('0.5');
|
|
58
|
+
expect(after.gradient).toBe('none');
|
|
59
|
+
expect(after.elide).toBe('clip');
|
|
60
|
+
expect(after.whiteSpace).toBe('pre');
|
|
61
|
+
expect(after.overflow).toBe('visible');
|
|
62
|
+
expect(after.lineHeight).toBe('30px');
|
|
63
|
+
await host.call('evaluate', 'document.getElementById("root").click()');
|
|
64
|
+
expect(JSON.parse(await styles())).toEqual(before);
|
|
65
|
+
} finally {
|
|
66
|
+
host.close();
|
|
67
|
+
}
|
|
68
|
+
}, 30000);
|
|
69
|
+
|
|
70
|
+
// Qt retains the long Wrap enum spelling used by styled code viewers.
|
|
71
|
+
test('StyledText can toggle WrapAtWordBoundaryOrAnywhere and grow its line count', async () => {
|
|
72
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
73
|
+
try {
|
|
74
|
+
await host.start();
|
|
75
|
+
await host.call('loadDocument', {
|
|
76
|
+
url: 'data:text/x-qml,wrap-alias', contentType: 'text/x-qml',
|
|
77
|
+
source: `import QtQuick
|
|
78
|
+
Rectangle {
|
|
79
|
+
id: root; width: 320; height: 200; property bool wrapped: false
|
|
80
|
+
onClicked: wrapped = !wrapped
|
|
81
|
+
Text { id: code; width: 90; textFormat: Text.StyledText
|
|
82
|
+
text: "<font color='red'>const</font> averylongidentifierwithoutspaces = 123456789;"
|
|
83
|
+
wrapMode: root.wrapped ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap
|
|
84
|
+
}
|
|
85
|
+
Text { id: count; y: 180; text: code.lineCount }
|
|
86
|
+
}`,
|
|
87
|
+
});
|
|
88
|
+
await host.call('whenLoaded');
|
|
89
|
+
const read = () => host.call('evaluate', `({
|
|
90
|
+
lines: Number(document.getElementById('count').textContent),
|
|
91
|
+
whiteSpace: document.getElementById('code').style.whiteSpace,
|
|
92
|
+
overflowWrap: document.getElementById('code').style.overflowWrap
|
|
93
|
+
})`);
|
|
94
|
+
expect((await read()).lines).toBe(1);
|
|
95
|
+
await host.call('evaluate', "document.getElementById('root').click()");
|
|
96
|
+
const wrapped = await read();
|
|
97
|
+
expect(wrapped.lines).toBeGreaterThan(1);
|
|
98
|
+
expect(wrapped.whiteSpace).toBe('pre-wrap');
|
|
99
|
+
expect(wrapped.overflowWrap).toBe('anywhere');
|
|
100
|
+
await host.call('evaluate', "document.getElementById('root').click()");
|
|
101
|
+
expect((await read()).lines).toBe(1);
|
|
102
|
+
} finally { host.close(); }
|
|
103
|
+
}, 30000);
|
|
104
|
+
|
|
105
|
+
test('Qt.binding attaches to a default property and a plain write detaches it', async () => {
|
|
106
|
+
const host = new RendererHost({timeout:20000});
|
|
107
|
+
try {
|
|
108
|
+
await host.start();
|
|
109
|
+
await host.call('loadDocument',{url:'data:text/x-qml,default-binding',contentType:'text/x-qml',source:`import QtQuick
|
|
110
|
+
Item {id:root;width:100;height:100;property bool shown:true
|
|
111
|
+
Rectangle {id:box;width:40;height:40}
|
|
112
|
+
Component.onCompleted:box.visible=Qt.binding(function(){return root.shown})
|
|
113
|
+
Button {id:toggle;onClicked:root.shown=!root.shown}
|
|
114
|
+
Button {id:detach;onClicked:box.visible=true}
|
|
115
|
+
}`});
|
|
116
|
+
await host.call('whenLoaded');
|
|
117
|
+
const display=()=>host.call('evaluate','document.getElementById("box").style.display');
|
|
118
|
+
const click=id=>host.call('evaluate',`document.getElementById('${id}').click()`);
|
|
119
|
+
expect(await display()).toBe('block');
|
|
120
|
+
await click('toggle');expect(await display()).toBe('none');
|
|
121
|
+
await click('toggle');expect(await display()).toBe('block');
|
|
122
|
+
await click('detach');await click('toggle');expect(await display()).toBe('block');
|
|
123
|
+
}finally{host.close();}
|
|
124
|
+
},30000);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { createBrowser } from '../lib/headless-shell.js';
|
|
4
|
+
import { loadGraphics } from '../lib/renderer/page-renderer.js';
|
|
5
|
+
|
|
6
|
+
test('ShaderEffectSource freezes, updates, hides sources and releases Loader references', async () => {
|
|
7
|
+
const dir = mkdtempSync('/tmp/qml-textures-');
|
|
8
|
+
const browser = await createBrowser({ spareRenderer: false });
|
|
9
|
+
const { CanvasKit: kit } = await loadGraphics();
|
|
10
|
+
async function pixel(x,y) {
|
|
11
|
+
const image=kit.MakeImageFromEncoded(Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
|
|
12
|
+
try { return [...image.readPixels(x,y,{width:1,height:1,colorType:kit.ColorType.RGBA_8888,alphaType:kit.AlphaType.Unpremul,colorSpace:kit.ColorSpace.SRGB})]; }
|
|
13
|
+
finally { image.delete(); }
|
|
14
|
+
}
|
|
15
|
+
const run = code => browser.context.evaluate(code);
|
|
16
|
+
async function until(fn) { for(let i=0;i<50;i++){if(await fn())return;await Bun.sleep(20);}throw Error(await run('document.body.innerText+JSON.stringify(errors)')); }
|
|
17
|
+
try {
|
|
18
|
+
await Bun.write(dir+'/main.qml', `import QtQuick
|
|
19
|
+
Rectangle {width:240;height:160;color:"white"
|
|
20
|
+
property int frames:0
|
|
21
|
+
Rectangle {id:source;width:60;height:40;color:"red"}
|
|
22
|
+
Loader {id:layer;x:80;width:60;height:40;active:true
|
|
23
|
+
sourceComponent:ShaderEffectSource {sourceItem:source;hideSource:true;live:false;onScheduledUpdateCompleted:frames+=1}}
|
|
24
|
+
Loader {id:mirror;x:160;width:60;height:40;active:false;sourceComponent:ShaderEffectSource {sourceItem:source;hideSource:true;live:false}}
|
|
25
|
+
Text {id:status;y:120;text:frames+":"+layer.active}
|
|
26
|
+
Button {id:toggle;x:210;y:80;text:"hide";onClicked:layer.item.hideSource=!layer.item.hideSource}
|
|
27
|
+
Button {id:blue;y:80;text:"blue";onClicked:source.color=source.color==="blue"?"red":"blue"}
|
|
28
|
+
Button {id:update;x:70;y:80;text:"update";onClicked:layer.item.scheduleUpdate()}
|
|
29
|
+
Button {id:live;x:140;y:80;text:"live";onClicked:layer.item.live=true}
|
|
30
|
+
Button {id:mirrorOn;y:140;text:"mirror";onClicked:mirror.active=true}
|
|
31
|
+
Button {id:clear;x:70;y:140;text:"clear";onClicked:mirror.item.sourceItem=null}
|
|
32
|
+
Button {id:release;x:140;y:120;text:"release";onClicked:layer.active=false}
|
|
33
|
+
}`);
|
|
34
|
+
await browser.context.addInitScript('globalThis.errors=[];addEventListener("error",e=>errors.push(String(e.error?.stack||e.message)))');
|
|
35
|
+
await browser.context.navigate(Bun.pathToFileURL(dir+'/main.qml').href);
|
|
36
|
+
await until(async()=> (await run('document.getElementById("status")?.textContent'))?.startsWith('1:'));
|
|
37
|
+
expect(await pixel(5,5)).toEqual([255,255,255,255]);
|
|
38
|
+
expect(await pixel(85,5)).toEqual([255,0,0,255]);
|
|
39
|
+
await run('document.getElementById("toggle").click()');await Bun.sleep(30);
|
|
40
|
+
expect(await pixel(5,5)).toEqual([255,0,0,255]);
|
|
41
|
+
await run('document.getElementById("toggle").click()');await Bun.sleep(30);
|
|
42
|
+
expect(await pixel(5,5)).toEqual([255,255,255,255]);
|
|
43
|
+
await run('document.getElementById("blue").click()');await Bun.sleep(50);
|
|
44
|
+
expect(await pixel(85,5)).toEqual([255,0,0,255]);
|
|
45
|
+
await run('document.getElementById("update").click()');
|
|
46
|
+
await until(async()=> (await run('document.getElementById("status").textContent')).startsWith('2:'));
|
|
47
|
+
expect(await pixel(85,5)).toEqual([0,0,255,255]);
|
|
48
|
+
await run('document.getElementById("live").click()');
|
|
49
|
+
await until(async()=> (await run('document.getElementById("status").textContent')).startsWith('3:'));
|
|
50
|
+
await run('document.getElementById("blue").click()');
|
|
51
|
+
await until(async()=> (await run('document.getElementById("status").textContent')).startsWith('4:'));
|
|
52
|
+
expect(await pixel(85,5)).toEqual([255,0,0,255]);
|
|
53
|
+
await Bun.sleep(120);
|
|
54
|
+
expect(await run('document.getElementById("status").textContent')).toBe('4:true');
|
|
55
|
+
await run('document.getElementById("mirrorOn").click()');
|
|
56
|
+
await until(async()=> (await pixel(165,5))[1]===0);
|
|
57
|
+
await run('document.getElementById("release").click()');await Bun.sleep(30);
|
|
58
|
+
expect(await pixel(5,5)).toEqual([255,255,255,255]);
|
|
59
|
+
expect(await pixel(165,5)).toEqual([255,0,0,255]);
|
|
60
|
+
await run('document.getElementById("clear").click()');await Bun.sleep(30);
|
|
61
|
+
expect(await pixel(165,5)).toEqual([255,255,255,255]);
|
|
62
|
+
expect(await pixel(5,5)).toEqual([255,0,0,255]);
|
|
63
|
+
expect(await pixel(85,5)).toEqual([255,255,255,255]);
|
|
64
|
+
expect(await run('errors')).toEqual([]);
|
|
65
|
+
} finally { browser.close();rmSync(dir,{recursive:true,force:true}); }
|
|
66
|
+
},30000);
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { RendererHost } from '../lib/renderer/host.js';
|
|
3
|
+
const wait = async (fn, predicate) => {
|
|
4
|
+
let value;
|
|
5
|
+
for (let i = 0; i < 80; i++) {
|
|
6
|
+
value = await fn();
|
|
7
|
+
if (predicate(value)) return value;
|
|
8
|
+
await Bun.sleep(15);
|
|
9
|
+
}
|
|
10
|
+
throw Error('View did not settle: ' + JSON.stringify(value));
|
|
11
|
+
};
|
|
12
|
+
test('ListView virtualizes a large DelegateModel, scrolls, updates count and reuses delegates', async () => {
|
|
13
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
14
|
+
try {
|
|
15
|
+
await host.start();
|
|
16
|
+
await host.call('loadDocument', {
|
|
17
|
+
url: 'data:text/x-qml,view',
|
|
18
|
+
contentType: 'text/x-qml',
|
|
19
|
+
source: `
|
|
20
|
+
import QtQuick
|
|
21
|
+
import QtQml.Models
|
|
22
|
+
Item {
|
|
23
|
+
id:root;width:300;height:200
|
|
24
|
+
ListView {id:view;width:200;height:100;reuseItems:true
|
|
25
|
+
model:DelegateModel {id:rows;model:100000000;filterOnGroup:"shown";groups:DelegateModelGroup {name:"shown";includeByDefault:false}
|
|
26
|
+
Component.onCompleted:items.addGroups(0,2000,"shown")
|
|
27
|
+
}
|
|
28
|
+
delegate:Text {required property int index;text:"row "+index;height:20;width:200}
|
|
29
|
+
}
|
|
30
|
+
Text {id:count;y:120;text:view.count}
|
|
31
|
+
Button {id:scroll;y:150;onClicked:view.positionViewAtIndex(1000,ListView.Beginning)}
|
|
32
|
+
Button {id:shrink;x:100;y:150;onClicked:{rows.groups.removeGroups(5,1995,"shown");view.positionViewAtBeginning()}}
|
|
33
|
+
}`,
|
|
34
|
+
});
|
|
35
|
+
await host.call('whenLoaded');
|
|
36
|
+
const read = async () =>
|
|
37
|
+
JSON.parse(
|
|
38
|
+
await host.call(
|
|
39
|
+
'evaluate',
|
|
40
|
+
'JSON.stringify({count:document.getElementById("count").textContent,text:document.body.innerText,n:document.querySelectorAll("[data-qml-type=Text]").length})',
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
await wait(read, (v) => v.count === '2000');
|
|
44
|
+
let result = await read();
|
|
45
|
+
expect(result.n).toBeLessThan(20);
|
|
46
|
+
expect(result.text).toContain('row 0');
|
|
47
|
+
await host.call('evaluate', 'document.getElementById("scroll").click()');
|
|
48
|
+
result = await wait(read, (v) => v.text.includes('row 1000'));
|
|
49
|
+
expect(result.n).toBeLessThan(25);
|
|
50
|
+
await host.call('evaluate', 'document.getElementById("shrink").click()');
|
|
51
|
+
result = await wait(
|
|
52
|
+
read,
|
|
53
|
+
(v) => v.count === '5' && v.text.includes('row 0'),
|
|
54
|
+
);
|
|
55
|
+
expect(result.n).toBeLessThan(25);
|
|
56
|
+
} finally {
|
|
57
|
+
host.close();
|
|
58
|
+
}
|
|
59
|
+
}, 30000);
|
|
60
|
+
|
|
61
|
+
test('Flickable routes default children to contentItem and responds to wheel without moving explicit overlays', async () => {
|
|
62
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
63
|
+
try {
|
|
64
|
+
await host.start();
|
|
65
|
+
await host.call('loadDocument', {
|
|
66
|
+
url: 'data:text/x-qml,flick',
|
|
67
|
+
contentType: 'text/x-qml',
|
|
68
|
+
source: `
|
|
69
|
+
import QtQuick
|
|
70
|
+
Flickable {id:root;width:200;height:100;contentWidth:200;contentHeight:500;clip:true
|
|
71
|
+
Rectangle {id:body;width:parent.width;height:500;color:"red"}
|
|
72
|
+
Text {id:overlay;parent:root;text:"fixed"}
|
|
73
|
+
Text {id:status;parent:root;y:20;text:root.contentY+":"+body.parent.height}
|
|
74
|
+
}`,
|
|
75
|
+
});
|
|
76
|
+
await host.call('whenLoaded');
|
|
77
|
+
expect(
|
|
78
|
+
await host.call(
|
|
79
|
+
'evaluate',
|
|
80
|
+
'document.getElementById("body").parentElement.hasAttribute("data-qml-content")',
|
|
81
|
+
),
|
|
82
|
+
).toBe(true);
|
|
83
|
+
expect(
|
|
84
|
+
await host.call(
|
|
85
|
+
'evaluate',
|
|
86
|
+
'document.getElementById("overlay").parentElement.id',
|
|
87
|
+
),
|
|
88
|
+
).toBe('root');
|
|
89
|
+
await host.call(
|
|
90
|
+
'evaluate',
|
|
91
|
+
'document.getElementById("root").dispatchEvent(new WheelEvent("wheel",{deltaY:50,bubbles:true,cancelable:true}))',
|
|
92
|
+
);
|
|
93
|
+
await wait(
|
|
94
|
+
() =>
|
|
95
|
+
host.call('evaluate', 'document.getElementById("status").textContent'),
|
|
96
|
+
(v) => v === '50:500',
|
|
97
|
+
);
|
|
98
|
+
expect(
|
|
99
|
+
await host.call(
|
|
100
|
+
'evaluate',
|
|
101
|
+
'document.getElementById("body").parentElement.style.top',
|
|
102
|
+
),
|
|
103
|
+
).toBe('-50px');
|
|
104
|
+
} finally {
|
|
105
|
+
host.close();
|
|
106
|
+
}
|
|
107
|
+
}, 30000);
|
|
108
|
+
|
|
109
|
+
test('empty Loaders, decoration Items and DropAreas do not swallow pointer activation', async () => {
|
|
110
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
111
|
+
try {
|
|
112
|
+
await host.start();
|
|
113
|
+
await host.call('loadDocument', {
|
|
114
|
+
url: 'data:text/x-qml,pointer',
|
|
115
|
+
contentType: 'text/x-qml',
|
|
116
|
+
source: `
|
|
117
|
+
import QtQuick
|
|
118
|
+
Item {width:200;height:100;property int clicks:0
|
|
119
|
+
Rectangle {width:80;height:40;color:"red";TapHandler {onTapped:clicks++}}
|
|
120
|
+
DropArea {anchors.fill:parent}
|
|
121
|
+
Loader {anchors.fill:parent;active:false;z:3}
|
|
122
|
+
Item {anchors.fill:parent;z:4}
|
|
123
|
+
Text {id:status;y:50;text:clicks}
|
|
124
|
+
}`,
|
|
125
|
+
});
|
|
126
|
+
await host.call('whenLoaded');
|
|
127
|
+
await host.call('click', 20, 20);
|
|
128
|
+
await wait(
|
|
129
|
+
() =>
|
|
130
|
+
host.call('evaluate', 'document.getElementById("status").textContent'),
|
|
131
|
+
(v) => v === '1',
|
|
132
|
+
);
|
|
133
|
+
} finally {
|
|
134
|
+
host.close();
|
|
135
|
+
}
|
|
136
|
+
}, 30000);
|
|
137
|
+
|
|
138
|
+
test('XmlListModel reads XBEL attributes through Qt role indices and reports unsupported queries', async () => {
|
|
139
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
140
|
+
try {
|
|
141
|
+
await host.start();
|
|
142
|
+
await host.call('loadDocument', {
|
|
143
|
+
url: 'data:text/x-qml,xml',
|
|
144
|
+
contentType: 'text/x-qml',
|
|
145
|
+
source: `
|
|
146
|
+
import QtQuick
|
|
147
|
+
import QtQml.XmlListModel
|
|
148
|
+
Item {
|
|
149
|
+
XmlListModel {id:model;query:"/xbel/bookmark";xml:"<xbel><bookmark href='file:///tmp/a' visited='2026-10-22'/><bookmark href='file:///tmp/b'/></xbel>"
|
|
150
|
+
XmlListModelRole {name:"href";attributeName:"href"}
|
|
151
|
+
XmlListModelRole {name:"visited";attributeName:"visited"}
|
|
152
|
+
}
|
|
153
|
+
Text {id:status;text:model.status+":"+model.count+":"+model.data(model.index(1,0),Qt.UserRole)}
|
|
154
|
+
Button {id:bad;onClicked:model.query="//bookmark[@bad]"}
|
|
155
|
+
}`,
|
|
156
|
+
});
|
|
157
|
+
await host.call('whenLoaded');
|
|
158
|
+
await wait(
|
|
159
|
+
() =>
|
|
160
|
+
host.call('evaluate', 'document.getElementById("status").textContent'),
|
|
161
|
+
(v) => v === '1:2:file:///tmp/b',
|
|
162
|
+
);
|
|
163
|
+
await host.call('evaluate', 'document.getElementById("bad").click()');
|
|
164
|
+
await wait(
|
|
165
|
+
() =>
|
|
166
|
+
host.call('evaluate', 'document.getElementById("status").textContent'),
|
|
167
|
+
(v) => v.startsWith('3:0:'),
|
|
168
|
+
);
|
|
169
|
+
} finally {
|
|
170
|
+
host.close();
|
|
171
|
+
}
|
|
172
|
+
}, 30000);
|
|
173
|
+
|
|
174
|
+
test('unchanged Flea StableModel resizes a virtualized view synchronously and warms reusable rows', async () => {
|
|
175
|
+
const { mkdtempSync, rmSync } = await import('node:fs');
|
|
176
|
+
const dir = mkdtempSync('/tmp/qml-stable-model-');
|
|
177
|
+
const ui = Bun.fileURLToPath(
|
|
178
|
+
new URL('./fixtures/qml/flea-theme/ui/', import.meta.url),
|
|
179
|
+
);
|
|
180
|
+
await Bun.write(
|
|
181
|
+
dir + '/main.qml',
|
|
182
|
+
`import QtQuick
|
|
183
|
+
import ${JSON.stringify(ui)} as Flea
|
|
184
|
+
Item {width:300;height:200;property int total:100
|
|
185
|
+
ListView {id:view;width:200;height:100;reuseItems:true
|
|
186
|
+
model:Flea.StableModel {id:stable;view:view;total:parent.parent.total;warm:true;warmRows:20}
|
|
187
|
+
delegate:Text {required property int index;width:200;height:20;text:"row "+index}
|
|
188
|
+
}
|
|
189
|
+
Text {id:status;y:120;text:view.count+":"+view.currentIndex}
|
|
190
|
+
Button {id:grow;y:150;onClicked:{total=105;stable.resize()}}
|
|
191
|
+
Button {id:scroll;x:80;y:150;onClicked:view.positionViewAtIndex(50,ListView.Beginning)}
|
|
192
|
+
}`,
|
|
193
|
+
);
|
|
194
|
+
const { createBrowser } = await import('../lib/headless-shell.js');
|
|
195
|
+
const browser = await createBrowser({ spareRenderer: false });
|
|
196
|
+
try {
|
|
197
|
+
await browser.context.navigate(Bun.pathToFileURL(dir + '/main.qml').href);
|
|
198
|
+
await wait(
|
|
199
|
+
() =>
|
|
200
|
+
browser.context.evaluate(
|
|
201
|
+
'document.getElementById("status").textContent',
|
|
202
|
+
),
|
|
203
|
+
(v) => v === '100:0',
|
|
204
|
+
);
|
|
205
|
+
await browser.context.evaluate('document.getElementById("scroll").click()');
|
|
206
|
+
await wait(
|
|
207
|
+
() => browser.context.evaluate('document.body.innerText'),
|
|
208
|
+
(v) => v.includes('row 50'),
|
|
209
|
+
);
|
|
210
|
+
await browser.context.evaluate('document.getElementById("grow").click()');
|
|
211
|
+
await wait(
|
|
212
|
+
() =>
|
|
213
|
+
browser.context.evaluate(
|
|
214
|
+
'document.getElementById("status").textContent',
|
|
215
|
+
),
|
|
216
|
+
(v) => v === '105:0',
|
|
217
|
+
);
|
|
218
|
+
expect(
|
|
219
|
+
await browser.context.evaluate(
|
|
220
|
+
'document.querySelectorAll("[data-qml-type=Text]").length',
|
|
221
|
+
),
|
|
222
|
+
).toBeLessThan(30);
|
|
223
|
+
} finally {
|
|
224
|
+
browser.close();
|
|
225
|
+
rmSync(dir, { recursive: true, force: true });
|
|
226
|
+
}
|
|
227
|
+
}, 30000);
|
|
228
|
+
|
|
229
|
+
test('passive HoverHandler observes a decoration without intercepting the button beneath', async () => {
|
|
230
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
231
|
+
try {
|
|
232
|
+
await host.start();
|
|
233
|
+
await host.call('loadDocument', {
|
|
234
|
+
url: 'data:text/x-qml,hover',
|
|
235
|
+
contentType: 'text/x-qml',
|
|
236
|
+
source: `
|
|
237
|
+
import QtQuick
|
|
238
|
+
Item {width:200;height:120;property int clicks:0
|
|
239
|
+
Rectangle {width:80;height:40;color:"red";TapHandler {onTapped:clicks++}}
|
|
240
|
+
Item {width:80;height:40;z:5;HoverHandler {id:hover}}
|
|
241
|
+
Text {id:status;y:60;text:hover.hovered+":"+clicks}
|
|
242
|
+
}`,
|
|
243
|
+
});
|
|
244
|
+
await host.call('whenLoaded');
|
|
245
|
+
await host.call('move', 20, 20);
|
|
246
|
+
await host.call('click', 20, 20);
|
|
247
|
+
await wait(
|
|
248
|
+
() =>
|
|
249
|
+
host.call('evaluate', 'document.getElementById("status").textContent'),
|
|
250
|
+
(v) => v === 'true:1',
|
|
251
|
+
);
|
|
252
|
+
await host.call('move', 110, 80);
|
|
253
|
+
await wait(
|
|
254
|
+
() =>
|
|
255
|
+
host.call('evaluate', 'document.getElementById("status").textContent'),
|
|
256
|
+
(v) => v === 'false:1',
|
|
257
|
+
);
|
|
258
|
+
} finally {
|
|
259
|
+
host.close();
|
|
260
|
+
}
|
|
261
|
+
}, 30000);
|
|
262
|
+
|
|
263
|
+
test('turning reuseItems off and back on in one handler immediately drains parked delegates', async () => {
|
|
264
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
265
|
+
try {
|
|
266
|
+
await host.start();
|
|
267
|
+
await host.call('loadDocument', {
|
|
268
|
+
url: 'data:text/x-qml,pool',
|
|
269
|
+
contentType: 'text/x-qml',
|
|
270
|
+
source: `import QtQuick
|
|
271
|
+
Item {width:300;height:200
|
|
272
|
+
ListView {id:view;width:200;height:100;reuseItems:true;cacheBuffer:0;model:100
|
|
273
|
+
delegate:Text {required property int index;width:200;height:20;text:"row "+index}
|
|
274
|
+
}
|
|
275
|
+
Button {id:park;y:120;onClicked:{view.displayMarginEnd=300;view.forceLayout();view.displayMarginEnd=0;view.forceLayout()}}
|
|
276
|
+
Button {id:drain;x:80;y:120;onClicked:{view.reuseItems=false;view.reuseItems=true}}
|
|
277
|
+
}`,
|
|
278
|
+
});
|
|
279
|
+
await host.call('whenLoaded');
|
|
280
|
+
const count = () =>
|
|
281
|
+
host.call(
|
|
282
|
+
'evaluate',
|
|
283
|
+
'document.querySelectorAll("[data-qml-type=Text]").length',
|
|
284
|
+
);
|
|
285
|
+
await host.call('evaluate', 'document.getElementById("park").click()');
|
|
286
|
+
const parked = await count();
|
|
287
|
+
expect(parked).toBeGreaterThan(10);
|
|
288
|
+
await host.call('evaluate', 'document.getElementById("drain").click()');
|
|
289
|
+
expect(await count()).toBeLessThan(10);
|
|
290
|
+
} finally {
|
|
291
|
+
host.close();
|
|
292
|
+
}
|
|
293
|
+
}, 30000);
|
|
294
|
+
|
|
295
|
+
test('replacing a numeric ListView model resets its scroll before creating new delegates',async()=>{
|
|
296
|
+
const host=new RendererHost({timeout:20000});
|
|
297
|
+
try{
|
|
298
|
+
await host.start();await host.call('loadDocument',{url:'data:text/x-qml,reset',contentType:'text/x-qml',source:`import QtQuick
|
|
299
|
+
Item {width:300;height:200
|
|
300
|
+
ListView {id:view;width:200;height:100;model:100;delegate:Text {required property int index;text:"row "+index;width:200;height:20}}
|
|
301
|
+
Text {id:offset;y:110;text:String(view.contentY)+","+view.model+","+view.count}
|
|
302
|
+
Button {id:scroll;y:140;onClicked:view.positionViewAtIndex(50,ListView.Beginning)}
|
|
303
|
+
Button {id:replace;x:100;y:140;onClicked:view.model=80}
|
|
304
|
+
}`});await host.call('whenLoaded');
|
|
305
|
+
await host.call('evaluate','document.getElementById("scroll").click()');
|
|
306
|
+
await wait(()=>host.call('evaluate','document.getElementById("offset").textContent'),v=>Number(v.split(",")[0])>0);
|
|
307
|
+
await host.call('evaluate','document.getElementById("replace").click()');
|
|
308
|
+
await wait(()=>host.call('evaluate','document.getElementById("offset").textContent'),v=>Number(v.split(",")[0])===0);
|
|
309
|
+
expect(await host.call('evaluate','document.body.innerText')).toContain('row 0');
|
|
310
|
+
}finally{host.close();}
|
|
311
|
+
},30000);
|
|
312
|
+
|
|
313
|
+
test('ListView positions unequal delegates by measured extent and updates following rows when a later row grows',async()=>{
|
|
314
|
+
const host=new RendererHost({timeout:20000});
|
|
315
|
+
try{
|
|
316
|
+
await host.start();await host.call('loadDocument',{url:'data:text/x-qml,variable',contentType:'text/x-qml',source:`import QtQuick
|
|
317
|
+
Item {id:root;width:400;height:400;property int extra:0
|
|
318
|
+
ListView {id:view;width:200;height:300;spacing:5;model:[20,80,35,60]
|
|
319
|
+
delegate:Rectangle {required property int index;required property var modelData;width:200;height:modelData+(index===1?root.extra:0)}
|
|
320
|
+
}
|
|
321
|
+
Text {id:report;x:220}
|
|
322
|
+
Button {id:inspect;y:330;onClicked:report.text=JSON.stringify({rows:[0,1,2,3].map(i=>{var c=view.itemAtIndex(i);return c?[c.y,c.height]:null}),index:view.indexAt(10,110),gap:view.indexAt(10,22),content:view.contentHeight})}
|
|
323
|
+
Button {id:grow;x:100;y:330;onClicked:root.extra=50}
|
|
324
|
+
}`});await host.call('whenLoaded');
|
|
325
|
+
const read=async()=>{await host.call('evaluate','document.getElementById("inspect").click()');return JSON.parse(await host.call('evaluate','document.getElementById("report").textContent'));};
|
|
326
|
+
let state=await wait(read,s=>s.rows[3]?.[0]===150);
|
|
327
|
+
expect(state.rows).toEqual([[0,20],[25,80],[110,35],[150,60]]);expect(state.index).toBe(2);expect(state.gap).toBe(-1);
|
|
328
|
+
await host.call('evaluate','document.getElementById("grow").click()');
|
|
329
|
+
state=await wait(read,s=>s.rows[3]?.[0]===200);
|
|
330
|
+
expect(state.rows).toEqual([[0,20],[25,130],[160,35],[200,60]]);expect(state.content).toBe(265);
|
|
331
|
+
}finally{host.close();}
|
|
332
|
+
},30000);
|