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,205 @@
1
+ import { test, expect } from 'bun:test';
2
+ import { RendererHost } from '../lib/renderer/host.js';
3
+ async function fixture(source, run) {
4
+ const host = new RendererHost({ timeout: 20000 });
5
+ try {
6
+ await host.start();
7
+ await host.call('loadDocument', {
8
+ url: 'data:text/x-qml,layout',
9
+ contentType: 'text/x-qml',
10
+ source,
11
+ });
12
+ await host.call('whenLoaded');
13
+ await run(host);
14
+ } finally {
15
+ host.close();
16
+ }
17
+ }
18
+ test('Flow wraps dialog buttons and baseline anchors align different font sizes', async () => {
19
+ await fixture(
20
+ `import QtQuick
21
+ Item {width:400;height:300
22
+ Flow {id:flow;width:150;spacing:10
23
+ Rectangle {id:a;width:100;height:20}
24
+ Rectangle {id:b;width:80;height:30}
25
+ Rectangle {id:c;width:50;height:10}
26
+ }
27
+ Text {id:large;y:100;text:"Large";font.pixelSize:30}
28
+ Text {id:small;x:120;anchors.baseline:large.baseline;text:"Small";font.pixelSize:12}
29
+ Text {id:status;y:200;text:b.x+":"+b.y+":"+c.x+":"+c.y+":"+flow.height+":"+Math.round(small.y+small.baselineOffset-large.y-large.baselineOffset)}
30
+ }`,
31
+ async (h) => {
32
+ expect(
33
+ await h.call(
34
+ 'evaluate',
35
+ 'document.getElementById("status").textContent',
36
+ ),
37
+ ).toBe('0:30:90:30:60:0');
38
+ },
39
+ );
40
+ }, 30000);
41
+ test('RowLayout shrinks and fills bounded children without losing alignment', async () => {
42
+ await fixture(
43
+ `import QtQuick
44
+ import QtQuick.Layouts
45
+ Item {width:400;height:200
46
+ RowLayout {width:200;height:40;spacing:10
47
+ Rectangle {id:a;implicitWidth:40;implicitHeight:20;Layout.alignment:Qt.AlignVCenter}
48
+ Rectangle {id:b;implicitWidth:50;implicitHeight:10;Layout.fillWidth:true;Layout.maximumWidth:60;Layout.alignment:Qt.AlignBottom}
49
+ Rectangle {id:c;implicitWidth:10;implicitHeight:10;Layout.fillWidth:true;Layout.fillHeight:true}
50
+ }
51
+ Text {id:status;y:100;text:a.width+":"+a.y+":"+b.width+":"+b.y+":"+c.width+":"+c.height}
52
+ }`,
53
+ async (h) => {
54
+ expect(
55
+ await h.call(
56
+ 'evaluate',
57
+ 'document.getElementById("status").textContent',
58
+ ),
59
+ ).toBe('40:10:60:30:80:40');
60
+ },
61
+ );
62
+ }, 30000);
63
+ test('StyledText creates inert styled DOM, emits link activation and PlainText stays literal', async () => {
64
+ await fixture(
65
+ `import QtQuick
66
+ Item {id:root;width:400;height:200;property string link:""
67
+ Text {id:rich;width:300;height:40;textFormat:Text.StyledText;text:"<b>Bold</b> <a href='https://example.com'>link</a><script>globalThis.bad=true</script>";onLinkActivated:function(link){root.link=link}}
68
+ Text {id:plain;y:50;text:"<b>literal</b>";textFormat:Text.PlainText}
69
+ Text {id:status;y:100;text:root.link}
70
+ }`,
71
+ async (h) => {
72
+ expect(
73
+ await h.call(
74
+ 'evaluate',
75
+ 'document.getElementById("rich").querySelector("b").textContent',
76
+ ),
77
+ ).toBe('Bold');
78
+ expect(
79
+ await h.call(
80
+ 'evaluate',
81
+ 'document.getElementById("rich").querySelector("script")===null&&globalThis.bad===undefined',
82
+ ),
83
+ ).toBe(true);
84
+ expect(
85
+ await h.call(
86
+ 'evaluate',
87
+ 'document.getElementById("plain").textContent',
88
+ ),
89
+ ).toBe('<b>literal</b>');
90
+ await h.call('evaluate', 'document.querySelector("a").click()');
91
+ expect(
92
+ await h.call(
93
+ 'evaluate',
94
+ 'document.getElementById("status").textContent',
95
+ ),
96
+ ).toBe('https://example.com');
97
+ },
98
+ );
99
+ }, 30000);
100
+
101
+ test('ShapePath linear gradients draw through the existing page canvas while solid paths retain SVG', async () => {
102
+ await fixture(
103
+ `import QtQuick
104
+ import QtQuick.Shapes
105
+ Item {width:200;height:100
106
+ Shape {width:100;height:40
107
+ ShapePath {strokeWidth:0;fillGradient:LinearGradient {x1:0;y1:0;x2:100;y2:0
108
+ GradientStop {position:0;color:"red"} GradientStop {position:1;color:"blue"}}
109
+ PathSvg {path:"M0 0 H100 V40 H0 Z"}
110
+ }
111
+ }
112
+ Shape {x:110;width:40;height:40;ShapePath {fillColor:"green";strokeWidth:0;PathSvg {path:"M0 0H40V40H0Z"}}}
113
+ }`,
114
+ async (h) => {
115
+ const pixels = JSON.parse(
116
+ await h.call(
117
+ 'evaluate',
118
+ `(()=>{const c=document.querySelector('canvas[data-qml-gradient]').getContext('2d');return JSON.stringify([Array.from(c.getImageData(5,20,1,1).data),Array.from(c.getImageData(95,20,1,1).data)]);})()`,
119
+ ),
120
+ );
121
+ expect(pixels[0][0]).toBeGreaterThan(220);
122
+ expect(pixels[0][2]).toBeLessThan(35);
123
+ expect(pixels[1][2]).toBeGreaterThan(220);
124
+ expect(
125
+ await h.call(
126
+ 'evaluate',
127
+ 'document.querySelectorAll("svg path").length',
128
+ ),
129
+ ).toBe(2);
130
+ },
131
+ );
132
+ }, 30000);
133
+
134
+ test('Text reports laid-out lineCount and keeps wrapped or explicit multiline captions when eliding', async () => {
135
+ await fixture(
136
+ `import QtQuick
137
+ Item {width:400;height:240
138
+ Text {id:empty;text:""}
139
+ Text {id:single;y:40;text:"one"}
140
+ Text {id:multi;y:80;text:"one\\ntwo"}
141
+ Text {id:clipped;y:140;text:"one\\ntwo\\nthree";maximumLineCount:2;elide:Text.ElideRight}
142
+ Text {id:wrapped;x:100;width:70;text:"one two three four five";wrapMode:Text.WordWrap;elide:Text.ElideRight}
143
+ Text {id:status;y:210;text:empty.implicitWidth+":"+empty.lineCount+":"+single.lineCount+":"+multi.lineCount+":"+clipped.lineCount+":"+clipped.truncated+":"+wrapped.truncated}
144
+ }`,
145
+ async (h) => {
146
+ expect(
147
+ await h.call(
148
+ 'evaluate',
149
+ 'document.getElementById("status").textContent',
150
+ ),
151
+ ).toBe('0:1:1:2:2:true:false');
152
+ expect(
153
+ await h.call(
154
+ 'evaluate',
155
+ 'document.getElementById("clipped").style.whiteSpace',
156
+ ),
157
+ ).toBe('pre');
158
+ expect(
159
+ await h.call(
160
+ 'evaluate',
161
+ 'document.getElementById("wrapped").style.whiteSpace',
162
+ ),
163
+ ).toBe('pre-wrap');
164
+ },
165
+ );
166
+ }, 30000);
167
+
168
+ test('Text defaults to AutoText, recognizes multi-letter tags and preserves plain trailing newlines', async () => {
169
+ await fixture(
170
+ `import QtQuick
171
+ Item {width:300;height:200
172
+ Text {id:auto;text:"<strong>Bold</strong><br/>next"}
173
+ Text {id:plain;y:60;text:"line\\n"}
174
+ Text {id:status;y:120;text:plain.lineCount}
175
+ }`,
176
+ async (h) => {
177
+ expect(
178
+ await h.call(
179
+ 'evaluate',
180
+ 'document.getElementById("auto").querySelector("strong").textContent',
181
+ ),
182
+ ).toBe('Bold');
183
+ expect(
184
+ await h.call('evaluate', 'document.getElementById("auto").textContent'),
185
+ ).toBe('Boldnext');
186
+ expect(
187
+ await h.call(
188
+ 'evaluate',
189
+ 'document.getElementById("status").textContent',
190
+ ),
191
+ ).toBe('2');
192
+ },
193
+ );
194
+ }, 30000);
195
+
196
+ test('markup formatting newlines after blocks do not add phantom text lines',async()=>{
197
+ await fixture(`import QtQuick
198
+ Item {width:400;height:200
199
+ Text {id:rich;width:300;text:"<p>one</p>\\n<p>two</p>\\n";textFormat:Text.RichText}
200
+ Text {id:plain;y:80;text:"one\\ntwo\\n";textFormat:Text.PlainText}
201
+ Text {id:status;y:150;text:rich.lineCount+":"+plain.lineCount}
202
+ }`,async host=>{
203
+ expect(await host.call('evaluate','document.getElementById("status").textContent')).toBe('2:3');
204
+ });
205
+ });
@@ -0,0 +1,45 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {createQt} from '../lib/qml/builtins.js';
3
+
4
+ test('C/POSIX locale is metric and uses Qt date order and 24-hour time',()=>{
5
+ for(const name of ['C','C.UTF-8','POSIX']) {
6
+ const qt=createQt({LC_ALL:name,LANG:'en_US.UTF-8',LC_TIME:'en_US.UTF-8',LC_MEASUREMENT:'en_US.UTF-8'});
7
+ const l=qt.locale();
8
+ expect(l.name).toBe('C');expect(l.measurementSystem).toBe(0);
9
+ expect(l.dateFormat(1)).toBe('d MM yyyy');expect(l.timeFormat(1)).toBe('HH:mm');
10
+ expect(qt.locale('en_US.UTF-8').measurementSystem).toBe(1);
11
+ }
12
+ });
13
+ test('locale categories override LANG but explicit locale ignores environment',()=>{
14
+ const qt=createQt({LANG:'en_US.UTF-8',LC_TIME:'en_GB.UTF-8',LC_MEASUREMENT:'zh_TW.UTF-8'});
15
+ expect(qt.locale().name).toBe('en_US');expect(qt.locale().measurementSystem).toBe(0);
16
+ expect(qt.locale().timeFormat(1)).not.toContain('AP');
17
+ expect(qt.locale().dateFormat(1).indexOf('d')).toBeLessThan(qt.locale().dateFormat(1).indexOf('M'));
18
+ expect(qt.locale('en_US').timeFormat(1)).toContain('AP');
19
+ expect(qt.locale('en_GB').measurementSystem).toBe(2);
20
+ expect(qt.locale('C').timeFormat(1)).toBe('HH:mm');
21
+ expect(qt.locale('not_a_valid_locale').name).toBe('C');
22
+ });
23
+ test('empty overrides inherit LANG; unset system defaults to C',()=>{
24
+ const qt=createQt({LC_ALL:'',LC_TIME:'',LANG:'zh_TW.UTF-8'});
25
+ expect(qt.locale().name).toBe('zh_TW');expect(qt.locale().measurementSystem).toBe(0);
26
+ expect(qt.locale().dateFormat(1).indexOf('y')).toBeLessThan(qt.locale().dateFormat(1).indexOf('M'));
27
+ expect(createQt({}).locale().name).toBe('C');
28
+ const d=new Date(2024,10,23,17,45,36);
29
+ expect(createQt({LANG:'C.UTF-8'}).formatTime(d,'HH:mm')).toBe('17:45');
30
+ expect(createQt({LANG:'C.UTF-8'}).formatDate(d,'MMMM')).toBe('November');
31
+ });
32
+
33
+ test('local QML host receives locale categories through its host capability',async()=>{
34
+ for(const [env,expected] of [
35
+ [{LC_ALL:'C.UTF-8',LANG:'en_US.UTF-8'},{name:'C',measurement:0,date:'d MM yyyy',time:'HH:mm'}],
36
+ [{LC_ALL:'',LANG:'en_US.UTF-8',LC_MESSAGES:'',LC_TIME:'en_GB.UTF-8',LC_MEASUREMENT:'zh_TW.UTF-8'},null],
37
+ ]) {
38
+ const child=Bun.spawn([process.execPath,new URL('./fixtures/qml-locale/check.mjs',import.meta.url).pathname],{env:{...process.env,...env},stdout:'pipe',stderr:'pipe'});
39
+ const [output,error,code]=await Promise.all([new Response(child.stdout).text(),new Response(child.stderr).text(),child.exited]);
40
+ expect({code,error}).toEqual({code:0,error:''});
41
+ const result=JSON.parse(output.trim());
42
+ if(expected)expect(result).toEqual(expected);
43
+ else {expect(result.name).toBe('en_US');expect(result.measurement).toBe(0);expect(result.time).toBe('HH:mm');}
44
+ }
45
+ },15000);
@@ -0,0 +1,63 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {mkdtempSync,rmSync} from 'node:fs';
3
+ import {join} from 'node:path';
4
+ import {createBrowser} from '../lib/headless-shell.js';
5
+ import {applyQmlMediaGain} from '../lib/qml/media-renderer.js';
6
+ test('QML MP4/MP3 reuse HTML decoding, metadata, pause and millisecond seek',async()=>{
7
+ const dir=mkdtempSync('/tmp/qml-media-');let browser;
8
+ try {
9
+ const video=Bun.pathToFileURL(join(import.meta.dir,'fixtures/long-video.mp4')).href;
10
+ const audio=Bun.pathToFileURL(join(import.meta.dir,'fixtures/tone.mp3')).href;
11
+ await Bun.write(join(dir,'main.qml'),`import QtQuick
12
+ import QtMultimedia
13
+ import Quickshell.Wayland
14
+ Rectangle {width:320;height:240;color:"black"
15
+ AudioOutput {id:out;volume:0;muted:true}
16
+ VideoOutput {id:screen;anchors.fill:parent}
17
+ MediaPlayer {id:player;source:${JSON.stringify(video)};audioOutput:out;videoOutput:screen}
18
+ IdleInhibitor {enabled:true}
19
+ Text {id:status;text:player.mediaStatus+":"+player.duration+":"+player.position+":"+player.error}
20
+ Button {id:play;onClicked:player.play()}
21
+ Button {id:pause;onClicked:player.pause()}
22
+ Button {id:seek;onClicked:player.position=500}
23
+ Button {id:switchAudio;onClicked:player.source=${JSON.stringify(audio)}}
24
+ }`);
25
+ browser=await createBrowser({spareRenderer:false});
26
+ await browser.context.addInitScript(`globalThis.mediaNodes=[];let rt;Object.defineProperty(globalThis,'__qmlRuntime',{get:()=>rt,set:r=>{rt=r;const d=r.describe;r.describe=(v,t,...a)=>{if(t.includes('MediaPlayer')||t.includes('VideoOutput'))mediaNodes.push({v,t});return d(v,t,...a)}}});`);
27
+ await browser.context.navigate(Bun.pathToFileURL(join(dir,'main.qml')).href);
28
+ const run=s=>browser.context.evaluate(s);
29
+
30
+ const status=()=>run("(()=>{const p=mediaNodes.find(n=>n.t.includes('MediaPlayer')).v;return {duration:p.duration,position:p.position,error:p.error,message:p.errorString,state:p.playbackState,ready:p.mediaStatus,hasVideo:p.hasVideo,hasAudio:p.hasAudio}})()");
31
+ async function until(fn){for(let i=0;i<200;i++){const s=await status();if(s.error)throw Error(JSON.stringify(s));if(fn(s))return s;await Bun.sleep(25)}throw Error(JSON.stringify(await status()));}
32
+ expect((await until(s=>s.duration>1000)).hasVideo).toBe(true);
33
+ await run("document.getElementById('play').click()");
34
+ await until(s=>s.state===1&&s.position>100);
35
+ await run("document.getElementById('pause').click()");
36
+ expect((await status()).state).toBe(2);
37
+ await run("document.getElementById('seek').click()");
38
+ await until(s=>Math.abs(s.position-500)<50);
39
+ const png=await browser.context.screenshot({format:'png'});
40
+ await Bun.write('/tmp/qml-media-video.png',Buffer.from(png,'base64'));
41
+ expect(await run("mediaNodes.find(n=>n.t.includes('VideoOutput')).v.contentRect.width")).toBeGreaterThan(0);
42
+ await run("document.getElementById('switchAudio').click()");
43
+ const s=await until(s=>s.hasAudio&&!s.hasVideo&&s.duration>500);
44
+ expect(s.error).toBe(0);
45
+ await run("document.getElementById('seek').click()");
46
+ await until(s=>Math.abs(s.position-500)<50);
47
+ }finally{browser?.close();rmSync(dir,{recursive:true,force:true})}
48
+ },30000);
49
+
50
+ test('QML PCM gain scales the output chunk without requiring a new audio backend',()=>{
51
+ const pcm=Buffer.alloc(6);pcm.writeInt16LE(-2000,0);pcm.writeInt16LE(1000,2);pcm.writeInt16LE(32767,4);
52
+ applyQmlMediaGain(pcm,0.5);
53
+ expect([pcm.readInt16LE(0),pcm.readInt16LE(2),pcm.readInt16LE(4)]).toEqual([-1000,500,16384]);
54
+ applyQmlMediaGain(pcm,0);expect([...pcm]).toEqual([0,0,0,0,0,0]);
55
+ });
56
+
57
+
58
+ test('QML MP3 playback sends real PCM, mutes and releases the shared audio output',async()=>{
59
+ const child=Bun.spawn([process.execPath,new URL('./fixtures/qml-media-audio.js',import.meta.url).pathname],{stdout:'pipe',stderr:'pipe'});
60
+ const [out,error,code]=await Promise.all([new Response(child.stdout).text(),new Response(child.stderr).text(),child.exited]);
61
+ expect({code,error}).toEqual({code:0,error:''});
62
+ expect(out).toContain('QML real MP3 PCM playback passed');
63
+ },30000);
@@ -0,0 +1,19 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {mkdtempSync,rmSync} from 'node:fs';
3
+ import {tmpdir} from 'node:os';
4
+ import {join} from 'node:path';
5
+ import {createBrowser} from '../lib/headless-shell.js';
6
+
7
+ test('dynamic QtObject readonly visible does not abort singleton initialization',async()=>{
8
+ const dir=mkdtempSync(join(tmpdir(),'qml-nonvisual-'));let browser;
9
+ try{
10
+ await Bun.write(join(dir,'qmldir'),'singleton Sound 1.0 Sound.qml\n');
11
+ await Bun.write(join(dir,'Sound.qml'),'pragma Singleton\nimport QtQuick\nQtObject { property int held: -1; readonly property real gain: held < 0 ? 1 : 0 }');
12
+ await Bun.write(join(dir,'Panel.qml'),'import QtQuick\nimport "."\nItem { QtObject { id:meta; readonly property bool visible: true; readonly property real gain: Sound.gain } Text { text: "gain="+meta.gain+" visible="+meta.visible } }');
13
+ await Bun.write(join(dir,'main.qml'),'import QtQuick\nItem { width:200;height:80; Loader { id:loader;source:"Panel.qml" } }');
14
+ browser=await createBrowser({spareRenderer:false});
15
+ await browser.context.navigate(Bun.pathToFileURL(join(dir,'main.qml')).href);
16
+ expect(await browser.context.evaluate('Array.from(document.querySelectorAll("[data-qml-type=Text]")).map(e=>e.textContent).join(" ")')).toContain('gain=1 visible=true');
17
+ expect(await browser.context.evaluate('Array.from(document.querySelectorAll("[data-qml-type=QtObject]")).every(e=>e.style.display==="none")')).toBe(true);
18
+ }finally{browser?.close();rmSync(dir,{recursive:true,force:true});}
19
+ },30000);
@@ -0,0 +1,45 @@
1
+ import { test, expect } from 'bun:test';
2
+ import { RendererHost } from '../lib/renderer/host.js';
3
+ test('created delegates expose persisted state before completion and are adopted without rebuilding', async () => {
4
+ const host = new RendererHost({ timeout: 20000 });
5
+ try {
6
+ await host.start();
7
+ await host.call('loadDocument', {
8
+ url: 'data:text/x-qml,persisted',
9
+ contentType: 'text/x-qml',
10
+ source: `
11
+ import QtQuick
12
+ import QtQml.Models
13
+ Item { id:root; width:300;height:200
14
+ property int built:0; property int gone:0; property var held:null; property string observed:""
15
+ ListView {id:view;width:200;height:100;reuseItems:false
16
+ model:DelegateModel {id:rows;model:100;filterOnGroup:"shown";groups:DelegateModelGroup {id:shown;name:"shown";includeByDefault:false}}
17
+ delegate:Text {id:cell;required property int index; text:"row "+index;width:200;height:20
18
+ Component.onCompleted:{root.built++;root.observed=String(cell.DelegateModel.inPersistedItems)}
19
+ Component.onDestruction:root.gone++ }
20
+ }
21
+ Button {id:create;onClicked:root.held=rows.items.create(0)}
22
+ Button {id:adopt;onClicked:{rows.items.addGroups(0,1,"shown");view.forceLayout();root.held.DelegateModel.inPersistedItems=false}}
23
+ Button {id:shrink;onClicked:{shown.removeGroups(0,1,"shown");view.forceLayout()}}
24
+ Text {id:status;y:140;text:[root.built,root.gone,rows.persistedItems.count,root.observed,view.count].join(":")}
25
+ }`,
26
+ });
27
+ await host.call('whenLoaded');
28
+ const click = async (id) => {
29
+ await host.call('evaluate', `document.getElementById('${id}').click()`);
30
+ await host.call('evaluate', '0');
31
+ };
32
+ const status = () =>
33
+ host.call('evaluate', 'document.getElementById("status").textContent');
34
+ await click('create');
35
+ expect(await status()).toBe('1:0:1:true:0');
36
+ await click('create');
37
+ expect(await status()).toBe('1:0:1:true:0');
38
+ await click('adopt');
39
+ expect(await status()).toBe('1:0:0:true:1');
40
+ await click('shrink');
41
+ expect(await status()).toBe('1:1:0:true:0');
42
+ } finally {
43
+ host.close();
44
+ }
45
+ }, 30000);
@@ -0,0 +1,65 @@
1
+ // Opt-in comparison against the genuine binary; no Quickshell implementation source.
2
+ import { test, expect } from 'bun:test';
3
+ import { withQuickshellFixture } from './helpers/quickshell-fixture.js';
4
+ test.skipIf(!process.env.BUNINU_QS_ROOT)(
5
+ 'real Quickshell DialogButton activates by click, Enter and Space; disabled stays inert',
6
+ async () => {
7
+ await withQuickshellFixture(
8
+ 'button.qml',
9
+ async ({ land, title, until, click, key }) => {
10
+ await until(() => title() === 'Flea buttons | 0');
11
+ await Bun.sleep(300);
12
+ await click(50, 36);
13
+ await until(() => title() === 'Flea buttons | 1');
14
+ await key('Enter', 'Enter');
15
+ await until(() => title() === 'Flea buttons | 2');
16
+ await key(' ', 'Space');
17
+ await until(() => title() === 'Flea buttons | 3');
18
+ await click(50, 86);
19
+ expect(title()).toBe('Flea buttons | 3');
20
+ await Bun.write(
21
+ '/tmp/quickshell-flea-buttons.png',
22
+ Buffer.from(
23
+ await land.context.screenshot({ format: 'png' }),
24
+ 'base64',
25
+ ),
26
+ );
27
+ },
28
+ );
29
+ },
30
+ 30000,
31
+ );
32
+
33
+ test.skipIf(!process.env.BUNINU_QS_ROOT)(
34
+ 'real Quickshell DialogField forwards edits, Enter, Tab and Shift-Tab',
35
+ async () => {
36
+ await withQuickshellFixture('field.qml', async ({ title, until, key }) => {
37
+ await until(() => title() === 'Flea field | first | 0:0:0');
38
+ await key('a', 'KeyA', 2);
39
+ await key('h', 'KeyH');
40
+ await until(() => title() === 'Flea field | h | 1:0:0');
41
+ await key('Enter', 'Enter');
42
+ await key('Tab', 'Tab');
43
+ await until(() => title() === 'Flea field | h | 1:1:1');
44
+ await key('Tab', 'Tab', 8);
45
+ await until(() => title() === 'Flea field | h | 1:1:0');
46
+ });
47
+ },
48
+ 30000,
49
+ );
50
+
51
+ test.skipIf(!process.env.BUNINU_QS_ROOT)(
52
+ 'real Quickshell reports empty and clipped multiline Text counts',
53
+ async () => {
54
+ await withQuickshellFixture(
55
+ 'text-metrics.qml',
56
+ async ({ title, until }) => {
57
+ await until(() => title()?.startsWith('Text metrics | 0:'));
58
+ const fields = title().split(' | ')[1].split(':');
59
+ expect(Number(fields[1])).toBeGreaterThan(0);
60
+ expect(fields.slice(2).join(':')).toBe('1:1:2:2:true');
61
+ },
62
+ );
63
+ },
64
+ 30000,
65
+ );
@@ -0,0 +1,62 @@
1
+ // Opt-in binary comparison. No Quickshell source or dependency installation.
2
+ // BUNINU_QS_ROOT=/path/to/extracted/sid/root bun test test/qml-quickshell-settings.test.js
3
+ import {test,expect} from 'bun:test';
4
+ import {mkdtempSync,cpSync,readFileSync,writeFileSync,readdirSync,existsSync,mkdirSync,rmSync} from 'node:fs';
5
+ import {join} from 'node:path';
6
+ const qsRoot=process.env.BUNINU_QS_ROOT;
7
+ test.skipIf(!qsRoot)('real Quickshell SettingsRuler matches size, disabled tap and reset behavior',async()=>{
8
+ const {start}=await import('../../buninu-land/compositor.js');
9
+ const dir=mkdtempSync('/tmp/qs-settings-test-'),fixture=join(dir,'fixture'),home=join(dir,'home');
10
+ const oldRuntime=process.env.XDG_RUNTIME_DIR;
11
+ let land,child;
12
+ try{
13
+ cpSync(Bun.fileURLToPath(new URL('./fixtures/qml/flea-theme/',import.meta.url)),fixture,{recursive:true});
14
+ // Qt validates all declared types, including ones not used by this subset.
15
+ // Trim only the temporary module indexes; all actual QML/JS stays unchanged.
16
+ const trim=path=>{
17
+ for(const e of readdirSync(path,{withFileTypes:true})){
18
+ const p=join(path,e.name);
19
+ if(e.isDirectory())trim(p);
20
+ else if(e.name==='qmldir')writeFileSync(p,readFileSync(p,'utf8').split('\n').filter(line=>{
21
+ const last=line.trim().split(/\s+/).at(-1);
22
+ return !last.endsWith('.qml')||existsSync(join(path,last));
23
+ }).join('\n'));
24
+ }
25
+ };trim(fixture);
26
+ const theme=join(home,'.local/state/omarchy/current/theme');mkdirSync(theme,{recursive:true});
27
+ writeFileSync(join(theme,'shell.toml'),'[font]\nbase-size = 14\n');
28
+ writeFileSync(join(theme,'colors.toml'),'background="#101315"\nforeground="#eeeeee"\naccent="#70e1e8"\n');
29
+ writeFileSync(join(theme,'../theme.name'),'demo');
30
+ process.env.XDG_RUNTIME_DIR=dir;
31
+ land=await start(['--remote-debugging-port=0','--window-size=1000,700']);
32
+ const lib=join(qsRoot,'usr/lib/aarch64-linux-gnu');
33
+ child=Bun.spawn([join(lib,'ld-linux-aarch64.so.1'),'--library-path',lib+':'+join(qsRoot,'usr/lib'),join(qsRoot,'usr/bin/qs'),'-p',join(fixture,'settings.qml')],{
34
+ env:{...process.env,HOME:home,HYPRLAND_INSTANCE_SIGNATURE:'',WAYLAND_DISPLAY:'wayland-buninu',QT_PLUGIN_PATH:join(lib,'qt6/plugins'),QML_IMPORT_PATH:join(fixture,'imports')+':'+join(lib,'qt6/qml'),QT_QPA_PLATFORM:'wayland',QT_WAYLAND_DISABLE_WINDOWDECORATION:'1',QT_QUICK_BACKEND:'software'},
35
+ stdout:'ignore',stderr:'pipe'
36
+ });
37
+ const logs=new Response(child.stderr).text();
38
+ const title=()=>land.compositor.wm.visible[0]?.surface.xdg.toplevel.title;
39
+ const until=async fn=>{for(let i=0;i<300;i++){if(await fn())return;if(child.exitCode!==null)throw Error(await logs);await Bun.sleep(50);}throw Error('Quickshell state timed out: '+title());};
40
+ await until(()=>title()==='Flea — text size settings | 14 | 0');
41
+ const click=async(x,y)=>{
42
+ const surface=land.compositor.wm.renderSurfaces().find(s=>s.pixels);
43
+ const o=land.compositor.wm.origin(surface);
44
+ for(const type of ['mouseMoved','mousePressed','mouseReleased'])land.context.dispatchMouseEvent({type,x:o.x+x,y:o.y+y,button:'left'});
45
+ await Bun.sleep(120);
46
+ };
47
+ await Bun.sleep(500);
48
+ await Bun.write('/tmp/qs-settings-before.png',Buffer.from(await land.context.screenshot({format:'png'}),'base64'));
49
+ await click(712,100);await until(()=>title()==='Flea — text size settings | 20 | 1');
50
+ await click(100,300);await click(76,100);
51
+ expect(title()).toBe('Flea — text size settings | 20 | 1');
52
+ await click(100,300);await click(76,100);await until(()=>title()==='Flea — text size settings | 9 | 2');
53
+ await click(380,300);await until(()=>title()==='Flea — text size settings | 14 | 2');
54
+ await Bun.write('/tmp/quickshell-flea-settings.png',Buffer.from(await land.context.screenshot({format:'png'}),'base64'));
55
+ expect(title()).toBe('Flea — text size settings | 14 | 2');
56
+ }finally{
57
+ if(child){child.kill();await child.exited;}
58
+ land?.close();
59
+ if(oldRuntime===undefined)delete process.env.XDG_RUNTIME_DIR;else process.env.XDG_RUNTIME_DIR=oldRuntime;
60
+ rmSync(dir,{recursive:true,force:true});
61
+ }
62
+ },30000);
@@ -0,0 +1,72 @@
1
+ import {test, expect} from 'bun:test';
2
+ import {RendererHost} from '../lib/renderer/host.js';
3
+
4
+ test('Repeater scopes delegates, updates models, preserves sibling order and disposes old handlers', async () => {
5
+ const host=new RendererHost({timeout:20000});
6
+ try {
7
+ await host.start();
8
+ await host.call('loadDocument',{url:'https://qml.test/repeater.qml',contentType:'text/x-qml',source:`
9
+ import QtQuick
10
+ Rectangle {
11
+ id: root; width: 600; height: 300
12
+ property var items: [10,20,30]
13
+ property int clicks: 0
14
+ property int factor: 2
15
+ Text {id: before; text: "before"}
16
+ Repeater {
17
+ id: list; model: root.items
18
+ delegate: Rectangle {
19
+ id: cell
20
+ required property var modelData
21
+ required property int index
22
+ x: index*100; y: 50; width: 90; height: 40
23
+ Text {text: cell.index+":"+cell.modelData*root.factor; anchors.horizontalCenter: parent.horizontalCenter}
24
+ TapHandler {gesturePolicy: TapHandler.ReleaseWithinBounds; onTapped: root.clicks++}
25
+ }
26
+ }
27
+ Text {id: after; text: "after"}
28
+ Text {id: status; text: root.clicks+":"+list.count}
29
+ Button {id: replace; onClicked: {root.items=[7,8];root.factor=3}}
30
+ Button {id: clear; onClicked: root.items=[]}
31
+ }`});
32
+ await host.call('whenLoaded');
33
+ const texts=()=>host.call('evaluate','JSON.stringify([...document.querySelectorAll("[data-qml-type=Text]")].map(e=>e.textContent))');
34
+ expect(JSON.parse(await texts())).toEqual(['before','0:20','1:40','2:60','after','0:3']);
35
+ await host.call('evaluate',`globalThis.oldCell=document.querySelector('[data-qml-type=Repeater]').previousElementSibling;oldCell.click()`);
36
+ expect(await host.call('evaluate','document.getElementById("status").textContent')).toBe('1:3');
37
+ await host.call('evaluate','document.getElementById("replace").click()');
38
+ expect(JSON.parse(await texts())).toEqual(['before','0:21','1:24','after','1:2']);
39
+ await host.call('evaluate','oldCell.click()');
40
+ expect(await host.call('evaluate','document.getElementById("status").textContent')).toBe('1:2');
41
+ await host.call('evaluate','document.getElementById("clear").click()');
42
+ expect(JSON.parse(await texts())).toEqual(['before','after','1:0']);
43
+ } finally {host.close();}
44
+ },30000);
45
+
46
+ test('nested numeric Repeaters keep instance IDs local and allow later outer IDs', async () => {
47
+ const host=new RendererHost({timeout:20000});
48
+ try {
49
+ await host.start();
50
+ await host.call('loadDocument',{url:'https://qml.test/nested.qml',contentType:'text/x-qml',source:`
51
+ import QtQuick
52
+ Item {
53
+ Repeater {
54
+ model: 2
55
+ delegate: Item {
56
+ id: row
57
+ required property int index
58
+ Repeater {
59
+ model: 2
60
+ delegate: Text {
61
+ required property int index
62
+ text: suffix.text+":"+row.index+":"+index
63
+ }
64
+ }
65
+ }
66
+ }
67
+ Text {id: suffix; text: "item"}
68
+ }`});
69
+ await host.call('whenLoaded');
70
+ expect(JSON.parse(await host.call('evaluate','JSON.stringify([...document.querySelectorAll("[data-qml-type=Text]")].map(e=>e.textContent))'))).toEqual(['item:0:0','item:0:1','item:1:0','item:1:1','item']);
71
+ }finally{host.close();}
72
+ },30000);
@@ -0,0 +1,6 @@
1
+ import {test,expect} from 'bun:test';
2
+ for(const mode of ['shader','grab','texture'])test('local QML software canvas: '+mode,async()=>{
3
+ const child=Bun.spawn([process.execPath,new URL('./fixtures/qml-shader-check.mjs',import.meta.url).pathname,mode],{stdout:'pipe',stderr:'pipe'});
4
+ const [exit,stdout,stderr]=await Promise.all([child.exited,new Response(child.stdout).text(),new Response(child.stderr).text()]);
5
+ expect({exit,stderr}).toEqual({exit:0,stderr:''});
6
+ },30000);
@@ -0,0 +1,39 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {RendererHost} from '../lib/renderer/host.js';
3
+ test('Instantiator owns Shortcut delegates and deactivation removes keyboard handlers',async()=>{
4
+ const host=new RendererHost({timeout:20000});
5
+ try{
6
+ await host.start();
7
+ await host.call('loadDocument',{url:'data:text/x-qml,shortcuts',contentType:'text/x-qml',source:`import QtQuick
8
+ import QtQml.Models
9
+ Item {
10
+ property int hits:0
11
+ Instantiator {id:inst;model:["Ctrl+L", "Shift+N", "?", "Ctrl+K, Ctrl+C"]
12
+ delegate:Shortcut {autoRepeat:false;sequence:modelData;onActivated:globalThis.pressed.push(index)}
13
+ }
14
+ Shortcut {sequence:"X";onActivated:globalThis.bad=true;onActivatedAmbiguously:globalThis.amb=(globalThis.amb||0)+1}
15
+ Shortcut {sequence:"X";onActivated:globalThis.bad=true;onActivatedAmbiguously:globalThis.amb=(globalThis.amb||0)+1}
16
+ Shortcut {id:probe;enabled:false;sequence:"ctrl+l"}
17
+ Component.onCompleted:{globalThis.pressed=[];globalThis.inst=inst;globalThis.probe=probe;}
18
+ }`});
19
+ await host.call('whenLoaded');
20
+ const key=async(key,mods={})=>host.call('evaluate',`document.body.dispatchEvent(new KeyboardEvent('keydown',{key:${JSON.stringify(key)},bubbles:true,cancelable:true,...${JSON.stringify(mods)}}))`);
21
+ expect(await host.call('evaluate','inst.count')).toBe(4);
22
+ expect(await host.call('evaluate','probe.portableText')).toBe('Ctrl+L');
23
+ expect(await host.call('evaluate','inst.objectAt(0).parent===inst')).toBe(true);
24
+ await key('l',{ctrlKey:true,repeat:true});
25
+ expect(await host.call('evaluate','pressed')).toEqual([]);
26
+ await key('x');
27
+ expect(await host.call('evaluate','[globalThis.amb,!!globalThis.bad]')).toEqual([2,false]);
28
+ await key('l',{ctrlKey:true});await key('N',{shiftKey:true});await key('?',{shiftKey:true});
29
+ await key('k',{ctrlKey:true});await key('c',{ctrlKey:true});
30
+ expect(await host.call('evaluate','pressed')).toEqual([0,1,2,3]);
31
+ await host.call('evaluate','inst.active=false');
32
+ await Bun.sleep(20);await key('l',{ctrlKey:true});
33
+ expect(await host.call('evaluate','inst.count')).toBe(0);
34
+ expect(await host.call('evaluate','inst.object')).toBe(null);
35
+ expect(await host.call('evaluate','pressed')).toEqual([0,1,2,3]);
36
+ await host.call('evaluate','inst.active=true');await Bun.sleep(20);await key('l',{ctrlKey:true});
37
+ expect(await host.call('evaluate','pressed')).toEqual([0,1,2,3,0]);
38
+ }finally{host.close();}
39
+ },20000);