buninu-browser 0.3.0 → 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 (239) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +19 -4
  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 +37 -2
  30. package/lib/document-types.js +11 -0
  31. package/lib/headless-shell.js +47 -8
  32. package/lib/layout/text-layout.js +17 -9
  33. package/lib/paint/display-list.js +5 -1
  34. package/lib/qml/README.md +971 -0
  35. package/lib/qml/builtins.js +700 -0
  36. package/lib/qml/canvas-dom.js +115 -0
  37. package/lib/qml/document.js +1176 -0
  38. package/lib/qml/format.js +9 -0
  39. package/lib/qml/host.js +324 -0
  40. package/lib/qml/images.js +199 -0
  41. package/lib/qml/launcher.js +132 -0
  42. package/lib/qml/loader.js +517 -0
  43. package/lib/qml/media-renderer.js +49 -0
  44. package/lib/qml/media.js +86 -0
  45. package/lib/qml/models.js +622 -0
  46. package/lib/qml/parser.js +381 -0
  47. package/lib/qml/qs +5 -0
  48. package/lib/qml/qscasty +3 -0
  49. package/lib/qml/qsheadless +3 -0
  50. package/lib/qml/qswayland +3 -0
  51. package/lib/qml/reactive.js +159 -0
  52. package/lib/qml/runtime.js +3381 -0
  53. package/lib/qml/shapes.js +243 -0
  54. package/lib/qml/text.js +109 -0
  55. package/lib/qml/textures.js +123 -0
  56. package/lib/qml/wayland-clipboard.js +32 -0
  57. package/lib/qml/worker-thread.js +70 -0
  58. package/lib/render-tree/index.js +16 -1
  59. package/lib/renderer/bindings.js +11 -0
  60. package/lib/renderer/canvas-2d.js +13 -0
  61. package/lib/renderer/host.js +31 -6
  62. package/lib/renderer/main.js +19 -6
  63. package/lib/renderer/page-realm.js +16 -1
  64. package/lib/renderer/page-renderer.js +230 -13
  65. package/lib/renderer/text-input-paint.js +54 -0
  66. package/lib/renderer/text-input.js +57 -0
  67. package/lib/style/computed-style.js +2 -2
  68. package/package.json +1 -1
  69. package/test/clipboard.test.js +19 -0
  70. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  71. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  72. package/test/fixtures/flea-helpers/bin/bat +3 -0
  73. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  74. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  75. package/test/fixtures/flea-helpers/bin/glow +3 -0
  76. package/test/fixtures/flea-helpers/bin/qs +3 -0
  77. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  78. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  79. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  80. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  81. package/test/fixtures/flea-helpers/check.mjs +36 -0
  82. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  83. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  84. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  85. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  86. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  87. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  88. package/test/fixtures/qml/flea/LICENSE +21 -0
  89. package/test/fixtures/qml/flea/README.md +25 -0
  90. package/test/fixtures/qml/flea/Theme.qml +8 -0
  91. package/test/fixtures/qml/flea/demo.qml +24 -0
  92. package/test/fixtures/qml/flea/qmldir +3 -0
  93. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  94. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  95. package/test/fixtures/qml/flea-theme/README.md +70 -0
  96. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  97. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  98. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  99. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  100. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  101. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  102. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  103. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  104. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  105. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  106. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  107. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  108. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  109. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  110. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  111. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  112. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  113. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  114. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  115. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  116. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  117. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  118. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  119. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  120. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  121. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  122. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  123. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  124. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  125. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  126. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  127. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  128. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  129. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  130. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  131. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  132. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  133. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  134. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  135. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  136. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  137. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  138. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  139. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  140. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  141. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  142. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  143. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  144. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  145. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  146. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  147. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  148. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  167. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  168. package/test/fixtures/qml/hello.qml +26 -0
  169. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  170. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  171. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  172. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  173. package/test/fixtures/qml/imports/logic/base.js +2 -0
  174. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  175. package/test/fixtures/qml/imports/logic/math.js +3 -0
  176. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  177. package/test/fixtures/qml/imports/main.qml +21 -0
  178. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  179. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  180. package/test/fixtures/qml/multiply.qml +61 -0
  181. package/test/fixtures/qml-canvas-dom.js +90 -0
  182. package/test/fixtures/qml-media-audio.js +39 -0
  183. package/test/helpers/flea-sandbox.js +48 -0
  184. package/test/helpers/quickshell-fixture.js +125 -0
  185. package/test/html-clipboard-wayland.test.js +47 -0
  186. package/test/index.test.js +13 -0
  187. package/test/manual-casty-without-qml.js +87 -0
  188. package/test/manual-flea-application.js +49 -0
  189. package/test/manual-flea-binary.js +62 -0
  190. package/test/manual-flea-qml.js +9 -0
  191. package/test/manual-flea-settings.js +40 -0
  192. package/test/manual-flea-theme.js +40 -0
  193. package/test/manual-qml-removal.js +46 -0
  194. package/test/manual-without-qml.js +45 -0
  195. package/test/qml-canvas-dom.test.js +37 -0
  196. package/test/qml-clipboard-bridge.test.js +25 -0
  197. package/test/qml-clipboard-wayland.test.js +40 -0
  198. package/test/qml-connections-timing.test.js +73 -0
  199. package/test/qml-dynamic.test.js +401 -0
  200. package/test/qml-flea-app.test.js +146 -0
  201. package/test/qml-flea-backend.test.js +78 -0
  202. package/test/qml-flea-columns-backend.test.js +38 -0
  203. package/test/qml-flea-compile.test.js +78 -0
  204. package/test/qml-flea-launcher.test.js +88 -0
  205. package/test/qml-flea-markdown-backend.test.js +31 -0
  206. package/test/qml-flea-media.test.js +28 -0
  207. package/test/qml-flea-navigation-backend.test.js +35 -0
  208. package/test/qml-flea-navigation-timing.test.js +147 -0
  209. package/test/qml-flea-preview-backend.test.js +53 -0
  210. package/test/qml-flea-previews.test.js +163 -0
  211. package/test/qml-flea-rows.test.js +101 -0
  212. package/test/qml-flea-textures.test.js +59 -0
  213. package/test/qml-flea-view-switch.test.js +33 -0
  214. package/test/qml-flea-wayland.test.js +68 -0
  215. package/test/qml-flea.test.js +34 -0
  216. package/test/qml-host.test.js +238 -0
  217. package/test/qml-imports.test.js +102 -0
  218. package/test/qml-inherited-handlers.test.js +51 -0
  219. package/test/qml-input-selection.test.js +66 -0
  220. package/test/qml-interactions.test.js +309 -0
  221. package/test/qml-language.test.js +167 -0
  222. package/test/qml-launcher.test.js +54 -0
  223. package/test/qml-layout-text.test.js +205 -0
  224. package/test/qml-media.test.js +63 -0
  225. package/test/qml-persisted-delegates.test.js +45 -0
  226. package/test/qml-quickshell-controls.test.js +65 -0
  227. package/test/qml-quickshell-settings.test.js +62 -0
  228. package/test/qml-repeater.test.js +72 -0
  229. package/test/qml-style-updates.test.js +124 -0
  230. package/test/qml-textures.test.js +66 -0
  231. package/test/qml-views.test.js +332 -0
  232. package/test/qml-worker.test.js +69 -0
  233. package/test/qml.test.js +83 -0
  234. package/test/renderer-host.test.js +11 -0
  235. package/test/text-input-pointer.test.js +31 -0
  236. package/test/text-input-scroll.test.js +29 -0
  237. package/test/text-input.test.js +47 -0
  238. package/test/wayland-frontend.test.js +83 -0
  239. package/test/wayland-urlbar.test.js +38 -0
@@ -0,0 +1,163 @@
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,
6
+ omarchy = process.env.BUNINU_OMARCHY_ROOT;
7
+ for (const [type, extension, contents, expected] of [
8
+ [
9
+ 'PreviewText',
10
+ 'txt',
11
+ Array.from({ length: 80 }, (_, i) => 'example line ' + i).join('\n'),
12
+ 'example line 0',
13
+ ],
14
+ ['PreviewText', 'csv', 'Name,Count\nApple,12\nOrange,9\n', 'Apple'],
15
+ [
16
+ 'PreviewMarkdown',
17
+ 'md',
18
+ '# Flea preview\n\nA **bold** paragraph.\n\n- First item\n- Second item\n',
19
+ 'Flea preview',
20
+ ],
21
+ ])
22
+ test.skipIf(!flea || !omarchy)(
23
+ 'unchanged Flea ' + type + ' renders a real .' + extension + ' file',
24
+ async () => {
25
+ const sandbox = fleaSandbox(flea, omarchy);
26
+ let browser;
27
+ try {
28
+ const path = join(sandbox.dir, 'sample.' + extension);
29
+ await Bun.write(path, contents);
30
+ const file = join(sandbox.dir, 'main.qml');
31
+ await Bun.write(
32
+ file,
33
+ `import QtQuick
34
+ import ${JSON.stringify(join(sandbox.flea, 'ui'))} as Flea
35
+ Rectangle {width:800;height:600;color:Flea.Theme.color.background
36
+ Flea.${type} {id:preview;anchors.fill:parent;active:true;path:${JSON.stringify(path)};size:${Buffer.byteLength(contents)}}
37
+ }`,
38
+ );
39
+ browser = await createBrowser({
40
+ spareRenderer: false,
41
+ rendererTimeout: 30000,
42
+ });
43
+ await browser.context.addInitScript(
44
+ `globalThis.qmlErrors=[];addEventListener('error',e=>qmlErrors.push(String(e.error?.stack||e.message)));const old=console.error;console.error=(...args)=>{qmlErrors.push(args.map(String).join(' '));old(...args);};`,
45
+ );
46
+ await browser.context.navigate(Bun.pathToFileURL(file).href);
47
+ let text = '';
48
+ for (let i = 0; i < 150; i++) {
49
+ text = await browser.context.evaluate('document.body.innerText');
50
+ if (text.includes(expected)) break;
51
+ await Bun.sleep(40);
52
+ }
53
+ expect(
54
+ await browser.context.evaluate('JSON.stringify(qmlErrors)'),
55
+ ).toBe('[]');
56
+ expect(text).toContain(expected);
57
+ await Bun.write(
58
+ '/tmp/buninu-flea-preview-' + extension + '.png',
59
+ Buffer.from(
60
+ await browser.context.screenshot({ format: 'png' }),
61
+ 'base64',
62
+ ),
63
+ );
64
+ } finally {
65
+ browser?.close();
66
+ sandbox.close();
67
+ }
68
+ },
69
+ 60000,
70
+ );
71
+
72
+ for (const extension of ['svg', 'png'])
73
+ test.skipIf(!flea || !omarchy)(
74
+ 'unchanged Flea PreviewImage loads, zooms and resets a real ' +
75
+ extension +
76
+ ' image',
77
+ async () => {
78
+ const sandbox = fleaSandbox(flea, omarchy);
79
+ let browser;
80
+ try {
81
+ const path = join(sandbox.dir, 'sample.' + extension);
82
+ if (extension === 'svg')
83
+ await Bun.write(
84
+ path,
85
+ '<svg xmlns="http://www.w3.org/2000/svg" width="320" height="160"><rect width="160" height="160" fill="#ff0000"/><rect x="160" width="160" height="160" fill="#0000ff"/></svg>',
86
+ );
87
+ else {
88
+ const { encodeImage } = await import('../lib/paint/frame.js');
89
+ const pixels = new Uint8Array(320 * 160 * 4);
90
+ for (let y = 0; y < 160; y++)
91
+ for (let x = 0; x < 320; x++)
92
+ pixels.set(
93
+ x < 160 ? [255, 0, 0, 255] : [0, 0, 255, 255],
94
+ (y * 320 + x) * 4,
95
+ );
96
+ await Bun.write(path, await encodeImage(pixels, 320, 160));
97
+ }
98
+ const file = join(sandbox.dir, 'image.qml');
99
+ await Bun.write(
100
+ file,
101
+ `import QtQuick
102
+ import ${JSON.stringify(join(sandbox.flea, 'ui'))} as Flea
103
+ Rectangle {width:800;height:600;color:Flea.Theme.color.background
104
+ Flea.PreviewImage {id:preview;anchors.fill:parent;path:${JSON.stringify(path)}}
105
+ Text {id:status;text:preview.status+":"+preview.zoom+":"+preview.fullPicture.drawn}
106
+ Button {id:zoom;y:550;text:"Zoom";onClicked:preview.zoomBy(1)}
107
+ Button {id:fit;x:100;y:550;text:"Fit";onClicked:preview.zoomFit()}
108
+ }`,
109
+ );
110
+ browser = await createBrowser({
111
+ spareRenderer: false,
112
+ rendererTimeout: 30000,
113
+ });
114
+ await browser.context.addInitScript(
115
+ `globalThis.qmlErrors=[];addEventListener('error',e=>qmlErrors.push(String(e.error?.stack||e.message)));`,
116
+ );
117
+ await browser.context.navigate(Bun.pathToFileURL(file).href);
118
+ const until = async (predicate) => {
119
+ for (let i = 0; i < 150; i++) {
120
+ const status = await browser.context.evaluate(
121
+ 'document.getElementById("status")?.textContent',
122
+ );
123
+ if (predicate(status)) return status;
124
+ await Bun.sleep(40);
125
+ }
126
+ throw Error(
127
+ await browser.context.evaluate(
128
+ 'document.body.innerText+JSON.stringify(qmlErrors)',
129
+ ),
130
+ );
131
+ };
132
+ await until((s) => s?.startsWith('image:1:'));
133
+ await browser.context.evaluate(
134
+ 'document.getElementById("zoom").click()',
135
+ );
136
+ const zoomed = await until(
137
+ (s) =>
138
+ s?.startsWith('image:') &&
139
+ !s.startsWith('image:1:') &&
140
+ s.endsWith(':true'),
141
+ );
142
+ expect(zoomed).toContain(':true');
143
+ await Bun.write(
144
+ '/tmp/buninu-flea-preview-' + extension + '.png',
145
+ Buffer.from(
146
+ await browser.context.screenshot({ format: 'png' }),
147
+ 'base64',
148
+ ),
149
+ );
150
+ await browser.context.evaluate(
151
+ 'document.getElementById("fit").click()',
152
+ );
153
+ await until((s) => s === 'image:1:false');
154
+ expect(
155
+ await browser.context.evaluate('JSON.stringify(qmlErrors)'),
156
+ ).toBe('[]');
157
+ } finally {
158
+ browser?.close();
159
+ sandbox.close();
160
+ }
161
+ },
162
+ 60000,
163
+ );
@@ -0,0 +1,101 @@
1
+ // Component tests with ordinary input data, not a replacement Flea backend.
2
+ import { test, expect } from 'bun:test';
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,
7
+ omarchy = process.env.BUNINU_OMARCHY_ROOT;
8
+ for (const type of ['Row', 'GridTile'])
9
+ test.skipIf(!flea || !omarchy)(
10
+ 'unchanged Flea ' +
11
+ type +
12
+ ' displays file metadata and reacts to changed input',
13
+ async () => {
14
+ const sandbox = fleaSandbox(flea, omarchy);
15
+ let browser;
16
+ try {
17
+ const file = join(sandbox.dir, 'rows.qml');
18
+ await Bun.write(
19
+ file,
20
+ `import QtQuick
21
+ import ${JSON.stringify(join(sandbox.flea, 'ui'))} as Flea
22
+ Rectangle {id:root;width:800;height:600;color:Flea.Theme.color.background
23
+ property var entry:({n:"example-notes.txt",s:12345,m:1700000000,p:33188,i:"file",k:0,d:false})
24
+ Flea.${type} {id:row;x:20;y:30;width:${type === 'Row' ? 760 : 260};height:${type === 'Row' ? 60 : 300};row:root.entry;cursor:true}
25
+ ${
26
+ type === 'GridTile'
27
+ ? `Text {id:captionMetrics;x:400;y:400;text:row.captionItem.lineCount+":"+row.captionItem.truncated}
28
+ Button {id:longName;x:200;y:500;text:"Long filename";onClicked:root.entry=({n:"a very long file name with many words and notes.txt",s:456,m:1700000000,p:33188,i:"file",k:0,d:false})}`
29
+ : ''
30
+ }
31
+ Button {id:change;y:500;text:"Change input";onClicked:root.entry=({n:"renamed-notes.txt",s:456,m:1700000000,p:33188,i:"file",k:0,d:false})}
32
+ }`,
33
+ );
34
+ browser = await createBrowser({
35
+ spareRenderer: false,
36
+ rendererTimeout: 30000,
37
+ });
38
+ await browser.context.addInitScript(
39
+ `globalThis.qmlErrors=[];addEventListener('error',e=>qmlErrors.push(String(e.error?.stack||e.message)));const old=console.error;console.error=(...args)=>{qmlErrors.push(args.map(String).join(' '));old(...args);};`,
40
+ );
41
+ await browser.context.navigate(Bun.pathToFileURL(file).href);
42
+ const ready = await browser.context.evaluate(
43
+ '!!document.getElementById("change")',
44
+ );
45
+ if (!ready)
46
+ throw Error(
47
+ await browser.context.evaluate(
48
+ 'document.querySelector("p")?.textContent||document.body.innerText',
49
+ ),
50
+ );
51
+
52
+ const until = async (word) => {
53
+ for (let i = 0; i < 150; i++) {
54
+ const text = await browser.context.evaluate(
55
+ 'document.body.innerText',
56
+ );
57
+ if (text.includes(word)) return;
58
+ await Bun.sleep(40);
59
+ }
60
+ throw Error(
61
+ await browser.context.evaluate(
62
+ 'document.body.innerText+JSON.stringify(qmlErrors)',
63
+ ),
64
+ );
65
+ };
66
+ await until('example-notes.txt');
67
+ await browser.context.evaluate(
68
+ 'document.getElementById("change").click()',
69
+ );
70
+ await until('renamed-notes.txt');
71
+ if (type === 'GridTile') {
72
+ await browser.context.evaluate(
73
+ 'document.getElementById("longName").click()',
74
+ );
75
+ let metric = '';
76
+ for (let i = 0; i < 100; i++) {
77
+ metric = await browser.context.evaluate(
78
+ 'document.getElementById("captionMetrics").textContent',
79
+ );
80
+ if (metric === '2:false') break;
81
+ await Bun.sleep(20);
82
+ }
83
+ expect(metric).toBe('2:false');
84
+ }
85
+ expect(
86
+ await browser.context.evaluate('JSON.stringify(qmlErrors)'),
87
+ ).toBe('[]');
88
+ await Bun.write(
89
+ '/tmp/buninu-flea-' + type + '.png',
90
+ Buffer.from(
91
+ await browser.context.screenshot({ format: 'png' }),
92
+ 'base64',
93
+ ),
94
+ );
95
+ } finally {
96
+ browser?.close();
97
+ sandbox.close();
98
+ }
99
+ },
100
+ 60000,
101
+ );
@@ -0,0 +1,59 @@
1
+ import { test, expect } from 'bun:test';
2
+ import { join } from 'node:path';
3
+ import { createBrowser } from '../lib/headless-shell.js';
4
+ import { loadGraphics } from '../lib/renderer/page-renderer.js';
5
+ import { fleaSandbox } from './helpers/flea-sandbox.js';
6
+ const flea=process.env.BUNINU_FLEA_ROOT, omarchy=process.env.BUNINU_OMARCHY_ROOT;
7
+ for(const kind of ['swap','clip']) test.skipIf(!flea||!omarchy)('unchanged Flea '+kind+' paints actual texture pixels',async()=>{
8
+ const sandbox=fleaSandbox(flea,omarchy);
9
+ let browser;
10
+ try {
11
+ const content=kind==='swap'?`
12
+ property bool complete:true
13
+ Flea.PreviewSwap {id:swap;x:20;y:20;width:180;height:100;ready:complete;folderHold:true
14
+ Rectangle {id:source;anchors.fill:parent;color:"red"}}
15
+ Button {id:move;y:150;text:"Move";onClicked:{complete=false;swap.hold(function(){source.color="blue";swap.start(false)},1)}}
16
+ Button {id:finish;x:100;y:150;text:"Finish";onClicked:{swap.folderHold=false;complete=true}}
17
+ Text {id:status;y:200;text:JSON.stringify(swap.describe())}
18
+ `:`
19
+ Rectangle {id:source;x:20;y:20;width:100;height:60;color:"red"}
20
+ Flea.HtmlClip {id:clip;x:150;y:20;width:100;height:60;radius:20;target:source}
21
+ Button {id:blue;y:150;text:"Blue";onClicked:source.color="blue"}
22
+ `;
23
+ const file=join(sandbox.dir,'main.qml');
24
+ await Bun.write(file,`import QtQuick\nimport ${JSON.stringify(join(sandbox.flea,'ui'))} as Flea\nRectangle {width:400;height:240;color:"white"\n${content}\n}`);
25
+ browser=await createBrowser({spareRenderer:false,rendererTimeout:15000});
26
+ await browser.context.resize(400,240);
27
+ await browser.context.addInitScript('globalThis.errors=[];addEventListener("error",e=>errors.push(String(e.error?.stack||e.message)))');
28
+ await browser.context.navigate(Bun.pathToFileURL(file).href);
29
+ const run=code=>browser.context.evaluate(code);
30
+ async function until(fn){for(let i=0;i<70;i++){if(await fn())return;await Bun.sleep(20);}throw Error(await run('document.body.innerText+JSON.stringify(errors)'));}
31
+ const {CanvasKit:kit}=await loadGraphics();
32
+ async function capture(){return Buffer.from(await browser.context.screenshot({format:'png'}),'base64');}
33
+ function pixel(bytes,x,y){const image=kit.MakeImageFromEncoded(bytes);try{return [...image.readPixels(x,y,{width:1,height:1,colorType:kit.ColorType.RGBA_8888,alphaType:kit.AlphaType.Unpremul,colorSpace:kit.ColorSpace.SRGB})];}finally{image.delete();}}
34
+ if(kind==='swap'){
35
+ await until(async()=>await run('!!document.getElementById("move")'));
36
+ expect(pixel(await capture(),30,30)).toEqual([255,0,0,255]);
37
+ await run('document.getElementById("move").click()');
38
+ await until(async()=>await run('JSON.parse(document.getElementById("status").textContent).holding'));
39
+ const held=await capture();await Bun.write('/tmp/flea-preview-held.png',held);
40
+ expect(pixel(held,30,30)).toEqual([255,0,0,255]);
41
+ expect(await run('document.getElementById("source").style.backgroundColor')).toBe('blue');
42
+ await run('document.getElementById("finish").click()');
43
+ await until(async()=>!(await run('JSON.parse(document.getElementById("status").textContent).holding')));
44
+ const done=await capture();await Bun.write('/tmp/flea-preview-released.png',done);
45
+ expect(pixel(done,30,30)).toEqual([0,0,255,255]);
46
+ expect(await run('JSON.parse(document.getElementById("status").textContent).last.end')).toBe('landed');
47
+ }else{
48
+ await until(async()=>pixel(await capture(),200,50)[0]===255&&pixel(await capture(),200,50)[1]===0);
49
+ const rounded=await capture();await Bun.write('/tmp/flea-preview-rounded.png',rounded);
50
+ expect(pixel(rounded,25,25)).toEqual([255,255,255,255]);
51
+ expect(pixel(rounded,151,21)).toEqual([255,255,255,255]);
52
+ expect(pixel(rounded,200,50)).toEqual([255,0,0,255]);
53
+ await run('document.getElementById("blue").click()');
54
+ await until(async()=>pixel(await capture(),200,50)[0]===0);
55
+ expect(pixel(await capture(),200,50)).toEqual([0,0,255,255]);
56
+ }
57
+ expect(await run('errors')).toEqual([]);
58
+ }finally{browser?.close();sandbox.close();}
59
+ },30000);
@@ -0,0 +1,33 @@
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 hides retained columns when switching back to list',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.views=[];let rt;Object.defineProperty(globalThis,"__qmlRuntime",{get:()=>rt,set:r=>{rt=r;const d=r.describe;r.describe=(v,t,...a)=>{if(t.includes("ColumnsArea")||t.includes("Pane"))views.push({v,t});return d(v,t,...a)}}});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
+ await until(0,['Documents','hello.md','second.js']);
23
+ await browser.context.press('2',{code:'Digit2',modifiers:2});
24
+ await until(1,['inside.txt']);
25
+ await browser.context.press('1',{code:'Digit1',modifiers:2});
26
+ await Bun.sleep(100);
27
+ expect(await browser.context.evaluate(`views.find(({t})=>t.includes('Pane')).v.viewMode`)).toBe('list');
28
+ expect(await browser.context.evaluate(`views.find(({t})=>t.includes('Pane')).v.columnsArea.visible`)).toBe(false);
29
+ await until(1,[]);
30
+ await Bun.write('/tmp/flea-list-switch.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
31
+ expect(await browser.context.evaluate('qmlErrors')).toEqual([]);
32
+ }finally{browser?.close();if(oldPath===undefined)delete process.env.FLEA_PATH;else process.env.FLEA_PATH=oldPath;sandbox.close();}
33
+ },60000);
@@ -0,0 +1,68 @@
1
+ // Opt-in real socket test; owns its compositor and never touches a user's Land.
2
+ import { test, expect } from 'bun:test';
3
+ import { join } from 'node:path';
4
+ import { fleaSandbox } from './helpers/flea-sandbox.js';
5
+ import { launch } from '../frontends/wayland/index.js';
6
+ const flea = process.env.BUNINU_FLEA_ROOT,
7
+ omarchy = process.env.BUNINU_OMARCHY_ROOT;
8
+ test.skipIf(!flea || !omarchy || !process.env.BUNINU_QML_WAYLAND_TEST)(
9
+ 'real Flea receives Wayland keys and closes through Super-W',
10
+ async () => {
11
+ const { start } = await import('../../buninu-land/compositor.js');
12
+ const sandbox = fleaSandbox(flea, omarchy),
13
+ oldRuntime = process.env.XDG_RUNTIME_DIR;
14
+ let land, app;
15
+ try {
16
+ process.env.XDG_RUNTIME_DIR = sandbox.dir;
17
+ land = await start([
18
+ '--remote-debugging-port=0',
19
+ '--window-size=1000,720',
20
+ ]);
21
+ app = await launch({
22
+ url: Bun.pathToFileURL(join(sandbox.flea, 'ui/boot/shell.qml')).href,
23
+ width: 1000,
24
+ height: 720,
25
+ socket: join(sandbox.dir, 'wayland-buninu'),
26
+ });
27
+ const until = async (predicate) => {
28
+ for (let i = 0; i < 300; i++) {
29
+ if (await predicate()) return;
30
+ await Bun.sleep(40);
31
+ }
32
+ throw Error('Flea Wayland state did not settle');
33
+ };
34
+ const text = () =>
35
+ app.browser.context.evaluate('document.body.innerText');
36
+ await until(() =>
37
+ land.compositor.wm.renderSurfaces().some((s) => s.pixels),
38
+ );
39
+ expect(await text()).toContain('PLACES');
40
+ const key = async (key, code, modifiers = 0) => {
41
+ for (const type of ['keyDown', 'keyUp'])
42
+ land.context.dispatchKeyEvent({ type, key, code, modifiers });
43
+ await Bun.sleep(100);
44
+ };
45
+ await key(',', 'Comma');
46
+ await until(async () => (await text()).includes('Settings'));
47
+ await Bun.write(
48
+ '/tmp/buninu-flea-wayland.png',
49
+ Buffer.from(await land.context.screenshot({ format: 'png' }), 'base64'),
50
+ );
51
+ await key('Escape', 'Escape');
52
+ await until(async () => !(await text()).includes('Settings'));
53
+ let closed = false;
54
+ app.done.then(() => (closed = true));
55
+ await key('w', 'KeyW', 4);
56
+ await until(() => closed);
57
+ await until(() => !land.compositor.wm.visible.length);
58
+ expect(closed).toBe(true);
59
+ } finally {
60
+ app?.close();
61
+ land?.close();
62
+ if (oldRuntime === undefined) delete process.env.XDG_RUNTIME_DIR;
63
+ else process.env.XDG_RUNTIME_DIR = oldRuntime;
64
+ sandbox.close();
65
+ }
66
+ },
67
+ 120000,
68
+ );
@@ -0,0 +1,34 @@
1
+ import { test, expect } from 'bun:test';
2
+ import { createBrowser } from '../lib/headless-shell.js';
3
+
4
+ test('Flea EscapeHint imports a real singleton and shares reactive theme updates', async () => {
5
+ const browser = await createBrowser({ spareRenderer: false });
6
+ try {
7
+ await browser.context.resize(640, 400, 1);
8
+ await browser.context.navigate(
9
+ new URL('./fixtures/qml/flea/demo.qml', import.meta.url).href,
10
+ );
11
+ expect(await browser.context.evaluate('document.title')).toBe(
12
+ 'Browser — Flea real import test',
13
+ );
14
+ const colors = () =>
15
+ browser.context.evaluate(
16
+ "JSON.stringify([...document.querySelectorAll('[data-qml-type=Text]')].filter(e=>e.textContent==='esc').map(e=>e.style.color))",
17
+ );
18
+ expect(JSON.parse(await colors())).toEqual(['#70e1e8', '#70e1e8']);
19
+ await browser.context.evaluate('document.querySelector("button").click()');
20
+ expect(JSON.parse(await colors())).toEqual(['#ffcc66', '#ffcc66']);
21
+ await browser.context.evaluate('document.querySelector("button").click()');
22
+ expect(JSON.parse(await colors())).toEqual(['#70e1e8', '#70e1e8']);
23
+ expect(await browser.context.evaluate('typeof Theme')).toBe('undefined');
24
+ await Bun.write(
25
+ '/tmp/browser-flea-import.png',
26
+ Buffer.from(
27
+ await browser.context.screenshot({ format: 'png' }),
28
+ 'base64',
29
+ ),
30
+ );
31
+ } finally {
32
+ browser.close();
33
+ }
34
+ }, 30000);