buninu-browser 0.2.4 → 0.4.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 (249) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +102 -2
  3. package/frontends/wayland/LICENSE-land +21 -0
  4. package/frontends/wayland/README.md +123 -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 +116 -0
  20. package/frontends/wayland/input.js +56 -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 +158 -0
  26. package/index.js +28 -1
  27. package/lib/clipboard-command.js +10 -0
  28. package/lib/clipboard-powershell.js +22 -0
  29. package/lib/clipboard.js +57 -0
  30. package/lib/document-types.js +11 -0
  31. package/lib/headless-shell.js +78 -13
  32. package/lib/layout/text-layout.js +17 -9
  33. package/lib/paint/display-list.js +5 -1
  34. package/lib/paint/frame.js +35 -3
  35. package/lib/paint/raw-frames.js +47 -0
  36. package/lib/qml/README.md +971 -0
  37. package/lib/qml/builtins.js +700 -0
  38. package/lib/qml/canvas-dom.js +115 -0
  39. package/lib/qml/document.js +1176 -0
  40. package/lib/qml/format.js +9 -0
  41. package/lib/qml/host.js +324 -0
  42. package/lib/qml/images.js +199 -0
  43. package/lib/qml/launcher.js +132 -0
  44. package/lib/qml/loader.js +517 -0
  45. package/lib/qml/media-renderer.js +49 -0
  46. package/lib/qml/media.js +86 -0
  47. package/lib/qml/models.js +622 -0
  48. package/lib/qml/parser.js +381 -0
  49. package/lib/qml/qs +5 -0
  50. package/lib/qml/qscasty +3 -0
  51. package/lib/qml/qsheadless +3 -0
  52. package/lib/qml/qswayland +3 -0
  53. package/lib/qml/reactive.js +159 -0
  54. package/lib/qml/runtime.js +3381 -0
  55. package/lib/qml/shapes.js +243 -0
  56. package/lib/qml/text.js +109 -0
  57. package/lib/qml/textures.js +123 -0
  58. package/lib/qml/wayland-clipboard.js +32 -0
  59. package/lib/qml/worker-thread.js +70 -0
  60. package/lib/render-tree/index.js +16 -1
  61. package/lib/renderer/bindings.js +11 -0
  62. package/lib/renderer/canvas-2d.js +13 -0
  63. package/lib/renderer/compositor-channel.js +103 -0
  64. package/lib/renderer/compositor.js +25 -33
  65. package/lib/renderer/flags.js +1 -1
  66. package/lib/renderer/host.js +128 -92
  67. package/lib/renderer/main.js +38 -4
  68. package/lib/renderer/page-realm.js +32 -1
  69. package/lib/renderer/page-renderer.js +250 -21
  70. package/lib/renderer/text-input-paint.js +54 -0
  71. package/lib/renderer/text-input.js +57 -0
  72. package/lib/style/computed-style.js +2 -2
  73. package/package.json +1 -1
  74. package/test/clipboard.test.js +54 -0
  75. package/test/fixtures/browser-raw-demo.html +36 -0
  76. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  77. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  78. package/test/fixtures/flea-helpers/bin/bat +3 -0
  79. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  80. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  81. package/test/fixtures/flea-helpers/bin/glow +3 -0
  82. package/test/fixtures/flea-helpers/bin/qs +3 -0
  83. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  84. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  85. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  86. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  87. package/test/fixtures/flea-helpers/check.mjs +36 -0
  88. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  89. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  90. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  91. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  92. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  93. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  94. package/test/fixtures/qml/flea/LICENSE +21 -0
  95. package/test/fixtures/qml/flea/README.md +25 -0
  96. package/test/fixtures/qml/flea/Theme.qml +8 -0
  97. package/test/fixtures/qml/flea/demo.qml +24 -0
  98. package/test/fixtures/qml/flea/qmldir +3 -0
  99. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  100. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  101. package/test/fixtures/qml/flea-theme/README.md +70 -0
  102. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  103. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  104. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  105. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  106. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  107. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  108. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  109. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  110. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  111. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  112. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  113. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  114. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  115. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  116. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  117. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  118. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  119. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  120. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  121. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  122. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  123. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  124. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  125. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  126. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  127. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  128. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  129. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  130. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  131. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  132. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  133. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  134. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  135. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  136. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  137. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  138. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  139. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  140. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  141. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  142. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  143. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  144. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  145. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  146. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  147. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  148. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  167. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  168. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  169. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  170. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  171. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  172. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  173. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  174. package/test/fixtures/qml/hello.qml +26 -0
  175. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  176. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  177. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  178. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  179. package/test/fixtures/qml/imports/logic/base.js +2 -0
  180. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  181. package/test/fixtures/qml/imports/logic/math.js +3 -0
  182. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  183. package/test/fixtures/qml/imports/main.qml +21 -0
  184. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  185. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  186. package/test/fixtures/qml/multiply.qml +61 -0
  187. package/test/fixtures/qml-canvas-dom.js +90 -0
  188. package/test/fixtures/qml-media-audio.js +39 -0
  189. package/test/fixtures/raw-capture-benchmark.json +76 -0
  190. package/test/helpers/flea-sandbox.js +48 -0
  191. package/test/helpers/quickshell-fixture.js +125 -0
  192. package/test/html-clipboard-wayland.test.js +47 -0
  193. package/test/index.test.js +13 -0
  194. package/test/manual-casty-without-qml.js +87 -0
  195. package/test/manual-flea-application.js +49 -0
  196. package/test/manual-flea-binary.js +62 -0
  197. package/test/manual-flea-qml.js +9 -0
  198. package/test/manual-flea-settings.js +40 -0
  199. package/test/manual-flea-theme.js +40 -0
  200. package/test/manual-qml-removal.js +46 -0
  201. package/test/manual-without-qml.js +45 -0
  202. package/test/qml-canvas-dom.test.js +37 -0
  203. package/test/qml-clipboard-bridge.test.js +25 -0
  204. package/test/qml-clipboard-wayland.test.js +40 -0
  205. package/test/qml-connections-timing.test.js +73 -0
  206. package/test/qml-dynamic.test.js +401 -0
  207. package/test/qml-flea-app.test.js +146 -0
  208. package/test/qml-flea-backend.test.js +78 -0
  209. package/test/qml-flea-columns-backend.test.js +38 -0
  210. package/test/qml-flea-compile.test.js +78 -0
  211. package/test/qml-flea-launcher.test.js +88 -0
  212. package/test/qml-flea-markdown-backend.test.js +31 -0
  213. package/test/qml-flea-media.test.js +28 -0
  214. package/test/qml-flea-navigation-backend.test.js +35 -0
  215. package/test/qml-flea-navigation-timing.test.js +147 -0
  216. package/test/qml-flea-preview-backend.test.js +53 -0
  217. package/test/qml-flea-previews.test.js +163 -0
  218. package/test/qml-flea-rows.test.js +101 -0
  219. package/test/qml-flea-textures.test.js +59 -0
  220. package/test/qml-flea-view-switch.test.js +33 -0
  221. package/test/qml-flea-wayland.test.js +68 -0
  222. package/test/qml-flea.test.js +34 -0
  223. package/test/qml-host.test.js +238 -0
  224. package/test/qml-imports.test.js +102 -0
  225. package/test/qml-inherited-handlers.test.js +51 -0
  226. package/test/qml-input-selection.test.js +66 -0
  227. package/test/qml-interactions.test.js +309 -0
  228. package/test/qml-language.test.js +167 -0
  229. package/test/qml-launcher.test.js +54 -0
  230. package/test/qml-layout-text.test.js +205 -0
  231. package/test/qml-media.test.js +63 -0
  232. package/test/qml-persisted-delegates.test.js +45 -0
  233. package/test/qml-quickshell-controls.test.js +65 -0
  234. package/test/qml-quickshell-settings.test.js +62 -0
  235. package/test/qml-repeater.test.js +72 -0
  236. package/test/qml-style-updates.test.js +124 -0
  237. package/test/qml-textures.test.js +66 -0
  238. package/test/qml-views.test.js +332 -0
  239. package/test/qml-worker.test.js +69 -0
  240. package/test/qml.test.js +83 -0
  241. package/test/raw-capture.test.js +152 -0
  242. package/test/renderer-host.test.js +11 -0
  243. package/test/text-input-pointer.test.js +31 -0
  244. package/test/text-input-scroll.test.js +29 -0
  245. package/test/text-input.test.js +47 -0
  246. package/test/wayland-frontend.test.js +83 -0
  247. package/test/wayland-urlbar.test.js +38 -0
  248. package/test/window-resize.test.js +36 -0
  249. package/tools/bench-raw-capture.js +32 -0
@@ -0,0 +1,40 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {mkdtempSync,rmSync} from 'node:fs';
3
+ import {join} from 'node:path';
4
+ import {launch} from '../frontends/wayland/index.js';
5
+ test.skipIf(process.env.BUNINU_TEST_WAYLAND!=='1')('QML uses shared input defaults and exchanges text through Land on demand',async()=>{
6
+ const {start}=await import('../../buninu-land/compositor.js');
7
+ const dir=mkdtempSync('/tmp/qml-clipboard-'),old=process.env.XDG_RUNTIME_DIR;
8
+ let land,app;const writes=[];
9
+ const wait=async fn=>{for(let i=0;i<200;i++){if(await fn())return;await Bun.sleep(25);}throw Error('Clipboard did not settle');};
10
+ try {
11
+ process.env.XDG_RUNTIME_DIR=dir;
12
+ await Bun.write(join(dir,'main.qml'),`import QtQuick
13
+ Rectangle {width:400;height:150;color:"white"
14
+ TextInput {id:field;x:10;y:10;width:300;height:30;text:"abc中文"}
15
+ Button {id:all;x:10;y:70;onClicked:{field.forceActiveFocus();field.selectAll()}}
16
+ Button {id:copy;x:100;y:70;onClicked:field.copy()}
17
+ }`);
18
+ land=await start(['--remote-debugging-port=0','--window-size=640,480']);
19
+ land.compositor.clipboard.host={write:async text=>writes.push(text),read:async()=>{throw Error('Unexpected external read');},close(){}};
20
+ app=await launch({url:'about:blank',chrome:false,socket:join(dir,'wayland-buninu')});
21
+ await app.browser.context.navigate(Bun.pathToFileURL(join(dir,'main.qml')).href);
22
+ const run=s=>app.browser.context.evaluate(s);
23
+ await wait(()=>app.client.clipboard.serial>0);
24
+ await run(`document.getElementById('all').click()`);
25
+ const key=async(key,modifiers=0)=>{
26
+ for(const type of ['keyDown','keyUp'])land.context.dispatchKeyEvent({type,key,code:'Key'+key.toUpperCase(),modifiers});
27
+ await Bun.sleep(100);
28
+ };
29
+ await key('c',2);await wait(()=>writes.length>0);expect(writes.at(-1)).toBe('abc中文');
30
+ await key('ArrowRight');await key('v',2);
31
+ await wait(async()=>await run(`document.getElementById('field').value`)==='abc中文abc中文');
32
+ await key('a',2);await key('x',2);
33
+ await wait(async()=>await run(`document.getElementById('field').value`)==='');
34
+ expect(writes.at(-1)).toBe('abc中文abc中文');
35
+ await key('v',2);await wait(async()=>await run(`document.getElementById('field').value`)==='abc中文abc中文');
36
+ await run(`document.getElementById('all').click()`);
37
+ const before=writes.length;await run(`document.getElementById('copy').click()`);
38
+ await wait(()=>writes.length>before);expect(writes.at(-1)).toBe('abc中文abc中文');
39
+ }finally{app?.close();land?.close();if(old===undefined)delete process.env.XDG_RUNTIME_DIR;else process.env.XDG_RUNTIME_DIR=old;rmSync(dir,{recursive:true,force:true});}
40
+ },30000);
@@ -0,0 +1,73 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {RendererHost} from '../lib/renderer/host.js';
3
+
4
+ test('Connections catches first synchronous Loader request and follows same-handler target/enabled changes',async()=>{
5
+ const host=new RendererHost({timeout:20000});
6
+ try{
7
+ await host.start();
8
+ await host.call('loadDocument',{url:'data:text/x-qml,connections-timing',contentType:'text/x-qml',source:`import QtQuick
9
+ Rectangle {
10
+ id:root;width:300;height:200;property int received:0;property bool enabledLink:true;property int phase:0
11
+ Component {id:dialog;Item {signal requested();function open(){requested()}}}
12
+ Loader {id:loader;active:false;sourceComponent:dialog}
13
+ Connections {target:loader.item;enabled:root.enabledLink;function onRequested(){root.received++}}
14
+ Text {id:status;text:root.received}
15
+ onClicked:{
16
+ if(phase===0){loader.active=true;loader.item.open()}
17
+ else if(phase===1){root.enabledLink=false;loader.item.open()}
18
+ else if(phase===2){root.enabledLink=true;loader.item.open()}
19
+ else if(phase===3){loader.active=false;loader.active=true;loader.item.open()}
20
+ else loader.item.open()
21
+ phase++
22
+ }
23
+ }`});
24
+ await host.call('whenLoaded');
25
+ for(const count of [1,1,2,3,4]){
26
+ await host.call('evaluate',"document.getElementById('root').click()");
27
+ expect(await host.call('evaluate',"Number(document.getElementById('status').textContent)")).toBe(count);
28
+ }
29
+ }finally{host.close()}
30
+ },30000);
31
+
32
+ test('synchronous Loader reaches indirect item bindings before open returns',async()=>{
33
+ const host=new RendererHost({timeout:20000});
34
+ try{
35
+ await host.start();
36
+ await host.call('loadDocument',{url:'data:text/x-qml,loader-chain',contentType:'text/x-qml',source:`import QtQuick
37
+ Rectangle {
38
+ id:root;width:300;height:200;property int received:0
39
+ readonly property var quickLook:loader.item
40
+ property var preview:root.quickLook
41
+ Component {id:dialog;Item {signal requested();function open(){requested()}}}
42
+ Loader {id:loader;active:false;sourceComponent:dialog}
43
+ Connections {target:loader.item;function onRequested(){root.received++}}
44
+ Text {id:status;text:root.received}
45
+ onClicked:{loader.active=true;root.preview.open()}
46
+ }`});
47
+ await host.call('whenLoaded');
48
+ await host.call('evaluate',"document.getElementById('root').click()");
49
+ expect(await host.call('evaluate',"Number(document.getElementById('status').textContent)")).toBe(1);
50
+ }finally{host.close()}
51
+ },30000);
52
+
53
+ test('initial property handlers can call child ListView methods in a lazy component',async()=>{
54
+ const host=new RendererHost({timeout:20000});
55
+ try{
56
+ await host.start();
57
+ await host.call('loadDocument',{url:'data:text/x-qml,loader-child-methods',contentType:'text/x-qml',source:`import QtQuick
58
+ Rectangle {
59
+ id:root;width:300;height:200;property int result:-9
60
+ Component {id:preview;Item {
61
+ property bool wrap:true
62
+ onWrapChanged:root.result=list.indexAt(0,1)
63
+ ListView {id:list;width:100;height:100;model:2;delegate:Rectangle {height:20;width:100}}
64
+ }}
65
+ Loader {id:loader;active:false;sourceComponent:preview}
66
+ Text {id:status;text:root.result}
67
+ onClicked:loader.active=true
68
+ }`});
69
+ await host.call('whenLoaded');
70
+ await host.call('evaluate',"document.getElementById('root').click()");
71
+ expect(await host.call('evaluate',"Number(document.getElementById('status').textContent)")).toBe(0);
72
+ }finally{host.close()}
73
+ },30000);
@@ -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);
@@ -0,0 +1,146 @@
1
+ // Opt-in integration against an unchanged Flea/Omarchy checkout. This never
2
+ // substitutes backend replies. Without FLEA_BIN the real unavailable state is expected.
3
+ import { test, expect } from 'bun:test';
4
+ import { mkdtempSync, mkdirSync, symlinkSync, rmSync } from 'node:fs';
5
+ import { join, resolve } from 'node:path';
6
+ import { createBrowser } from '../lib/headless-shell.js';
7
+ const flea = process.env.BUNINU_FLEA_ROOT,
8
+ omarchy = process.env.BUNINU_OMARCHY_ROOT;
9
+ test.skipIf(!flea || !omarchy)(
10
+ 'unchanged Flea boots and opens, switches and closes its real settings panel',
11
+ async () => {
12
+ const dir = mkdtempSync('/tmp/qml-flea-app-'),
13
+ saved = {};
14
+ const variables = {
15
+ HOME: join(dir, 'home'),
16
+ XDG_CONFIG_HOME: join(dir, 'config'),
17
+ XDG_STATE_HOME: join(dir, 'state'),
18
+ XDG_DATA_HOME: join(dir, 'data'),
19
+ XDG_CACHE_HOME: join(dir, 'cache'),
20
+ QML_IMPORT_PATH: join(dir, 'imports'),
21
+ HYPRLAND_INSTANCE_SIGNATURE: '',
22
+ FLEA_BIN: process.env.FLEA_BIN || join(dir, 'no-flea-backend'),
23
+ };
24
+ for (const [key, value] of Object.entries(variables)) {
25
+ saved[key] = process.env[key];
26
+ process.env[key] = value;
27
+ }
28
+ for (const key of [
29
+ 'HOME',
30
+ 'XDG_CONFIG_HOME',
31
+ 'XDG_STATE_HOME',
32
+ 'XDG_DATA_HOME',
33
+ 'XDG_CACHE_HOME',
34
+ ])
35
+ mkdirSync(variables[key], { recursive: true });
36
+ mkdirSync(join(dir, 'imports/qs'), { recursive: true });
37
+ for (const name of ['Commons', 'Ui'])
38
+ symlinkSync(
39
+ resolve(omarchy, 'shell', name),
40
+ join(dir, 'imports/qs', name),
41
+ 'dir',
42
+ );
43
+ let browser;
44
+ try {
45
+ browser = await createBrowser({
46
+ spareRenderer: false,
47
+ rendererTimeout: 60000,
48
+ });
49
+ await browser.context.addInitScript(
50
+ `globalThis.qmlErrors=[];addEventListener('error',e=>qmlErrors.push(String(e.error?.stack||e.message)));const originalError=console.error;console.error=(...args)=>{qmlErrors.push(args.map(String).join(' '));originalError(...args);};`,
51
+ );
52
+ await browser.context.navigate(
53
+ Bun.pathToFileURL(resolve(flea, 'ui/boot/shell.qml')).href,
54
+ );
55
+ const text = () => browser.context.evaluate('document.body.innerText');
56
+ const until = async (fn) => {
57
+ for (let i = 0; i < 150; i++) {
58
+ if (await fn()) return;
59
+ await Bun.sleep(40);
60
+ }
61
+ throw Error('Flea did not settle: ' + (await text()));
62
+ };
63
+ await until(async () => (await text()).includes('PLACES'));
64
+ const clickExpression = async (expression) => {
65
+ const point = JSON.parse(
66
+ await browser.context.evaluate(
67
+ `(()=>{const e=${expression};if(!e)throw Error('Missing Flea control');const r=e.getBoundingClientRect();return JSON.stringify([r.x+r.width/2,r.y+r.height/2]);})()`,
68
+ ),
69
+ );
70
+ await browser.context.click(...point);
71
+ };
72
+ await clickExpression(`document.querySelector('[aria-label="sliders"]')`);
73
+ await until(async () => (await text()).includes('Settings'));
74
+ const visibleLabel = (label) =>
75
+ `[...document.querySelectorAll('[data-qml-type=Text]')].find(e=>e.textContent===${JSON.stringify(label)}&&e.getBoundingClientRect().width>0&&e.getBoundingClientRect().height>0)`;
76
+ await clickExpression(visibleLabel('Display'));
77
+ await until(async () => (await text()).includes('Text size'));
78
+ await Bun.write(
79
+ '/tmp/buninu-flea-settings-display.png',
80
+ Buffer.from(
81
+ await browser.context.screenshot({ format: 'png' }),
82
+ 'base64',
83
+ ),
84
+ );
85
+ await clickExpression(visibleLabel('Keys'));
86
+ await until(async () => (await text()).includes('Keybinding preset'));
87
+ await browser.context.press('Escape', { code: 'Escape' });
88
+ await until(async () => !(await text()).includes('Settings'));
89
+ await browser.context.press('l', { modifiers: 2 });
90
+ await until(
91
+ async () =>
92
+ await browser.context.evaluate(
93
+ 'document.activeElement?.tagName==="INPUT"',
94
+ ),
95
+ );
96
+ await browser.context.type('/tmp/example');
97
+ expect(
98
+ await browser.context.evaluate('document.activeElement.value'),
99
+ ).toBe('/tmp/example');
100
+ await browser.context.press('Escape');
101
+ await until(
102
+ async () =>
103
+ await browser.context.evaluate(
104
+ 'document.activeElement?.tagName!=="INPUT"',
105
+ ),
106
+ );
107
+ await browser.context.press('?', { code: 'Slash', modifiers: 8 });
108
+ await until(async () => (await text()).includes('esc closes'));
109
+ await browser.context.press('Escape');
110
+ await browser.context.press('a');
111
+ await until(async () => (await text()).includes('Connect and save'));
112
+ await browser.context.type('example.invalid');
113
+ await until(async () => (await text()).includes('smb://example.invalid'));
114
+ await clickExpression(visibleLabel('SFTP'));
115
+ await until(async () =>
116
+ (await text()).includes('sftp://example.invalid'),
117
+ );
118
+ await Bun.write(
119
+ '/tmp/buninu-flea-network.png',
120
+ Buffer.from(
121
+ await browser.context.screenshot({ format: 'png' }),
122
+ 'base64',
123
+ ),
124
+ );
125
+ await browser.context.press('Escape');
126
+ expect(await browser.context.evaluate('JSON.stringify(qmlErrors)')).toBe(
127
+ '[]',
128
+ );
129
+ await Bun.write(
130
+ '/tmp/buninu-flea-app.png',
131
+ Buffer.from(
132
+ await browser.context.screenshot({ format: 'png' }),
133
+ 'base64',
134
+ ),
135
+ );
136
+ } finally {
137
+ browser?.close();
138
+ for (const [key, value] of Object.entries(saved)) {
139
+ if (value === undefined) delete process.env[key];
140
+ else process.env[key] = value;
141
+ }
142
+ rmSync(dir, { recursive: true, force: true });
143
+ }
144
+ },
145
+ 120000,
146
+ );