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.
Files changed (296) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +40 -4
  3. package/frontends/wayland/LICENSE-land +21 -0
  4. package/frontends/wayland/README.md +152 -0
  5. package/frontends/wayland/casty/LICENSE +21 -0
  6. package/frontends/wayland/casty/bookmarks.js +32 -0
  7. package/frontends/wayland/casty/click.js +96 -0
  8. package/frontends/wayland/casty/commands.js +61 -0
  9. package/frontends/wayland/casty/config.js +45 -0
  10. package/frontends/wayland/casty/download-directory.js +28 -0
  11. package/frontends/wayland/casty/download.js +49 -0
  12. package/frontends/wayland/casty/hints.js +334 -0
  13. package/frontends/wayland/casty/keys.js +56 -0
  14. package/frontends/wayland/casty/page-commands.js +80 -0
  15. package/frontends/wayland/casty/prompt-width.js +148 -0
  16. package/frontends/wayland/casty/urlbar.js +606 -0
  17. package/frontends/wayland/client.js +88 -0
  18. package/frontends/wayland/clipboard.js +80 -0
  19. package/frontends/wayland/index.js +130 -0
  20. package/frontends/wayland/input.js +75 -0
  21. package/frontends/wayland/protocol.js +55 -0
  22. package/frontends/wayland/protocols/wayland.xml +3248 -0
  23. package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
  24. package/frontends/wayland/transport.js +120 -0
  25. package/frontends/wayland/urlbar.js +162 -0
  26. package/index.js +28 -1
  27. package/lib/audio/shared-connection.js +31 -0
  28. package/lib/cdp-server.js +15 -2
  29. package/lib/clipboard-command.js +10 -0
  30. package/lib/clipboard-powershell.js +22 -0
  31. package/lib/clipboard.js +37 -2
  32. package/lib/document-types.js +11 -0
  33. package/lib/headless-shell.js +54 -12
  34. package/lib/layout/text-layout.js +17 -9
  35. package/lib/paint/display-list.js +6 -1
  36. package/lib/qml/README.md +1427 -0
  37. package/lib/qml/builtins.js +771 -0
  38. package/lib/qml/canvas-dom.js +115 -0
  39. package/lib/qml/canvas.js +76 -0
  40. package/lib/qml/document.js +1199 -0
  41. package/lib/qml/fonts.js +69 -0
  42. package/lib/qml/format.js +9 -0
  43. package/lib/qml/host.js +352 -0
  44. package/lib/qml/images.js +199 -0
  45. package/lib/qml/launcher.js +132 -0
  46. package/lib/qml/loader.js +521 -0
  47. package/lib/qml/media-renderer.js +49 -0
  48. package/lib/qml/media.js +86 -0
  49. package/lib/qml/models.js +622 -0
  50. package/lib/qml/parser.js +381 -0
  51. package/lib/qml/qs +5 -0
  52. package/lib/qml/qscasty +3 -0
  53. package/lib/qml/qsheadless +3 -0
  54. package/lib/qml/qswayland +3 -0
  55. package/lib/qml/reactive.js +182 -0
  56. package/lib/qml/runtime.js +3603 -0
  57. package/lib/qml/shader/fragment.js +83 -0
  58. package/lib/qml/shader/qsb.js +27 -0
  59. package/lib/qml/shader/renderer.js +39 -0
  60. package/lib/qml/shader/runtime.js +61 -0
  61. package/lib/qml/shapes.js +251 -0
  62. package/lib/qml/shortcuts.js +65 -0
  63. package/lib/qml/text.js +109 -0
  64. package/lib/qml/textures.js +126 -0
  65. package/lib/qml/wayland-clipboard.js +32 -0
  66. package/lib/qml/window-constraints.js +16 -0
  67. package/lib/qml/worker-thread.js +70 -0
  68. package/lib/render-tree/index.js +16 -1
  69. package/lib/renderer/bindings.js +16 -1
  70. package/lib/renderer/canvas-2d.js +29 -0
  71. package/lib/renderer/document-selection.js +19 -0
  72. package/lib/renderer/host.js +45 -17
  73. package/lib/renderer/main.js +21 -6
  74. package/lib/renderer/page-realm.js +20 -10
  75. package/lib/renderer/page-renderer.js +497 -42
  76. package/lib/renderer/pointer-move.js +5 -3
  77. package/lib/renderer/render-interval.js +9 -0
  78. package/lib/renderer/text-input-paint.js +54 -0
  79. package/lib/renderer/text-input.js +57 -0
  80. package/lib/style/computed-style.js +10 -4
  81. package/package.json +1 -1
  82. package/test/audio-connection-lifetime.test.js +46 -0
  83. package/test/canvas-paint.test.js +8 -0
  84. package/test/clipboard.test.js +19 -0
  85. package/test/computed-style.test.js +12 -0
  86. package/test/document-selection.test.js +27 -0
  87. package/test/double-click.test.js +85 -0
  88. package/test/fixtures/canvas-damage.js +40 -0
  89. package/test/fixtures/fake-renderer.js +1 -0
  90. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  91. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  92. package/test/fixtures/flea-helpers/bin/bat +3 -0
  93. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  94. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  95. package/test/fixtures/flea-helpers/bin/glow +3 -0
  96. package/test/fixtures/flea-helpers/bin/qs +3 -0
  97. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  98. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  99. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  100. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  101. package/test/fixtures/flea-helpers/check.mjs +36 -0
  102. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  103. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  104. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  105. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  106. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  107. package/test/fixtures/omastorm-helpers/Dockerfile +35 -0
  108. package/test/fixtures/omastorm-helpers/check.mjs +56 -0
  109. package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
  110. package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
  111. package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
  112. package/test/fixtures/pointer-paint-batching.js +23 -0
  113. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  114. package/test/fixtures/qml/flea/LICENSE +21 -0
  115. package/test/fixtures/qml/flea/README.md +25 -0
  116. package/test/fixtures/qml/flea/Theme.qml +8 -0
  117. package/test/fixtures/qml/flea/demo.qml +24 -0
  118. package/test/fixtures/qml/flea/qmldir +3 -0
  119. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  120. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  121. package/test/fixtures/qml/flea-theme/README.md +70 -0
  122. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  123. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  124. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  125. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  126. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  127. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  128. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  129. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  130. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  131. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  132. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  133. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  134. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  135. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  136. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  137. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  138. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  139. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  140. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  141. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  142. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  143. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  144. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  145. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  146. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  147. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  148. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  167. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  168. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  169. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  170. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  171. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  172. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  173. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  174. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  175. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  176. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  177. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  178. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  179. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  180. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  181. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  182. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  183. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  184. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  185. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  186. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  187. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  188. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  189. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  190. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  191. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  192. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  193. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  194. package/test/fixtures/qml/hello.qml +26 -0
  195. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  196. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  197. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  198. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  199. package/test/fixtures/qml/imports/logic/base.js +2 -0
  200. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  201. package/test/fixtures/qml/imports/logic/math.js +3 -0
  202. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  203. package/test/fixtures/qml/imports/main.qml +21 -0
  204. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  205. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  206. package/test/fixtures/qml/multiply.qml +61 -0
  207. package/test/fixtures/qml-canvas-dom.js +90 -0
  208. package/test/fixtures/qml-fonts/main.qml +10 -0
  209. package/test/fixtures/qml-locale/check.mjs +6 -0
  210. package/test/fixtures/qml-locale/main.qml +8 -0
  211. package/test/fixtures/qml-media-audio.js +39 -0
  212. package/test/fixtures/qml-shader/grab.qml +14 -0
  213. package/test/fixtures/qml-shader/gradient.frag +5 -0
  214. package/test/fixtures/qml-shader/main.qml +14 -0
  215. package/test/fixtures/qml-shader/texture-grab.qml +25 -0
  216. package/test/fixtures/qml-shader/texture-size.frag +5 -0
  217. package/test/fixtures/qml-shader-check.mjs +24 -0
  218. package/test/fixtures/title-no-paint.js +20 -0
  219. package/test/helpers/flea-sandbox.js +48 -0
  220. package/test/helpers/quickshell-fixture.js +125 -0
  221. package/test/html-clipboard-wayland.test.js +47 -0
  222. package/test/index.test.js +13 -0
  223. package/test/manual-casty-without-qml.js +87 -0
  224. package/test/manual-double-click.html +21 -0
  225. package/test/manual-flea-application.js +49 -0
  226. package/test/manual-flea-binary.js +62 -0
  227. package/test/manual-flea-qml.js +9 -0
  228. package/test/manual-flea-settings.js +40 -0
  229. package/test/manual-flea-theme.js +40 -0
  230. package/test/manual-mouse-buttons.html +18 -0
  231. package/test/manual-omastorm-engine.mjs +19 -0
  232. package/test/manual-omastorm-shaders.mjs +28 -0
  233. package/test/manual-omastorm-software.mjs +25 -0
  234. package/test/manual-pointer-scroll.html +43 -0
  235. package/test/manual-qml-removal.js +46 -0
  236. package/test/manual-without-qml.js +45 -0
  237. package/test/pointer-drag.test.js +104 -0
  238. package/test/qml-canvas-dom.test.js +37 -0
  239. package/test/qml-canvas.test.js +21 -0
  240. package/test/qml-clipboard-bridge.test.js +25 -0
  241. package/test/qml-clipboard-wayland.test.js +40 -0
  242. package/test/qml-connections-timing.test.js +95 -0
  243. package/test/qml-controls-window.test.js +182 -0
  244. package/test/qml-dynamic.test.js +401 -0
  245. package/test/qml-flea-app.test.js +146 -0
  246. package/test/qml-flea-backend.test.js +78 -0
  247. package/test/qml-flea-columns-backend.test.js +38 -0
  248. package/test/qml-flea-compile.test.js +78 -0
  249. package/test/qml-flea-launcher.test.js +88 -0
  250. package/test/qml-flea-markdown-backend.test.js +31 -0
  251. package/test/qml-flea-media.test.js +28 -0
  252. package/test/qml-flea-navigation-backend.test.js +35 -0
  253. package/test/qml-flea-navigation-timing.test.js +147 -0
  254. package/test/qml-flea-preview-backend.test.js +53 -0
  255. package/test/qml-flea-previews.test.js +163 -0
  256. package/test/qml-flea-rows.test.js +101 -0
  257. package/test/qml-flea-textures.test.js +59 -0
  258. package/test/qml-flea-view-switch.test.js +33 -0
  259. package/test/qml-flea-wayland.test.js +68 -0
  260. package/test/qml-flea.test.js +34 -0
  261. package/test/qml-fonts.test.js +31 -0
  262. package/test/qml-grid-multiline.test.js +48 -0
  263. package/test/qml-host.test.js +238 -0
  264. package/test/qml-imports.test.js +102 -0
  265. package/test/qml-inherited-handlers.test.js +51 -0
  266. package/test/qml-input-selection.test.js +66 -0
  267. package/test/qml-interactions.test.js +309 -0
  268. package/test/qml-language.test.js +167 -0
  269. package/test/qml-launcher.test.js +54 -0
  270. package/test/qml-layout-text.test.js +205 -0
  271. package/test/qml-locale.test.js +45 -0
  272. package/test/qml-media.test.js +63 -0
  273. package/test/qml-nonvisual-visible.test.js +19 -0
  274. package/test/qml-persisted-delegates.test.js +45 -0
  275. package/test/qml-quickshell-controls.test.js +65 -0
  276. package/test/qml-quickshell-settings.test.js +62 -0
  277. package/test/qml-repeater.test.js +72 -0
  278. package/test/qml-shader-runtime.test.js +6 -0
  279. package/test/qml-shortcuts.test.js +39 -0
  280. package/test/qml-socket.test.js +60 -0
  281. package/test/qml-software-shader.test.js +55 -0
  282. package/test/qml-style-updates.test.js +124 -0
  283. package/test/qml-textures.test.js +66 -0
  284. package/test/qml-views.test.js +332 -0
  285. package/test/qml-window-update.test.js +36 -0
  286. package/test/qml-worker.test.js +69 -0
  287. package/test/qml.test.js +83 -0
  288. package/test/render-interval.test.js +33 -0
  289. package/test/renderer-host.test.js +11 -0
  290. package/test/text-input-pointer.test.js +31 -0
  291. package/test/text-input-scroll.test.js +29 -0
  292. package/test/text-input.test.js +47 -0
  293. package/test/title-no-paint.test.js +6 -0
  294. package/test/wayland-frontend.test.js +83 -0
  295. package/test/wayland-scroll.test.js +40 -0
  296. 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);