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,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
+ );
@@ -0,0 +1,78 @@
1
+ // Opt-in integration: unmodified packaged QML and the real installed Flea binary.
2
+ import { test, expect } from 'bun:test';
3
+ import { join } from 'node:path';
4
+ import { mkdirSync, writeFileSync } from 'node:fs';
5
+ import { createBrowser } from '../lib/headless-shell.js';
6
+ import { fleaSandbox } from './helpers/flea-sandbox.js';
7
+ const root = process.env.BUNINU_FLEA_ROOT,
8
+ omarchy = process.env.BUNINU_OMARCHY_ROOT,
9
+ binary = process.env.FLEA_BIN;
10
+ test.skipIf(!root || !omarchy || !binary)(
11
+ 'official backend lists real files, receives navigation and refreshes directory changes',
12
+ async () => {
13
+ const sandbox = fleaSandbox(root, omarchy),
14
+ oldPath = process.env.FLEA_PATH;
15
+ const home = sandbox.values.HOME,
16
+ folder = join(home, 'Documents');
17
+ mkdirSync(folder);
18
+ writeFileSync(join(home, 'hello.txt'), 'hello');
19
+ writeFileSync(join(folder, 'inside.txt'), 'inside');
20
+ process.env.FLEA_PATH = home;
21
+ let browser;
22
+ try {
23
+ browser = await createBrowser({
24
+ spareRenderer: false,
25
+ rendererTimeout: 30000,
26
+ });
27
+ await browser.context.addInitScript(
28
+ `globalThis.qmlErrors=[];addEventListener('error',e=>qmlErrors.push(String(e.error?.stack||e.message)));const original=console.error;console.error=(...args)=>{qmlErrors.push(args.map(String).join(' '));original(...args);};`,
29
+ );
30
+ await browser.context.navigate(
31
+ Bun.pathToFileURL(join(sandbox.flea, 'ui/boot/shell.qml')).href,
32
+ );
33
+ const text = () => browser.context.evaluate('document.body.innerText');
34
+ const until = async (predicate) => {
35
+ for (let n = 0; n < 150; n++) {
36
+ if (await predicate()) return;
37
+ await Bun.sleep(40);
38
+ }
39
+ throw Error('Flea did not settle: ' + (await text()));
40
+ };
41
+ await until(async () => (await text()).includes('hello.txt'));
42
+ writeFileSync(join(home, 'added.txt'), 'watch update');
43
+ await until(async () => (await text()).includes('added.txt'));
44
+ await browser.context.press('l', { code: 'KeyL', modifiers: 2 });
45
+ await until(
46
+ async () =>
47
+ await browser.context.evaluate(
48
+ 'document.activeElement?.tagName==="INPUT"',
49
+ ),
50
+ );
51
+ await browser.context.type(folder);
52
+ await browser.context.press('Enter', { code: 'Enter' });
53
+ await until(
54
+ async () =>
55
+ (await text()).includes('inside.txt') &&
56
+ !(await text()).includes('hello.txt'),
57
+ );
58
+ await Bun.write(
59
+ '/tmp/buninu-flea-official-backend.png',
60
+ Buffer.from(
61
+ await browser.context.screenshot({ format: 'png' }),
62
+ 'base64',
63
+ ),
64
+ );
65
+ // Wait through the upstream prebuild driver's first idle tick too.
66
+ await Bun.sleep(1500);
67
+ expect(await browser.context.evaluate('JSON.stringify(qmlErrors)')).toBe(
68
+ '[]',
69
+ );
70
+ } finally {
71
+ browser?.close();
72
+ if (oldPath === undefined) delete process.env.FLEA_PATH;
73
+ else process.env.FLEA_PATH = oldPath;
74
+ sandbox.close();
75
+ }
76
+ },
77
+ 60000,
78
+ );
@@ -0,0 +1,38 @@
1
+ import { test, expect } from 'bun:test';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { createBrowser } from '../lib/headless-shell.js';
5
+ import { fleaSandbox } from './helpers/flea-sandbox.js';
6
+ const flea=process.env.BUNINU_FLEA_ROOT, omarchy=process.env.BUNINU_OMARCHY_ROOT;
7
+ test.skipIf(!flea||!omarchy||!process.env.FLEA_BIN)('Flea initially shows language-marked files and retains rows after repeated pooling',async()=>{
8
+ const sandbox=fleaSandbox(flea,omarchy), oldPath=process.env.FLEA_PATH;
9
+ let browser;
10
+ try {
11
+ process.env.FLEA_PATH=sandbox.values.HOME;
12
+ mkdirSync(join(sandbox.values.HOME,'Documents'));
13
+ await Bun.write(join(sandbox.values.HOME,'Documents/inside.txt'),'inside');
14
+ await Bun.write(join(sandbox.values.HOME,'hello.md'),'hello');
15
+ await Bun.write(join(sandbox.values.HOME,'second.js'),'second');
16
+ browser=await createBrowser({spareRenderer:false,rendererTimeout:30000});
17
+ await browser.context.resize(800,600);
18
+ await browser.context.addInitScript(`globalThis.qmlErrors=[];addEventListener('error',e=>qmlErrors.push(String(e.error?.stack||e.message)));`);
19
+ await browser.context.navigate(Bun.pathToFileURL(join(sandbox.flea,'ui/boot/shell.qml')).href);
20
+ const names=column=>browser.context.evaluate(`Array.from(document.querySelectorAll('[data-qml-type="Text"]')).map(e=>({text:e.textContent,r:e.getBoundingClientRect()})).filter(({text,r})=>/^(cache|config|data|home|imports|state|Documents|inside.txt|hello.md|second.js)$/.test(text)&&r.width>0&&r.y>20&&r.x>=${column===0?200:500}&&r.x<${column===0?500:800}).sort((a,b)=>a.r.y-b.r.y).map(e=>e.text)`);
21
+ async function until(column,wanted){for(let i=0;i<150;i++){const actual=await names(column);if(JSON.stringify(actual)===JSON.stringify(wanted))return;await Bun.sleep(30);}expect(await names(column)).toEqual(wanted);}
22
+ const key=k=>browser.context.press(k,{code:k});
23
+ await until(0,['Documents','hello.md','second.js']);
24
+ await browser.context.press('2',{code:'Digit2',modifiers:2});
25
+ await until(1,['inside.txt']);
26
+ for(let i=0;i<4;i++){
27
+ await key('ArrowRight');await until(0,['inside.txt']);
28
+ await key('ArrowLeft');await until(0,['Documents','hello.md','second.js']);
29
+ }
30
+ await key('ArrowLeft');
31
+ const parent=['cache','config','data','home','imports','state'];
32
+ await until(0,parent);await until(1,['Documents','hello.md','second.js']);
33
+ await key('ArrowUp');await until(1,[]);expect(await names(0)).toEqual(parent);
34
+ await key('ArrowDown');await until(1,['Documents','hello.md','second.js']);expect(await names(0)).toEqual(parent);
35
+ await Bun.write('/tmp/flea-columns-fixed.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
36
+ expect(await browser.context.evaluate('qmlErrors')).toEqual([]);
37
+ }finally{browser?.close();if(oldPath===undefined)delete process.env.FLEA_PATH;else process.env.FLEA_PATH=oldPath;sandbox.close();}
38
+ },60000);
@@ -0,0 +1,78 @@
1
+ // Coverage inventory against unchanged sources. Compilation is not a runtime claim.
2
+ import { test, expect } from 'bun:test';
3
+ import { readFileSync, readdirSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { loadQmlGraphSync } from '../lib/qml/loader.js';
6
+ import { createQmlDocument } from '../lib/qml/document.js';
7
+ import { parseQml } from '../lib/qml/parser.js';
8
+ import { fleaSandbox } from './helpers/flea-sandbox.js';
9
+ const flea = process.env.BUNINU_FLEA_ROOT,
10
+ omarchy = process.env.BUNINU_OMARCHY_ROOT;
11
+ test.skipIf(!flea || !omarchy)(
12
+ 'Flea source inventory parses every QML file and reports known unsupported modules',
13
+ () => {
14
+ const sandbox = fleaSandbox(flea, omarchy);
15
+ try {
16
+ const dir = join(sandbox.flea, 'ui');
17
+ let parsed = 0,
18
+ compiled = 0;
19
+ const blocked = [];
20
+ const parseDirectory = (path) => {
21
+ for (const entry of readdirSync(path, { withFileTypes: true })) {
22
+ const file = join(path, entry.name);
23
+ if (entry.isDirectory()) parseDirectory(file);
24
+ else if (entry.name.endsWith('.qml')) {
25
+ parseQml(readFileSync(file, 'utf8'));
26
+ parsed++;
27
+ }
28
+ }
29
+ };
30
+ parseDirectory(dir);
31
+ for (const name of readdirSync(dir).filter((name) =>
32
+ name.endsWith('.qml'),
33
+ )) {
34
+ const file = join(dir, name),
35
+ url = Bun.pathToFileURL(file).href,
36
+ source = readFileSync(file, 'utf8');
37
+ try {
38
+ const graph = loadQmlGraphSync(
39
+ source,
40
+ url,
41
+ (href) => ({ body: readFileSync(new URL(href)) }),
42
+ {
43
+ importRoots: [
44
+ Bun.pathToFileURL(sandbox.values.QML_IMPORT_PATH + '/').href,
45
+ ],
46
+ },
47
+ );
48
+ const result = createQmlDocument(source, url, {
49
+ graph,
50
+ component: true,
51
+ });
52
+ const error = result.document.querySelector('p')?.textContent;
53
+ if (error) throw Error(error);
54
+ compiled++;
55
+ } catch (error) {
56
+ blocked.push([name, error.message]);
57
+ }
58
+ }
59
+ const expected = {
60
+ PdfViewer: 'QtQuick.Pdf',
61
+ PreviewPdf: 'QtQuick.Pdf',
62
+ PreviewAnimation: 'AnimatedImage',
63
+ };
64
+ for (const [file, message] of blocked) {
65
+ expect(expected[file.slice(0, -4)]).toBeDefined();
66
+ expect(message).toContain(expected[file.slice(0, -4)]);
67
+ }
68
+ expect(parsed).toBeGreaterThanOrEqual(239);
69
+ expect(compiled).toBeGreaterThanOrEqual(231);
70
+ console.log(
71
+ `Flea: ${parsed} QML parsed; ${compiled} top-level components compiled; ${blocked.length} explicitly blocked. This is not full runtime coverage.`,
72
+ );
73
+ } finally {
74
+ sandbox.close();
75
+ }
76
+ },
77
+ 60000,
78
+ );
@@ -0,0 +1,88 @@
1
+ // Actual official executable -> PATH qs wrapper -> browser -> wl_shm on own Land.
2
+ import { test, expect } from 'bun:test';
3
+ import { join, resolve } from 'node:path';
4
+ import { mkdirSync, symlinkSync, writeFileSync } from 'node:fs';
5
+ import { fleaSandbox } from './helpers/flea-sandbox.js';
6
+ const root = process.env.BUNINU_FLEA_ROOT,
7
+ omarchy = process.env.BUNINU_OMARCHY_ROOT,
8
+ binary = process.env.FLEA_BIN;
9
+ test.skipIf(
10
+ !root || !omarchy || !binary || !process.env.BUNINU_QML_WAYLAND_TEST,
11
+ )(
12
+ 'official Flea executable starts our qs wrapper and closes through Wayland',
13
+ async () => {
14
+ const { start } = await import('../../buninu-land/compositor.js');
15
+ const sandbox = fleaSandbox(root, omarchy),
16
+ previous = process.env.XDG_RUNTIME_DIR;
17
+ let land, child, logs;
18
+ try {
19
+ const wrapperDir = join(sandbox.dir, 'bin');
20
+ mkdirSync(wrapperDir);
21
+ symlinkSync(resolve('lib/qml/qswayland'), join(wrapperDir, 'qs'));
22
+ mkdirSync(join(sandbox.values.HOME, 'Documents'));
23
+ writeFileSync(join(sandbox.values.HOME, 'hello.txt'), 'hello');
24
+ process.env.XDG_RUNTIME_DIR = sandbox.dir;
25
+ land = await start([
26
+ '--remote-debugging-port=0',
27
+ '--window-size=900,600',
28
+ ]);
29
+ child = Bun.spawn([resolve(binary), '--gui', sandbox.values.HOME], {
30
+ env: {
31
+ ...process.env,
32
+ PATH: wrapperDir + ':' + process.env.PATH,
33
+ FLEA_UI: join(sandbox.flea, 'ui'),
34
+ WAYLAND_DISPLAY: 'wayland-buninu',
35
+ QSG_RHI_BACKEND: 'software',
36
+ },
37
+ stdout: 'pipe',
38
+ stderr: 'pipe',
39
+ });
40
+ logs = Promise.all([
41
+ new Response(child.stdout).text(),
42
+ new Response(child.stderr).text(),
43
+ ]);
44
+ const until = async (fn) => {
45
+ for (let n = 0; n < 500; n++) {
46
+ if (await fn()) return;
47
+ if (child.exitCode !== null)
48
+ throw Error('Flea exited: ' + (await logs).join('\n'));
49
+ await Bun.sleep(40);
50
+ }
51
+ throw Error('Flea wrapper did not settle');
52
+ };
53
+ await until(() =>
54
+ land.compositor.wm.renderSurfaces().some((s) => s.pixels),
55
+ );
56
+ await Bun.sleep(4000);
57
+ const surfaces = land.compositor.wm
58
+ .renderSurfaces()
59
+ .filter((s) => s.pixels);
60
+ expect(surfaces.length).toBeGreaterThan(0);
61
+ await Bun.write(
62
+ '/tmp/buninu-flea-official-launcher.png',
63
+ Buffer.from(await land.context.screenshot({ format: 'png' }), 'base64'),
64
+ );
65
+ for (const type of ['keyDown', 'keyUp'])
66
+ land.context.dispatchKeyEvent({
67
+ type,
68
+ key: 'w',
69
+ code: 'KeyW',
70
+ modifiers: 4,
71
+ });
72
+ await until(() => child.exitCode !== null);
73
+ expect(await child.exited).toBe(0);
74
+ const output = (await logs).join('\n');
75
+ expect(output).not.toMatch(
76
+ /page error:|QML Loader:|QML createObject:|Wayland frontend:/,
77
+ );
78
+ } finally {
79
+ child?.kill();
80
+ if (child) await child.exited;
81
+ land?.close();
82
+ if (previous === undefined) delete process.env.XDG_RUNTIME_DIR;
83
+ else process.env.XDG_RUNTIME_DIR = previous;
84
+ sandbox.close();
85
+ }
86
+ },
87
+ 60000,
88
+ );
@@ -0,0 +1,31 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {join} from 'node:path';
3
+ import {createBrowser} from '../lib/headless-shell.js';
4
+ import {fleaSandbox} from './helpers/flea-sandbox.js';
5
+ const flea=process.env.BUNINU_FLEA_ROOT,omarchy=process.env.BUNINU_OMARCHY_ROOT;
6
+ test.skipIf(!flea||!omarchy||!process.env.FLEA_BIN)('Flea Markdown preview stacks unequal blocks without overlap after scrolling and resizing',async()=>{
7
+ const sandbox=fleaSandbox(flea,omarchy),oldPath=process.env.FLEA_PATH;let browser;
8
+ try{
9
+ process.env.FLEA_PATH=sandbox.values.HOME;
10
+ const sections=Array.from({length:8},(_,i)=>`## Section ${i}\n\n${'A paragraph with enough words to wrap. '.repeat(i+2)}\n\n\`\`\`\n${'a code line\n'.repeat(i+1)}\`\`\`\n`);
11
+ await Bun.write(join(sandbox.values.HOME,'document.md'),'# Markdown preview\n\n'+sections.join('\n'));
12
+ browser=await createBrowser({spareRenderer:false,rendererTimeout:30000});await browser.context.resize(800,600);
13
+ await browser.context.addInitScript(`globalThis.issues=[];addEventListener('unhandledrejection',e=>issues.push(String(e.reason?.stack||e.reason)));globalThis.views=[];let runtime;Object.defineProperty(globalThis,'__qmlRuntime',{get:()=>runtime,set:r=>{runtime=r;const d=r.describe;r.describe=(v,t,...a)=>{if(t.includes('ListView'))views.push(v);return d(v,t,...a)}}});`);
14
+ await browser.context.navigate(Bun.pathToFileURL(join(sandbox.flea,'ui/boot/shell.qml')).href);
15
+ const run=s=>browser.context.evaluate(s);
16
+ async function until(fn){for(let i=0;i<180;i++){if(await fn())return;await Bun.sleep(30);}throw Error('Markdown preview did not settle');}
17
+ await until(async()=>(await run('document.body.innerText')).includes('document.md'));
18
+ await browser.context.press(' ',{code:'Space'});
19
+ const view=`views.find(v=>v.count>0&&Array.from({length:v.count},(_,i)=>v.itemAtIndex(i)).some(c=>c?.block?.type))`;
20
+ await until(async()=>await run(`!!(${view})`));
21
+ const read=()=>run(`(()=>{const v=${view};return Array.from({length:v.count},(_,i)=>v.itemAtIndex(i)).filter(Boolean).map(c=>({i:c.blockIndex,y:c.y,h:c.height})).sort((a,b)=>a.i-b.i)})()`);
22
+ const separated=rows=>rows.length>2&&rows.every((row,i)=>row.h>0&&(!i||row.y>=rows[i-1].y+rows[i-1].h));
23
+ await until(async()=>separated(await read()));expect(separated(await read())).toBe(true);
24
+ await run(`(${view}).contentY=650`);await until(async()=>separated(await read()));
25
+ expect(separated(await read())).toBe(true);
26
+ await browser.context.resize(600,600);await until(async()=>separated(await read()));
27
+ expect(separated(await read())).toBe(true);
28
+ expect(await run('issues')).toEqual([]);
29
+ await Bun.write('/tmp/flea-markdown-layout.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
30
+ }finally{browser?.close();if(oldPath===undefined)delete process.env.FLEA_PATH;else process.env.FLEA_PATH=oldPath;sandbox.close();}
31
+ },60000);
@@ -0,0 +1,28 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {join} from 'node:path';
3
+ import {createBrowser} from '../lib/headless-shell.js';
4
+ import {fleaSandbox} from './helpers/flea-sandbox.js';
5
+ const root=process.env.BUNINU_FLEA_ROOT,omarchy=process.env.BUNINU_OMARCHY_ROOT;
6
+ test.skipIf(!root||!omarchy)('unchanged Flea PreviewMedia loads MP4 and MP3 through the shared player',async()=>{
7
+ const sandbox=fleaSandbox(root,omarchy);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
+ const file=join(sandbox.dir,'main.qml');
12
+ await Bun.write(file,`import QtQuick
13
+ import ${JSON.stringify(join(root,'ui'))} as Flea
14
+ Flea.PreviewMedia {id:preview;width:640;height:360;kind:"video";autoStart:false;path:${JSON.stringify(Bun.fileURLToPath(video))}}`);
15
+ browser=await createBrowser({spareRenderer:false});
16
+ await browser.context.addInitScript(`globalThis.players=[];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'))players.push(v);return d(v,t,...a)}}});`);
17
+ await browser.context.navigate(Bun.pathToFileURL(file).href);
18
+ const run=s=>browser.context.evaluate(s);
19
+ async function until(code){for(let i=0;i<200;i++){if(await run(code))return;await Bun.sleep(30)}throw Error(await run('document.body.innerText'));}
20
+ await until('players[0]?.duration>1000');
21
+ expect(await run('players[0].hasVideo')).toBe(true);
22
+ expect(await run('players[0].error')).toBe(0);
23
+ await Bun.write('/tmp/qml-flea-media-poster.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
24
+ await run(`players[0].source=${JSON.stringify(audio)}`);
25
+ await until('players[0]?.hasAudio&&!players[0].hasVideo&&players[0].duration>500');
26
+ expect(await run('players[0].error')).toBe(0);
27
+ }finally{browser?.close();sandbox.close();}
28
+ },30000);
@@ -0,0 +1,35 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {mkdirSync} from 'node:fs';
3
+ import {join} from 'node:path';
4
+ import {createBrowser} from '../lib/headless-shell.js';
5
+ import {fleaSandbox} from './helpers/flea-sandbox.js';
6
+ const flea=process.env.BUNINU_FLEA_ROOT,omarchy=process.env.BUNINU_OMARCHY_ROOT;
7
+ test.skipIf(!flea||!omarchy||!process.env.FLEA_BIN)('Flea expands tilde and preserves language icons after repeated returns from an empty home directory',async()=>{
8
+ const sandbox=fleaSandbox(flea,omarchy),oldPath=process.env.FLEA_PATH;
9
+ let browser;
10
+ try{
11
+ const listing=join(sandbox.dir,'listing');mkdirSync(listing);
12
+ for(const name of ['alpha','beta','gamma'])mkdirSync(join(listing,name));
13
+ for(const name of ['a.md','b.js','c.json','d.qml','e.css','f.md'])await Bun.write(join(listing,name),'test');
14
+ process.env.FLEA_PATH=listing;
15
+ browser=await createBrowser({spareRenderer:false,rendererTimeout:30000});await browser.context.resize(800,600);
16
+ await browser.context.addInitScript(`globalThis.qmlNodes=[];let runtime;Object.defineProperty(globalThis,'__qmlRuntime',{get:()=>runtime,set:r=>{runtime=r;const describe=r.describe;r.describe=(v,t,...a)=>{qmlNodes.push({v,t});return describe(v,t,...a)}}});`);
17
+ await browser.context.navigate(Bun.pathToFileURL(join(sandbox.flea,'ui/boot/shell.qml')).href);
18
+ const run=s=>browser.context.evaluate(s),pane=`qmlNodes.find(n=>n.t.includes('Pane')).v`;
19
+ const state=()=>run(`(()=>{const p=${pane};return {path:p.path,state:p.listingState,busy:p.listInFlight}})()`);
20
+ async function until(fn){for(let i=0;i<150;i++){if(await fn())return;await Bun.sleep(30);}throw Error(JSON.stringify(await state()));}
21
+ const icons=()=>run(`qmlNodes.filter(n=>n.t.includes('Row')).map(({v})=>({name:v.row?.n,slug:v.langSlug,live:v.live,item:!!v.langLoader?.item})).filter(v=>v.live&&v.slug)`);
22
+ await until(async()=>(await state()).state==='ready');
23
+ for(let attempt=0;attempt<3;attempt++){
24
+ await browser.context.press('l',{code:'KeyL',modifiers:2});
25
+ await browser.context.press('~',{code:'Backquote',modifiers:8});
26
+ await browser.context.press('Enter',{code:'Enter'});
27
+ await until(async()=>{const s=await state();return s.path===sandbox.values.HOME&&!s.busy&&s.state==='empty'});
28
+ expect((await state()).state).toBe('empty');
29
+ await run(`${pane}.goBack()`);
30
+ await until(async()=>{const s=await state();return s.path===listing&&s.state==='ready'&&!s.busy});
31
+ await until(async()=>{const rows=await icons();return rows.length===6&&rows.every(v=>v.item)});
32
+ expect((await icons()).map(v=>v.name).sort()).toEqual(['a.md','b.js','c.json','d.qml','e.css','f.md']);
33
+ }
34
+ }finally{browser?.close();if(oldPath===undefined)delete process.env.FLEA_PATH;else process.env.FLEA_PATH=oldPath;sandbox.close();}
35
+ },60000);