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,182 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {RendererHost} from '../lib/renderer/host.js';
|
|
3
|
+
import {qmlWindowConstraints} from '../lib/qml/window-constraints.js';
|
|
4
|
+
test('window constraints normalize and update Wayland size hints',async()=>{
|
|
5
|
+
const calls=[];let hints=['[360,240]','[200,0]'];
|
|
6
|
+
const sync=qmlWindowConstraints({request:(...args)=>calls.push(args)},{toplevel:8,surface:6},{url:'file:///test.qml',evaluate:async()=>hints});
|
|
7
|
+
await sync();await sync();
|
|
8
|
+
expect(calls).toEqual([[8,'set_min_size',360,240],[8,'set_max_size',360,0],[6,'commit']]);
|
|
9
|
+
hints=null;await sync();expect(calls.slice(3)).toEqual([[8,'set_min_size',0,0],[8,'set_max_size',0,0],[6,'commit']]);
|
|
10
|
+
});
|
|
11
|
+
test('FloatingWindow minimum size, attached window notifications and Button slots',async()=>{
|
|
12
|
+
const host=new RendererHost({timeout:20000});
|
|
13
|
+
try{
|
|
14
|
+
await host.start();
|
|
15
|
+
await host.call('loadDocument',{url:'data:text/x-qml,controls',contentType:'text/x-qml',source:`import QtQuick
|
|
16
|
+
import Quickshell
|
|
17
|
+
ShellRoot {
|
|
18
|
+
FloatingWindow {id:a;minimumSize:Qt.size(1500,1200)
|
|
19
|
+
Button {id:b;width:100;height:40;padding:5;focusPolicy:Qt.NoFocus
|
|
20
|
+
contentItem:Text {id:label;text:"hello"}
|
|
21
|
+
background:Rectangle {id:bg;color:"red"}
|
|
22
|
+
Window.onWindowChanged:globalThis.changes=(globalThis.changes||0)+1
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
FloatingWindow{id:c}
|
|
26
|
+
Component.onCompleted:{globalThis.a=a;globalThis.b=b;globalThis.c=c;globalThis.label=label;globalThis.bg=bg;}
|
|
27
|
+
}`});
|
|
28
|
+
await host.call('whenLoaded');
|
|
29
|
+
expect(await host.call('evaluate','[a.width,a.height]')).toEqual([1500,1200]);
|
|
30
|
+
expect(await host.call('evaluate','[label.x,label.y,label.width,label.height,bg.width,bg.height,b.activeFocusOnTab]')).toEqual([5,5,90,30,100,40,false]);
|
|
31
|
+
expect(await host.call('evaluate','b.Window.window===a')).toBe(true);
|
|
32
|
+
const before=await host.call('evaluate','globalThis.changes');
|
|
33
|
+
await host.call('evaluate','b.parent=c');await Bun.sleep(20);
|
|
34
|
+
expect(await host.call('evaluate','b.Window.window===c')).toBe(true);
|
|
35
|
+
expect(await host.call('evaluate','globalThis.changes')).toBe(before+1);
|
|
36
|
+
}finally{host.close();}
|
|
37
|
+
},20000);
|
|
38
|
+
|
|
39
|
+
test('ListModel roles populate required Repeater properties and remain reactive',async()=>{
|
|
40
|
+
const host=new RendererHost({timeout:20000});
|
|
41
|
+
try{
|
|
42
|
+
await host.start();await host.call('loadDocument',{url:'data:text/x-qml,roles',contentType:'text/x-qml',source:`import QtQuick
|
|
43
|
+
Item {
|
|
44
|
+
ListModel{id:model}
|
|
45
|
+
Repeater{id:rep;model:model;delegate:Text {required property string key;required property int amount;text:key+amount}}
|
|
46
|
+
Component.onCompleted:{globalThis.rep=rep;globalThis.rows=model;model.append({key:"a",amount:2});}
|
|
47
|
+
}`});await host.call('whenLoaded');
|
|
48
|
+
expect(await host.call('evaluate','[rep.count,rep.itemAt(0).text]')).toEqual([1,'a2']);
|
|
49
|
+
await host.call('evaluate','rows.setProperty(0,"amount",3)');await Bun.sleep(20);
|
|
50
|
+
expect(await host.call('evaluate','rep.itemAt(0).text')).toBe('a3');
|
|
51
|
+
}finally{host.close();}
|
|
52
|
+
},20000);
|
|
53
|
+
|
|
54
|
+
test('nested Loader startup preserves outer property notifications',async()=>{
|
|
55
|
+
const host=new RendererHost({timeout:20000});
|
|
56
|
+
try{
|
|
57
|
+
await host.start();await host.call('loadDocument',{url:'data:text/x-qml,nested-notifications',contentType:'text/x-qml',source:`import QtQuick
|
|
58
|
+
Item {id:root
|
|
59
|
+
property int counter:0
|
|
60
|
+
onCounterChanged:globalThis.changes=(globalThis.changes||[]).concat([counter])
|
|
61
|
+
Loader {sourceComponent:Item {property size dimensions;property real side:dimensions.width;Component.onCompleted:globalThis.initialSide=side}}
|
|
62
|
+
Component.onCompleted:{globalThis.root=root;counter=1;}
|
|
63
|
+
}`});await host.call('whenLoaded');
|
|
64
|
+
expect(await host.call('evaluate','globalThis.initialSide')).toBe(0);
|
|
65
|
+
expect(await host.call('evaluate','globalThis.changes')).toEqual([1]);
|
|
66
|
+
await host.call('evaluate','root.counter=2');await Bun.sleep(20);
|
|
67
|
+
expect(await host.call('evaluate','globalThis.changes')).toEqual([1,2]);
|
|
68
|
+
}finally{host.close();}
|
|
69
|
+
},20000);
|
|
70
|
+
|
|
71
|
+
import {createQt} from '../lib/qml/builtins.js';
|
|
72
|
+
test('Qt date/time formatting uses local date fields and locale format order',()=>{
|
|
73
|
+
const qt=createQt(),date=new Date(2024,10,23,17,45,36);
|
|
74
|
+
expect(qt.formatTime(date,'HH:mm')).toBe('17:45');
|
|
75
|
+
expect(qt.formatTime(date,'h:mm AP')).toBe('5:45 PM');
|
|
76
|
+
expect(qt.formatDateTime(date,"yyyy-MM-dd 'at' HH:mm")).toBe('2024-11-23 at 17:45');
|
|
77
|
+
expect(qt.locale('en_US').measurementSystem).toBe(1);
|
|
78
|
+
expect(qt.locale('zh_TW').measurementSystem).toBe(0);
|
|
79
|
+
expect(qt.locale('en_US').dateFormat(1).indexOf('M')).toBeLessThan(qt.locale('en_US').dateFormat(1).indexOf('d'));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('createObject does not make the owning binding depend on new child state',async()=>{
|
|
83
|
+
const host=new RendererHost({timeout:20000});
|
|
84
|
+
try{
|
|
85
|
+
await host.start();await host.call('loadDocument',{url:'data:text/x-qml,factory-tracking',contentType:'text/x-qml',source:`import QtQuick
|
|
86
|
+
Item {id:root
|
|
87
|
+
property var child:factory.createObject(root)
|
|
88
|
+
property Component factory:Component {Item {property int counter:0;property int derived:counter+1;Component.onCompleted:globalThis.created=(globalThis.created||0)+1}}
|
|
89
|
+
Component.onCompleted:{globalThis.root=root;globalThis.first=child;}
|
|
90
|
+
}`});await host.call('whenLoaded');
|
|
91
|
+
expect(await host.call('evaluate','globalThis.created')).toBe(1);
|
|
92
|
+
await host.call('evaluate','first.counter=9');await Bun.sleep(20);
|
|
93
|
+
expect(await host.call('evaluate','[root.child===first,root.child.derived,globalThis.created]')).toEqual([true,10,1]);
|
|
94
|
+
}finally{host.close();}
|
|
95
|
+
},20000);
|
|
96
|
+
|
|
97
|
+
test('Button custom content sizes its RowLayout slot and reacts without a width cycle',async()=>{
|
|
98
|
+
const host=new RendererHost({timeout:20000});
|
|
99
|
+
try {
|
|
100
|
+
await host.start();
|
|
101
|
+
await host.call('loadDocument',{url:'data:text/x-qml,button-size',contentType:'text/x-qml',source:`import QtQuick
|
|
102
|
+
import QtQuick.Layouts
|
|
103
|
+
import Quickshell
|
|
104
|
+
FloatingWindow {
|
|
105
|
+
RowLayout { width:500; height:60; spacing:8
|
|
106
|
+
Button {id:button;padding:4
|
|
107
|
+
contentItem: RowLayout {spacing:6
|
|
108
|
+
Rectangle {id:first;implicitWidth:80;implicitHeight:20}
|
|
109
|
+
Rectangle {implicitWidth:100;implicitHeight:24}
|
|
110
|
+
}
|
|
111
|
+
background: Rectangle {implicitWidth:120;implicitHeight:30}
|
|
112
|
+
}
|
|
113
|
+
Rectangle {id:next;implicitWidth:30;implicitHeight:30}
|
|
114
|
+
Item {Layout.fillWidth:true}
|
|
115
|
+
}
|
|
116
|
+
Component.onCompleted:{globalThis.button=button;globalThis.first=first;globalThis.next=next;}
|
|
117
|
+
}`});
|
|
118
|
+
await host.call('whenLoaded');
|
|
119
|
+
expect(await host.call('evaluate','[button.implicitWidth,button.implicitHeight,button.width,next.x]')).toEqual([194,32,194,202]);
|
|
120
|
+
await host.call('evaluate','first.implicitWidth=120');await Bun.sleep(20);
|
|
121
|
+
expect(await host.call('evaluate','[button.implicitWidth,button.width,next.x]')).toEqual([234,234,242]);
|
|
122
|
+
}finally{host.close();}
|
|
123
|
+
},20000);
|
|
124
|
+
|
|
125
|
+
test('Layouts retain explicit item dimensions for status dots and palette strips',async()=>{
|
|
126
|
+
const host=new RendererHost({timeout:20000});
|
|
127
|
+
try {
|
|
128
|
+
await host.start();
|
|
129
|
+
await host.call('loadDocument',{url:'data:text/x-qml,layout-fallback',contentType:'text/x-qml',source:`import QtQuick
|
|
130
|
+
import QtQuick.Layouts
|
|
131
|
+
import Quickshell
|
|
132
|
+
FloatingWindow {
|
|
133
|
+
RowLayout {id:row;width:300;height:30
|
|
134
|
+
Rectangle {id:dot;width:8;height:8;radius:4;color:"green"}
|
|
135
|
+
Item {Layout.fillWidth:true}
|
|
136
|
+
}
|
|
137
|
+
ColumnLayout {id:column;y:40;width:300;height:22;spacing:4
|
|
138
|
+
Rectangle {id:strip;Layout.fillWidth:true;height:6;color:"red"}
|
|
139
|
+
Item {implicitHeight:12;Layout.fillWidth:true}
|
|
140
|
+
}
|
|
141
|
+
GridLayout {y:80;columns:1
|
|
142
|
+
Rectangle {id:cell;width:11;height:7}
|
|
143
|
+
}
|
|
144
|
+
Component.onCompleted:{globalThis.dot=dot;globalThis.strip=strip;globalThis.cell=cell;globalThis.row=row;globalThis.column=column;}
|
|
145
|
+
}`});
|
|
146
|
+
await host.call('whenLoaded');
|
|
147
|
+
expect(await host.call('evaluate','[dot.width,dot.height,strip.width,strip.height,cell.width,cell.height]')).toEqual([8,8,300,6,11,7]);
|
|
148
|
+
await host.call('evaluate','row.width=500;column.width=500');await Bun.sleep(20);
|
|
149
|
+
expect(await host.call('evaluate','[dot.width,dot.height,strip.width,strip.height]')).toEqual([8,8,500,6]);
|
|
150
|
+
await host.call('evaluate','row.width=300;column.width=300');await Bun.sleep(20);
|
|
151
|
+
expect(await host.call('evaluate','[dot.width,dot.height,strip.width,strip.height]')).toEqual([8,8,300,6]);
|
|
152
|
+
}finally{host.close();}
|
|
153
|
+
},20000);
|
|
154
|
+
|
|
155
|
+
test('Nested Layout receives explicit size bindings even when the dot starts hidden',async()=>{
|
|
156
|
+
const host=new RendererHost({timeout:20000});
|
|
157
|
+
try {
|
|
158
|
+
await host.start();
|
|
159
|
+
await host.call('loadDocument',{url:'data:text/x-qml,late-dot',contentType:'text/x-qml',source:`import QtQuick
|
|
160
|
+
import QtQuick.Layouts
|
|
161
|
+
import Quickshell
|
|
162
|
+
FloatingWindow {
|
|
163
|
+
property bool ready:false
|
|
164
|
+
id:root
|
|
165
|
+
ColumnLayout {anchors.fill:parent
|
|
166
|
+
RowLayout {Layout.fillWidth:true
|
|
167
|
+
Text {text:"TITLE"}
|
|
168
|
+
Item {Layout.fillWidth:true}
|
|
169
|
+
RowLayout {
|
|
170
|
+
spacing:8
|
|
171
|
+
Rectangle {id:dot;width:8;height:8;radius:4;visible:root.ready;Layout.alignment:Qt.AlignVCenter;color:"blue"}
|
|
172
|
+
Text {text:"LIVE"}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
Component.onCompleted:{globalThis.root=root;globalThis.dot=dot;}
|
|
177
|
+
}`});
|
|
178
|
+
await host.call('whenLoaded');
|
|
179
|
+
await host.call('evaluate','root.ready=true');await Bun.sleep(20);
|
|
180
|
+
expect(await host.call('evaluate','[dot.width,dot.height,dot.visible]')).toEqual([8,8,true]);
|
|
181
|
+
}finally{host.close();}
|
|
182
|
+
},20000);
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { createBrowser } from '../lib/headless-shell.js';
|
|
4
|
+
test('local Qt.createComponent creates DOM, shares singleton bindings and destroys instances', async () => {
|
|
5
|
+
const dir = mkdtempSync('/tmp/qml-dynamic-');
|
|
6
|
+
const write = (name, text) => Bun.write(dir + '/' + name, text);
|
|
7
|
+
await write(
|
|
8
|
+
'qmldir',
|
|
9
|
+
'singleton Theme 1.0 Theme.qml\nsingleton Extra 1.0 Extra.qml\n',
|
|
10
|
+
);
|
|
11
|
+
await write(
|
|
12
|
+
'Theme.qml',
|
|
13
|
+
'pragma Singleton\nimport QtQuick\nQtObject {property int count: 1}',
|
|
14
|
+
);
|
|
15
|
+
await write(
|
|
16
|
+
'Extra.qml',
|
|
17
|
+
'pragma Singleton\nimport QtQuick\nQtObject {property int count: 10}',
|
|
18
|
+
);
|
|
19
|
+
await write(
|
|
20
|
+
'Cell.qml',
|
|
21
|
+
`import QtQuick
|
|
22
|
+
Rectangle {
|
|
23
|
+
required property string label
|
|
24
|
+
property int value: 0
|
|
25
|
+
width: 200; height: 30
|
|
26
|
+
Text {text: label+":"+Theme.count+":"+Extra.count+":"+value}
|
|
27
|
+
Component.onCompleted: Extra.count++
|
|
28
|
+
}`,
|
|
29
|
+
);
|
|
30
|
+
await write(
|
|
31
|
+
'main.qml',
|
|
32
|
+
`import QtQuick
|
|
33
|
+
Rectangle {
|
|
34
|
+
id:root;width:600;height:300
|
|
35
|
+
property int source: 3
|
|
36
|
+
property var cells: []
|
|
37
|
+
property var factory: null
|
|
38
|
+
Text {id:status;text:Theme.count}
|
|
39
|
+
Button {id:add;text:"Add";onClicked:{
|
|
40
|
+
if(!root.factory)root.factory=Qt.createComponent("Cell.qml")
|
|
41
|
+
const cell=root.factory.createObject(root,{label:"cell",value:Qt.binding(function(){return root.source})})
|
|
42
|
+
if(!cell)throw Error(root.factory.errorString())
|
|
43
|
+
root.cells=root.cells.concat([cell])
|
|
44
|
+
}}
|
|
45
|
+
Button {id:change;onClicked:{Theme.count++;root.source=7}}
|
|
46
|
+
Button {id:remove;onClicked:{root.cells[0].destroy();root.cells=root.cells.slice(1)}}
|
|
47
|
+
}`,
|
|
48
|
+
);
|
|
49
|
+
const b = await createBrowser({ spareRenderer: false });
|
|
50
|
+
try {
|
|
51
|
+
await b.context.navigate(Bun.pathToFileURL(dir + '/main.qml').href);
|
|
52
|
+
const click = (id) =>
|
|
53
|
+
b.context.evaluate(
|
|
54
|
+
'document.getElementById(' + JSON.stringify(id) + ').click()',
|
|
55
|
+
);
|
|
56
|
+
const texts = async () =>
|
|
57
|
+
JSON.parse(
|
|
58
|
+
await b.context.evaluate(
|
|
59
|
+
'JSON.stringify([...document.querySelectorAll("[data-qml-type=Text]")].map(e=>e.textContent))',
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
await click('add');
|
|
63
|
+
expect(await texts()).toEqual(['1', 'cell:1:11:3']);
|
|
64
|
+
await click('add');
|
|
65
|
+
expect(await texts()).toEqual(['1', 'cell:1:12:3', 'cell:1:12:3']);
|
|
66
|
+
await click('change');
|
|
67
|
+
expect(await texts()).toEqual(['2', 'cell:2:12:7', 'cell:2:12:7']);
|
|
68
|
+
await click('remove');
|
|
69
|
+
expect(await texts()).toEqual(['2', 'cell:2:12:7']);
|
|
70
|
+
await click('add');
|
|
71
|
+
expect(await texts()).toEqual(['2', 'cell:2:13:7', 'cell:2:13:7']);
|
|
72
|
+
} finally {
|
|
73
|
+
b.close();
|
|
74
|
+
rmSync(dir, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
}, 30000);
|
|
77
|
+
|
|
78
|
+
test('Loader creates inline and file components on demand, updates bindings and destroys hidden content', async () => {
|
|
79
|
+
const dir = mkdtempSync('/tmp/qml-loader-');
|
|
80
|
+
await Bun.write(
|
|
81
|
+
dir + '/Child.qml',
|
|
82
|
+
'import QtQuick\nText {required property string label; text:label}',
|
|
83
|
+
);
|
|
84
|
+
await Bun.write(
|
|
85
|
+
dir + '/main.qml',
|
|
86
|
+
`import QtQuick
|
|
87
|
+
Item {
|
|
88
|
+
id:root;width:500;height:300
|
|
89
|
+
property bool shown: false
|
|
90
|
+
property string label: "first"
|
|
91
|
+
property int loaded: 0
|
|
92
|
+
property int destroyed: 0
|
|
93
|
+
Component {id:body
|
|
94
|
+
Text {text:root.label;Component.onDestruction:root.destroyed++}
|
|
95
|
+
}
|
|
96
|
+
Loader {id:loader;active:root.shown;sourceComponent:body;onLoaded:root.loaded++}
|
|
97
|
+
Loader {id:fileLoader}
|
|
98
|
+
Text {id:status;text:root.loaded+":"+root.destroyed}
|
|
99
|
+
Button {id:show;onClicked:root.shown=!root.shown}
|
|
100
|
+
Button {id:change;onClicked:root.label="second"}
|
|
101
|
+
Button {id:file;onClicked:fileLoader.setSource("Child.qml",{label:Qt.binding(function(){return root.label})})}
|
|
102
|
+
}`,
|
|
103
|
+
);
|
|
104
|
+
const b = await createBrowser({ spareRenderer: false });
|
|
105
|
+
try {
|
|
106
|
+
await b.context.navigate(Bun.pathToFileURL(dir + '/main.qml').href);
|
|
107
|
+
const text = () => b.context.evaluate('document.body.innerText');
|
|
108
|
+
const settle = async (predicate) => {
|
|
109
|
+
for (let i = 0; i < 50; i++) {
|
|
110
|
+
if (await predicate()) return;
|
|
111
|
+
await Bun.sleep(10);
|
|
112
|
+
}
|
|
113
|
+
throw Error('Loader did not settle: ' + (await text()));
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const click = (id) =>
|
|
117
|
+
b.context.evaluate(
|
|
118
|
+
'document.getElementById(' + JSON.stringify(id) + ').click()',
|
|
119
|
+
);
|
|
120
|
+
expect(await text()).not.toContain('first');
|
|
121
|
+
await click('show');
|
|
122
|
+
await settle(async () => (await text()).includes('first'));
|
|
123
|
+
expect(await text()).toContain('first');
|
|
124
|
+
expect(await text()).toContain('1:0');
|
|
125
|
+
await click('file');
|
|
126
|
+
expect((await text()).match(/first/g)?.length).toBe(2);
|
|
127
|
+
await click('change');
|
|
128
|
+
await settle(async () => (await text()).match(/second/g)?.length === 2);
|
|
129
|
+
expect((await text()).match(/second/g)?.length).toBe(2);
|
|
130
|
+
await click('show');
|
|
131
|
+
await settle(async () => (await text()).includes('1:1'));
|
|
132
|
+
expect(await text()).toContain('1:1');
|
|
133
|
+
expect((await text()).match(/second/g)?.length).toBe(1);
|
|
134
|
+
await click('show');
|
|
135
|
+
await settle(async () => (await text()).includes('2:1'));
|
|
136
|
+
expect(await text()).toContain('2:1');
|
|
137
|
+
} finally {
|
|
138
|
+
b.close();
|
|
139
|
+
rmSync(dir, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
}, 30000);
|
|
142
|
+
|
|
143
|
+
test('synchronous Loader item observes a just-written bound activation in the same handler', async () => {
|
|
144
|
+
const dir = mkdtempSync('/tmp/qml-sync-loader-');
|
|
145
|
+
await Bun.write(
|
|
146
|
+
dir + '/main.qml',
|
|
147
|
+
`import QtQuick
|
|
148
|
+
Item {id:root;property bool built:false;property int answer:0
|
|
149
|
+
Component {id:body;Item {function result(){return 42}}}
|
|
150
|
+
Loader {id:loader;active:root.built;sourceComponent:body}
|
|
151
|
+
Text {id:status;text:root.answer}
|
|
152
|
+
Button {id:go;onClicked:{root.built=true;root.answer=loader.item.result()}}
|
|
153
|
+
}`,
|
|
154
|
+
);
|
|
155
|
+
const b = await createBrowser({ spareRenderer: false });
|
|
156
|
+
try {
|
|
157
|
+
await b.context.navigate(Bun.pathToFileURL(dir + '/main.qml').href);
|
|
158
|
+
await b.context.evaluate('document.getElementById("go").click()');
|
|
159
|
+
expect(
|
|
160
|
+
await b.context.evaluate('document.getElementById("status").textContent'),
|
|
161
|
+
).toBe('42');
|
|
162
|
+
} finally {
|
|
163
|
+
b.close();
|
|
164
|
+
rmSync(dir, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
}, 30000);
|
|
167
|
+
|
|
168
|
+
test('dynamic singleton references cannot be captured by application loop variables', async () => {
|
|
169
|
+
const dir = mkdtempSync('/tmp/qml-hygiene-');
|
|
170
|
+
await Bun.write(dir + '/qmldir', 'singleton Theme 1.0 Theme.qml\n');
|
|
171
|
+
await Bun.write(
|
|
172
|
+
dir + '/Theme.qml',
|
|
173
|
+
'pragma Singleton\nimport QtQuick\nQtObject {property int amount:7}',
|
|
174
|
+
);
|
|
175
|
+
await Bun.write(
|
|
176
|
+
dir + '/Child.qml',
|
|
177
|
+
`import QtQuick
|
|
178
|
+
Text {text:{var total=0;for(var r=0;r<3;r++){var n0=r;var j0=r;total+=Theme.amount+n0-j0;}return total}}`,
|
|
179
|
+
);
|
|
180
|
+
await Bun.write(
|
|
181
|
+
dir + '/main.qml',
|
|
182
|
+
`import QtQuick
|
|
183
|
+
Item {property int shared:Theme.amount;Loader {source:"Child.qml"}}`,
|
|
184
|
+
);
|
|
185
|
+
const b = await createBrowser({ spareRenderer: false });
|
|
186
|
+
try {
|
|
187
|
+
await b.context.navigate(Bun.pathToFileURL(dir + '/main.qml').href);
|
|
188
|
+
expect(await b.context.evaluate('document.body.innerText')).toBe('21');
|
|
189
|
+
} finally {
|
|
190
|
+
b.close();
|
|
191
|
+
rmSync(dir, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
}, 30000);
|
|
194
|
+
|
|
195
|
+
test('library scripts first imported by a delegate hoist their non-library dependencies into the library scope', async () => {
|
|
196
|
+
const dir = mkdtempSync('/tmp/qml-library-scope-');
|
|
197
|
+
await Bun.write(
|
|
198
|
+
dir + '/Utility.js',
|
|
199
|
+
'var count=0;function next(){return ++count;}',
|
|
200
|
+
);
|
|
201
|
+
await Bun.write(
|
|
202
|
+
dir + '/Library.js',
|
|
203
|
+
'.pragma library\n.import "Utility.js" as Utility\nfunction next(){return Utility.next();}',
|
|
204
|
+
);
|
|
205
|
+
await Bun.write(
|
|
206
|
+
dir + '/Cell.qml',
|
|
207
|
+
'import QtQuick\nimport "Library.js" as Library\nText {text:Library.next()}',
|
|
208
|
+
);
|
|
209
|
+
await Bun.write(
|
|
210
|
+
dir + '/main.qml',
|
|
211
|
+
'import QtQuick\nItem {Repeater {model:2;Cell {}}}',
|
|
212
|
+
);
|
|
213
|
+
const b = await createBrowser({ spareRenderer: false });
|
|
214
|
+
try {
|
|
215
|
+
await b.context.navigate(Bun.pathToFileURL(dir + '/main.qml').href);
|
|
216
|
+
expect(await b.context.evaluate('document.body.innerText')).toBe('12');
|
|
217
|
+
} finally {
|
|
218
|
+
b.close();
|
|
219
|
+
rmSync(dir, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
}, 30000);
|
|
222
|
+
|
|
223
|
+
test('alias writes detach an outer binding and Qt.binding reattaches it without replacing the alias', async () => {
|
|
224
|
+
const dir = mkdtempSync('/tmp/qml-alias-write-');
|
|
225
|
+
await Bun.write(
|
|
226
|
+
dir + '/Field.qml',
|
|
227
|
+
'import QtQuick\nItem {property alias text:label.text;Text {id:label}}',
|
|
228
|
+
);
|
|
229
|
+
await Bun.write(
|
|
230
|
+
dir + '/main.qml',
|
|
231
|
+
`import QtQuick
|
|
232
|
+
Item {id:root;property string source:"one"
|
|
233
|
+
Field {id:field;text:root.source}
|
|
234
|
+
Button {id:override;onClicked:{field.text="manual";root.source="two"}}
|
|
235
|
+
Button {id:rebind;onClicked:{field.text=Qt.binding(function(){return root.source});root.source="three"}}
|
|
236
|
+
}`,
|
|
237
|
+
);
|
|
238
|
+
const b = await createBrowser({ spareRenderer: false });
|
|
239
|
+
try {
|
|
240
|
+
await b.context.navigate(Bun.pathToFileURL(dir + '/main.qml').href);
|
|
241
|
+
const text = () =>
|
|
242
|
+
b.context.evaluate(
|
|
243
|
+
'document.querySelector("[data-qml-type=Text]").textContent',
|
|
244
|
+
);
|
|
245
|
+
expect(await text()).toBe('one');
|
|
246
|
+
await b.context.evaluate('document.getElementById("override").click()');
|
|
247
|
+
expect(await text()).toBe('manual');
|
|
248
|
+
await b.context.evaluate('document.getElementById("rebind").click()');
|
|
249
|
+
expect(await text()).toBe('three');
|
|
250
|
+
} finally {
|
|
251
|
+
b.close();
|
|
252
|
+
rmSync(dir, { recursive: true, force: true });
|
|
253
|
+
}
|
|
254
|
+
}, 30000);
|
|
255
|
+
|
|
256
|
+
test('named data aliases place title and buttons inside their real holders', async () => {
|
|
257
|
+
const dir = mkdtempSync('/tmp/qml-named-slots-');
|
|
258
|
+
await Bun.write(
|
|
259
|
+
dir + '/Card.qml',
|
|
260
|
+
`import QtQuick
|
|
261
|
+
Item {property alias title:top.data;property alias buttons:bottom.data;default property alias content:middle.data
|
|
262
|
+
Item {id:top;y:10}Item {id:middle;y:40}Item {id:bottom;y:80}}`,
|
|
263
|
+
);
|
|
264
|
+
await Bun.write(
|
|
265
|
+
dir + '/main.qml',
|
|
266
|
+
`import QtQuick
|
|
267
|
+
Card {title:Text {id:title;text:"Title"} buttons:Text {id:buttons;text:"Buttons"} Text {id:body;text:"Body"}}`,
|
|
268
|
+
);
|
|
269
|
+
const b = await createBrowser({ spareRenderer: false });
|
|
270
|
+
try {
|
|
271
|
+
await b.context.navigate(Bun.pathToFileURL(dir + '/main.qml').href);
|
|
272
|
+
expect(
|
|
273
|
+
await b.context.evaluate(
|
|
274
|
+
'document.getElementById("title").parentElement.style.top',
|
|
275
|
+
),
|
|
276
|
+
).toBe('10px');
|
|
277
|
+
expect(
|
|
278
|
+
await b.context.evaluate(
|
|
279
|
+
'document.getElementById("buttons").parentElement.style.top',
|
|
280
|
+
),
|
|
281
|
+
).toBe('80px');
|
|
282
|
+
expect(
|
|
283
|
+
await b.context.evaluate(
|
|
284
|
+
'document.getElementById("body").parentElement.style.top',
|
|
285
|
+
),
|
|
286
|
+
).toBe('40px');
|
|
287
|
+
} finally {
|
|
288
|
+
b.close();
|
|
289
|
+
rmSync(dir, { recursive: true, force: true });
|
|
290
|
+
}
|
|
291
|
+
}, 30000);
|
|
292
|
+
|
|
293
|
+
test('destroying a loaded component notifies nested objects once and still releases sibling factories', async () => {
|
|
294
|
+
const { mkdtempSync, rmSync } = await import('node:fs');
|
|
295
|
+
const dir = mkdtempSync('/tmp/qml-destruction-');
|
|
296
|
+
let browser;
|
|
297
|
+
try {
|
|
298
|
+
await Bun.write(
|
|
299
|
+
dir + '/main.qml',
|
|
300
|
+
`import QtQuick
|
|
301
|
+
Item {id:root;width:300;height:200;property string gone:""
|
|
302
|
+
Loader {id:loader;active:true;sourceComponent:Item {
|
|
303
|
+
Component.onDestruction:root.gone+="root,"
|
|
304
|
+
Item {Component.onDestruction:root.gone+="child,"}
|
|
305
|
+
Repeater {model:2;delegate:Item {required property int index;Component.onDestruction:root.gone+="row"+index+","}}
|
|
306
|
+
}}
|
|
307
|
+
Text {id:status;text:root.gone}
|
|
308
|
+
Button {id:destroy;y:80;onClicked:{loader.active=false;loader.active=false}}
|
|
309
|
+
}`,
|
|
310
|
+
);
|
|
311
|
+
const { createBrowser } = await import('../lib/headless-shell.js');
|
|
312
|
+
browser = await createBrowser({ spareRenderer: false });
|
|
313
|
+
await browser.context.navigate(Bun.pathToFileURL(dir + '/main.qml').href);
|
|
314
|
+
await browser.context.evaluate(
|
|
315
|
+
'document.getElementById("destroy").click()',
|
|
316
|
+
);
|
|
317
|
+
const gone = await browser.context.evaluate(
|
|
318
|
+
'document.getElementById("status").textContent',
|
|
319
|
+
);
|
|
320
|
+
expect(gone.split(',').filter(Boolean).sort()).toEqual([
|
|
321
|
+
'child',
|
|
322
|
+
'root',
|
|
323
|
+
'row0',
|
|
324
|
+
'row1',
|
|
325
|
+
]);
|
|
326
|
+
} finally {
|
|
327
|
+
browser?.close();
|
|
328
|
+
rmSync(dir, { recursive: true, force: true });
|
|
329
|
+
}
|
|
330
|
+
}, 30000);
|
|
331
|
+
|
|
332
|
+
test('shared JS libraries can create and release components relative to the script URL', async () => {
|
|
333
|
+
const dir = mkdtempSync('/tmp/qml-library-component-');
|
|
334
|
+
let browser;
|
|
335
|
+
try {
|
|
336
|
+
await Bun.write(dir + '/helpers/Factory.js', `.pragma library
|
|
337
|
+
function make(parent, label) {
|
|
338
|
+
const component=Qt.createComponent("Cell.qml");
|
|
339
|
+
if(component.status!==1)throw Error(component.errorString());
|
|
340
|
+
const object=component.createObject(parent,{text:label});
|
|
341
|
+
component.destroy();
|
|
342
|
+
return object;
|
|
343
|
+
}`);
|
|
344
|
+
await Bun.write(dir + '/helpers/Cell.qml', 'import QtQuick\nText {width:100;height:20}');
|
|
345
|
+
await Bun.write(dir + '/main.qml', `import QtQuick
|
|
346
|
+
import "helpers/Factory.js" as Factory
|
|
347
|
+
Item {id:root;property var first:null;property var second:null
|
|
348
|
+
Component.onCompleted:{first=Factory.make(root,"first");second=Factory.make(root,"second");second.y=30}
|
|
349
|
+
Button {id:remove;y:80;onClicked:root.first.destroy()}
|
|
350
|
+
}`);
|
|
351
|
+
browser=await createBrowser({spareRenderer:false});
|
|
352
|
+
await browser.context.navigate(Bun.pathToFileURL(dir+'/main.qml').href);
|
|
353
|
+
expect(await browser.context.evaluate('document.body.innerText')).toContain('first');
|
|
354
|
+
expect(await browser.context.evaluate('document.body.innerText')).toContain('second');
|
|
355
|
+
await browser.context.evaluate('document.getElementById("remove").click()');
|
|
356
|
+
expect(await browser.context.evaluate('document.body.innerText')).not.toContain('first');
|
|
357
|
+
expect(await browser.context.evaluate('document.body.innerText')).toContain('second');
|
|
358
|
+
} finally {browser?.close();rmSync(dir,{recursive:true,force:true});}
|
|
359
|
+
},30000);
|
|
360
|
+
|
|
361
|
+
test('children of an instantiated component resolve properties added at the use site; rejected MouseAreas pass clicks to underlying rows',async()=>{
|
|
362
|
+
const dir=mkdtempSync('/tmp/qml-click-');let browser;
|
|
363
|
+
try{
|
|
364
|
+
await Bun.write(dir+'/Cell.qml','import QtQuick\nRectangle {width:200;height:40;color:"red"}');
|
|
365
|
+
await Bun.write(dir+'/main.qml',`import QtQuick
|
|
366
|
+
Rectangle {width:300;height:150
|
|
367
|
+
property bool block: false
|
|
368
|
+
Cell {id:cell;property int rowIndex:7
|
|
369
|
+
TapHandler {onTapped:status.text=String(rowIndex)}
|
|
370
|
+
}
|
|
371
|
+
MouseArea {anchors.fill:cell;z:1;onPressed:mouse=>{mouse.accepted=false}}
|
|
372
|
+
MouseArea {anchors.fill:cell;z:2;onPressed:mouse=>{mouse.accepted=block}}
|
|
373
|
+
Text {id:status;y:60;text:"none"}
|
|
374
|
+
Button {id:gate;y:90;onClicked:block=true}
|
|
375
|
+
}`);
|
|
376
|
+
browser=await createBrowser({spareRenderer:false});await browser.context.resize(300,150);await browser.context.navigate(Bun.pathToFileURL(dir+'/main.qml').href);
|
|
377
|
+
await browser.context.click(20,20);await browser.context.evaluate('0');
|
|
378
|
+
expect(await browser.context.evaluate('document.getElementById("status").textContent')).toBe('7');
|
|
379
|
+
await browser.context.evaluate('document.getElementById("gate").click();document.getElementById("status").textContent="blocked"');await browser.context.evaluate('0');
|
|
380
|
+
await browser.context.click(20,20);
|
|
381
|
+
expect(await browser.context.evaluate('document.getElementById("status").textContent')).toBe('blocked');
|
|
382
|
+
}finally{browser?.close();rmSync(dir,{recursive:true,force:true});}
|
|
383
|
+
},30000);
|
|
384
|
+
|
|
385
|
+
test('dynamic objects belong to their explicit parent, not the Loader currently creating them',async()=>{
|
|
386
|
+
const dir=mkdtempSync('/tmp/qml-parent-owner-');let browser;
|
|
387
|
+
try{
|
|
388
|
+
await Bun.write(dir+'/Icon.qml','import QtQuick\nText {text:"surviving icon"}');
|
|
389
|
+
await Bun.write(dir+'/main.qml',`import QtQuick
|
|
390
|
+
Item {id:root;width:300;height:150
|
|
391
|
+
Item {id:keeper}
|
|
392
|
+
Loader {id:maker;sourceComponent:Text {text:"temporary maker";Component.onCompleted:{var c=Qt.createComponent("Icon.qml");c.createObject(keeper);c.destroy()}}}
|
|
393
|
+
Button {id:remove;y:50;onClicked:maker.active=false}
|
|
394
|
+
}`);
|
|
395
|
+
browser=await createBrowser({spareRenderer:false});await browser.context.navigate(Bun.pathToFileURL(dir+'/main.qml').href);
|
|
396
|
+
expect(await browser.context.evaluate('document.body.innerText')).toContain('surviving icon');
|
|
397
|
+
await browser.context.evaluate('document.getElementById("remove").click()');await browser.context.evaluate('0');
|
|
398
|
+
const text=await browser.context.evaluate('document.body.innerText');
|
|
399
|
+
expect(text).toContain('surviving icon');expect(text).not.toContain('temporary maker');
|
|
400
|
+
}finally{browser?.close();rmSync(dir,{recursive:true,force:true});}
|
|
401
|
+
},30000);
|