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.
- package/LICENSES/jsmdcui.txt +22 -0
- package/README.md +102 -2
- package/frontends/wayland/LICENSE-land +21 -0
- package/frontends/wayland/README.md +123 -0
- package/frontends/wayland/casty/LICENSE +21 -0
- package/frontends/wayland/casty/bookmarks.js +32 -0
- package/frontends/wayland/casty/click.js +96 -0
- package/frontends/wayland/casty/commands.js +61 -0
- package/frontends/wayland/casty/config.js +45 -0
- package/frontends/wayland/casty/download-directory.js +28 -0
- package/frontends/wayland/casty/download.js +49 -0
- package/frontends/wayland/casty/hints.js +334 -0
- package/frontends/wayland/casty/keys.js +56 -0
- package/frontends/wayland/casty/page-commands.js +80 -0
- package/frontends/wayland/casty/prompt-width.js +148 -0
- package/frontends/wayland/casty/urlbar.js +606 -0
- package/frontends/wayland/client.js +88 -0
- package/frontends/wayland/clipboard.js +80 -0
- package/frontends/wayland/index.js +116 -0
- package/frontends/wayland/input.js +56 -0
- package/frontends/wayland/protocol.js +55 -0
- package/frontends/wayland/protocols/wayland.xml +3248 -0
- package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
- package/frontends/wayland/transport.js +120 -0
- package/frontends/wayland/urlbar.js +158 -0
- package/index.js +28 -1
- package/lib/clipboard-command.js +10 -0
- package/lib/clipboard-powershell.js +22 -0
- package/lib/clipboard.js +57 -0
- package/lib/document-types.js +11 -0
- package/lib/headless-shell.js +78 -13
- package/lib/layout/text-layout.js +17 -9
- package/lib/paint/display-list.js +5 -1
- package/lib/paint/frame.js +35 -3
- package/lib/paint/raw-frames.js +47 -0
- package/lib/qml/README.md +971 -0
- package/lib/qml/builtins.js +700 -0
- package/lib/qml/canvas-dom.js +115 -0
- package/lib/qml/document.js +1176 -0
- package/lib/qml/format.js +9 -0
- package/lib/qml/host.js +324 -0
- package/lib/qml/images.js +199 -0
- package/lib/qml/launcher.js +132 -0
- package/lib/qml/loader.js +517 -0
- package/lib/qml/media-renderer.js +49 -0
- package/lib/qml/media.js +86 -0
- package/lib/qml/models.js +622 -0
- package/lib/qml/parser.js +381 -0
- package/lib/qml/qs +5 -0
- package/lib/qml/qscasty +3 -0
- package/lib/qml/qsheadless +3 -0
- package/lib/qml/qswayland +3 -0
- package/lib/qml/reactive.js +159 -0
- package/lib/qml/runtime.js +3381 -0
- package/lib/qml/shapes.js +243 -0
- package/lib/qml/text.js +109 -0
- package/lib/qml/textures.js +123 -0
- package/lib/qml/wayland-clipboard.js +32 -0
- package/lib/qml/worker-thread.js +70 -0
- package/lib/render-tree/index.js +16 -1
- package/lib/renderer/bindings.js +11 -0
- package/lib/renderer/canvas-2d.js +13 -0
- package/lib/renderer/compositor-channel.js +103 -0
- package/lib/renderer/compositor.js +25 -33
- package/lib/renderer/flags.js +1 -1
- package/lib/renderer/host.js +128 -92
- package/lib/renderer/main.js +38 -4
- package/lib/renderer/page-realm.js +32 -1
- package/lib/renderer/page-renderer.js +250 -21
- package/lib/renderer/text-input-paint.js +54 -0
- package/lib/renderer/text-input.js +57 -0
- package/lib/style/computed-style.js +2 -2
- package/package.json +1 -1
- package/test/clipboard.test.js +54 -0
- package/test/fixtures/browser-raw-demo.html +36 -0
- package/test/fixtures/flea-helpers/Dockerfile +36 -0
- package/test/fixtures/flea-helpers/bat.mjs +43 -0
- package/test/fixtures/flea-helpers/bin/bat +3 -0
- package/test/fixtures/flea-helpers/bin/bwrap +10 -0
- package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
- package/test/fixtures/flea-helpers/bin/glow +3 -0
- package/test/fixtures/flea-helpers/bin/qs +3 -0
- package/test/fixtures/flea-helpers/bin/unsupported +3 -0
- package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
- package/test/fixtures/flea-helpers/check-media.mjs +29 -0
- package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
- package/test/fixtures/flea-helpers/check.mjs +36 -0
- package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
- package/test/fixtures/flea-helpers/source-mime.xml +28 -0
- package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
- package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
- package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
- package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea/LICENSE +21 -0
- package/test/fixtures/qml/flea/README.md +25 -0
- package/test/fixtures/qml/flea/Theme.qml +8 -0
- package/test/fixtures/qml/flea/demo.qml +24 -0
- package/test/fixtures/qml/flea/qmldir +3 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
- package/test/fixtures/qml/flea-theme/README.md +70 -0
- package/test/fixtures/qml/flea-theme/button.qml +17 -0
- package/test/fixtures/qml/flea-theme/demo.qml +27 -0
- package/test/fixtures/qml/flea-theme/field.qml +21 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
- package/test/fixtures/qml/flea-theme/segment.qml +17 -0
- package/test/fixtures/qml/flea-theme/settings.qml +58 -0
- package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
- package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
- package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
- package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
- package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
- package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
- package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
- package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
- package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
- package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
- package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
- package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
- package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
- package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
- package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
- package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
- package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
- package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
- package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
- package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
- package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
- package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
- package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
- package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
- package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
- package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
- package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
- package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
- package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
- package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
- package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
- package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
- package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
- package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
- package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
- package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
- package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
- package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
- package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
- package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
- package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
- package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
- package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
- package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
- package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
- package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
- package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
- package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
- package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
- package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
- package/test/fixtures/qml/hello.qml +26 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
- package/test/fixtures/qml/imports/components/Badge.qml +15 -0
- package/test/fixtures/qml/imports/logic/base.js +2 -0
- package/test/fixtures/qml/imports/logic/counter.js +4 -0
- package/test/fixtures/qml/imports/logic/math.js +3 -0
- package/test/fixtures/qml/imports/logic/shared.js +3 -0
- package/test/fixtures/qml/imports/main.qml +21 -0
- package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
- package/test/fixtures/qml/imports/theme/qmldir +2 -0
- package/test/fixtures/qml/multiply.qml +61 -0
- package/test/fixtures/qml-canvas-dom.js +90 -0
- package/test/fixtures/qml-media-audio.js +39 -0
- package/test/fixtures/raw-capture-benchmark.json +76 -0
- package/test/helpers/flea-sandbox.js +48 -0
- package/test/helpers/quickshell-fixture.js +125 -0
- package/test/html-clipboard-wayland.test.js +47 -0
- package/test/index.test.js +13 -0
- package/test/manual-casty-without-qml.js +87 -0
- package/test/manual-flea-application.js +49 -0
- package/test/manual-flea-binary.js +62 -0
- package/test/manual-flea-qml.js +9 -0
- package/test/manual-flea-settings.js +40 -0
- package/test/manual-flea-theme.js +40 -0
- package/test/manual-qml-removal.js +46 -0
- package/test/manual-without-qml.js +45 -0
- package/test/qml-canvas-dom.test.js +37 -0
- package/test/qml-clipboard-bridge.test.js +25 -0
- package/test/qml-clipboard-wayland.test.js +40 -0
- package/test/qml-connections-timing.test.js +73 -0
- package/test/qml-dynamic.test.js +401 -0
- package/test/qml-flea-app.test.js +146 -0
- package/test/qml-flea-backend.test.js +78 -0
- package/test/qml-flea-columns-backend.test.js +38 -0
- package/test/qml-flea-compile.test.js +78 -0
- package/test/qml-flea-launcher.test.js +88 -0
- package/test/qml-flea-markdown-backend.test.js +31 -0
- package/test/qml-flea-media.test.js +28 -0
- package/test/qml-flea-navigation-backend.test.js +35 -0
- package/test/qml-flea-navigation-timing.test.js +147 -0
- package/test/qml-flea-preview-backend.test.js +53 -0
- package/test/qml-flea-previews.test.js +163 -0
- package/test/qml-flea-rows.test.js +101 -0
- package/test/qml-flea-textures.test.js +59 -0
- package/test/qml-flea-view-switch.test.js +33 -0
- package/test/qml-flea-wayland.test.js +68 -0
- package/test/qml-flea.test.js +34 -0
- package/test/qml-host.test.js +238 -0
- package/test/qml-imports.test.js +102 -0
- package/test/qml-inherited-handlers.test.js +51 -0
- package/test/qml-input-selection.test.js +66 -0
- package/test/qml-interactions.test.js +309 -0
- package/test/qml-language.test.js +167 -0
- package/test/qml-launcher.test.js +54 -0
- package/test/qml-layout-text.test.js +205 -0
- package/test/qml-media.test.js +63 -0
- package/test/qml-persisted-delegates.test.js +45 -0
- package/test/qml-quickshell-controls.test.js +65 -0
- package/test/qml-quickshell-settings.test.js +62 -0
- package/test/qml-repeater.test.js +72 -0
- package/test/qml-style-updates.test.js +124 -0
- package/test/qml-textures.test.js +66 -0
- package/test/qml-views.test.js +332 -0
- package/test/qml-worker.test.js +69 -0
- package/test/qml.test.js +83 -0
- package/test/raw-capture.test.js +152 -0
- package/test/renderer-host.test.js +11 -0
- package/test/text-input-pointer.test.js +31 -0
- package/test/text-input-scroll.test.js +29 -0
- package/test/text-input.test.js +47 -0
- package/test/wayland-frontend.test.js +83 -0
- package/test/wayland-urlbar.test.js +38 -0
- package/test/window-resize.test.js +36 -0
- package/tools/bench-raw-capture.js +32 -0
|
@@ -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);
|
|
@@ -0,0 +1,147 @@
|
|
|
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 {
|
|
5
|
+
mkdirSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
statSync,
|
|
9
|
+
unlinkSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { createBrowser } from '../lib/headless-shell.js';
|
|
12
|
+
import { fleaSandbox } from './helpers/flea-sandbox.js';
|
|
13
|
+
const root = process.env.BUNINU_FLEA_ROOT,
|
|
14
|
+
omarchy = process.env.BUNINU_OMARCHY_ROOT,
|
|
15
|
+
binary = process.env.FLEA_BIN;
|
|
16
|
+
test.skipIf(!root || !omarchy || !binary)(
|
|
17
|
+
'measure real Flea directory navigation and raw capture',
|
|
18
|
+
async () => {
|
|
19
|
+
const sandbox = fleaSandbox(root, omarchy),
|
|
20
|
+
oldPath = process.env.FLEA_PATH;
|
|
21
|
+
const home = sandbox.values.HOME,
|
|
22
|
+
folder = join(home, 'Documents');
|
|
23
|
+
mkdirSync(folder);
|
|
24
|
+
writeFileSync(join(home, 'hello.txt'), 'hello');
|
|
25
|
+
writeFileSync(join(folder, 'inside.txt'), 'inside');
|
|
26
|
+
process.env.FLEA_PATH = home;
|
|
27
|
+
let browser;
|
|
28
|
+
try {
|
|
29
|
+
browser = await createBrowser({
|
|
30
|
+
spareRenderer: false,
|
|
31
|
+
rendererTimeout: 30000,
|
|
32
|
+
});
|
|
33
|
+
await browser.context.addInitScript(
|
|
34
|
+
`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);};`,
|
|
35
|
+
);
|
|
36
|
+
if (process.env.BUNINU_QML_TIMING_TRACE)
|
|
37
|
+
await browser.context.addInitScript(`
|
|
38
|
+
globalThis.qmlTiming=[];
|
|
39
|
+
const stamp=(kind,detail)=>qmlTiming.push({at:Date.now(),kind,detail});
|
|
40
|
+
stamp('init',!!globalThis.__qmlHost);
|
|
41
|
+
const microtask=globalThis.queueMicrotask;
|
|
42
|
+
globalThis.queueMicrotask=fn=>microtask(()=>{const start=Date.now();try{return fn();}finally{const ms=Date.now()-start;if(ms>10)stamp('microtask',{ms,fn:String(fn).slice(0,180)});}});
|
|
43
|
+
const timeout=globalThis.setTimeout;
|
|
44
|
+
globalThis.setTimeout=(fn,delay,...args)=>timeout(()=>{const start=Date.now();try{return fn(...args);}finally{const ms=Date.now()-start;if(ms>10)stamp('timer',{ms,delay,fn:String(fn).slice(0,180)});}},delay);
|
|
45
|
+
|
|
46
|
+
if(globalThis.__qmlHost){
|
|
47
|
+
const host=globalThis.__qmlHost,run=host.run;
|
|
48
|
+
for(const name of ['compile','read','write']){
|
|
49
|
+
const original=host[name];if(!original)continue;
|
|
50
|
+
host[name]=(...args)=>{const start=Date.now();try{return original(...args);}finally{const ms=Date.now()-start;if(ms>10)stamp('host-'+name,{ms,path:args[0]});}};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
host.run=(command,callback,options={})=>{
|
|
54
|
+
const traced={...options};
|
|
55
|
+
for(const channel of ['stdout','stderr']) if(options[channel]) traced[channel]=chunk=>{
|
|
56
|
+
stamp(channel,String(chunk).slice(0,500));
|
|
57
|
+
const start=Date.now();try{return options[channel](chunk);}finally{stamp(channel+'-handled',Date.now()-start);}
|
|
58
|
+
};
|
|
59
|
+
const child=run.call(host,command,callback,traced),write=child.write;
|
|
60
|
+
if(write)child.write=text=>{stamp('stdin',String(text).slice(0,500));return write(text);};
|
|
61
|
+
return child;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
addEventListener('keydown',e=>stamp('keydown',e.key),true);
|
|
65
|
+
`);
|
|
66
|
+
await browser.context.navigate(
|
|
67
|
+
Bun.pathToFileURL(join(sandbox.flea, 'ui/boot/shell.qml')).href,
|
|
68
|
+
);
|
|
69
|
+
const text = () => browser.context.evaluate('document.body.innerText');
|
|
70
|
+
const until = async (predicate) => {
|
|
71
|
+
for (let n = 0; n < 150; n++) {
|
|
72
|
+
if (await predicate()) return;
|
|
73
|
+
await Bun.sleep(40);
|
|
74
|
+
}
|
|
75
|
+
throw Error('Flea did not settle: ' + (await text()));
|
|
76
|
+
};
|
|
77
|
+
await until(async () => (await text()).includes('hello.txt'));
|
|
78
|
+
const results = [];
|
|
79
|
+
for (let i = 0; i < 6; i++) {
|
|
80
|
+
const entering = i % 2 === 0,
|
|
81
|
+
key = entering ? 'ArrowRight' : 'ArrowLeft';
|
|
82
|
+
const start = performance.now();
|
|
83
|
+
await browser.context.press(key, { code: key });
|
|
84
|
+
const dispatch = performance.now();
|
|
85
|
+
for (let n = 0; n < 300; n++) {
|
|
86
|
+
const content = await text();
|
|
87
|
+
if (
|
|
88
|
+
entering
|
|
89
|
+
? content.includes('inside.txt') && !content.includes('hello.txt')
|
|
90
|
+
: content.includes('hello.txt') && !content.includes('inside.txt')
|
|
91
|
+
)
|
|
92
|
+
break;
|
|
93
|
+
if (n === 299) throw Error('Navigation timed out: ' + key);
|
|
94
|
+
await Bun.sleep(10);
|
|
95
|
+
}
|
|
96
|
+
const ready = performance.now();
|
|
97
|
+
const frame = JSON.parse(
|
|
98
|
+
await browser.context.screenshot({ format: 'bgra' }),
|
|
99
|
+
);
|
|
100
|
+
if (frame.path) unlinkSync(frame.path);
|
|
101
|
+
const painted = performance.now();
|
|
102
|
+
results.push({
|
|
103
|
+
key,
|
|
104
|
+
dispatchMs: dispatch - start,
|
|
105
|
+
contentMs: ready - start,
|
|
106
|
+
captureMs: painted - ready,
|
|
107
|
+
totalMs: painted - start,
|
|
108
|
+
});
|
|
109
|
+
await Bun.sleep(250);
|
|
110
|
+
}
|
|
111
|
+
const fsTimes = [];
|
|
112
|
+
for (let i = 0; i < 20; i++) {
|
|
113
|
+
const start = performance.now();
|
|
114
|
+
for (const dir of [home, folder])
|
|
115
|
+
for (const name of readdirSync(dir)) statSync(join(dir, name));
|
|
116
|
+
fsTimes.push(performance.now() - start);
|
|
117
|
+
}
|
|
118
|
+
const trace = process.env.BUNINU_QML_TIMING_TRACE
|
|
119
|
+
? JSON.parse(
|
|
120
|
+
await browser.context.evaluate('JSON.stringify(qmlTiming)'),
|
|
121
|
+
)
|
|
122
|
+
: [];
|
|
123
|
+
const report = {
|
|
124
|
+
viewport: '800x600',
|
|
125
|
+
results,
|
|
126
|
+
fsReadBothDirsMs: fsTimes,
|
|
127
|
+
trace,
|
|
128
|
+
};
|
|
129
|
+
await Bun.write(
|
|
130
|
+
'/tmp/flea-navigation-timing.json',
|
|
131
|
+
JSON.stringify(report, null, 2),
|
|
132
|
+
);
|
|
133
|
+
console.log(JSON.stringify(report));
|
|
134
|
+
// Wait through the upstream prebuild driver's first idle tick too.
|
|
135
|
+
await Bun.sleep(1500);
|
|
136
|
+
expect(await browser.context.evaluate('JSON.stringify(qmlErrors)')).toBe(
|
|
137
|
+
'[]',
|
|
138
|
+
);
|
|
139
|
+
} finally {
|
|
140
|
+
browser?.close();
|
|
141
|
+
if (oldPath === undefined) delete process.env.FLEA_PATH;
|
|
142
|
+
else process.env.FLEA_PATH = oldPath;
|
|
143
|
+
sandbox.close();
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
60000,
|
|
147
|
+
);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createBrowser } from '../lib/headless-shell.js';
|
|
4
|
+
import { encodeImage } from '../lib/paint/frame.js';
|
|
5
|
+
import { fleaSandbox } from './helpers/flea-sandbox.js';
|
|
6
|
+
const root=process.env.BUNINU_FLEA_ROOT,omarchy=process.env.BUNINU_OMARCHY_ROOT,binary=process.env.FLEA_BIN;
|
|
7
|
+
test.skipIf(!root||!omarchy||!binary)('official Flea backend opens and switches real text and image previews',async()=>{
|
|
8
|
+
const sandbox=fleaSandbox(root,omarchy),oldPath=process.env.FLEA_PATH;
|
|
9
|
+
let browser;
|
|
10
|
+
try {
|
|
11
|
+
process.env.FLEA_PATH=sandbox.values.HOME;
|
|
12
|
+
await Bun.write(join(sandbox.values.HOME,'first.txt'),'First real Flea preview\nDOM textures preserve the previous frame.\n'+'A long preview line.\n'.repeat(150));
|
|
13
|
+
await Bun.write(join(sandbox.values.HOME,'second.txt'),'Second real Flea preview\nThe official backend loaded this file.\n'+'Another long preview.\n'.repeat(170));
|
|
14
|
+
const rgba=new Uint8Array(320*160*4);
|
|
15
|
+
for(let y=0;y<160;y++)for(let x=0;x<320;x++)rgba.set(x<80?[0,100,255,255]:[255,0,0,255],(y*320+x)*4);
|
|
16
|
+
await Bun.write(join(sandbox.values.HOME,'third.png'),await encodeImage(rgba,320,160));
|
|
17
|
+
browser=await createBrowser({spareRenderer:false,rendererTimeout:20000});
|
|
18
|
+
await browser.context.resize(900,600);
|
|
19
|
+
await browser.context.addInitScript(`globalThis.qmlReaders=[];globalThis.qmlRows=[];let runtime;Object.defineProperty(globalThis,'__qmlRuntime',{get:()=>runtime,set:r=>{runtime=r;const describe=r.describe;r.describe=(v,t,...a)=>{if(t.includes('PreviewText'))qmlReaders.push(v);if(t.includes('Row'))qmlRows.push(v);return describe(v,t,...a)}}});globalThis.errors=[];addEventListener('error',e=>errors.push(String(e.error?.stack||e.message)));const old=console.error;console.error=(...a)=>{errors.push(a.map(String).join(' '));old(...a)};`);
|
|
20
|
+
await browser.context.navigate(Bun.pathToFileURL(join(sandbox.flea,'ui/boot/shell.qml')).href);
|
|
21
|
+
const run=code=>browser.context.evaluate(code);
|
|
22
|
+
async function until(fn){for(let i=0;i<120;i++){if(await fn())return;await Bun.sleep(30);}throw Error(await run('document.body.innerText+JSON.stringify(errors)'));}
|
|
23
|
+
await until(async()=>(await run('document.body.innerText')).includes('first.txt'));
|
|
24
|
+
const second=await run(`(()=>{const e=[...document.querySelectorAll('[data-qml-type=Text]')].find(e=>e.textContent==='second.txt'&&e.getBoundingClientRect().width>0);const r=e.getBoundingClientRect();return[r.x+r.width/2,r.y+r.height/2]})()`);
|
|
25
|
+
await browser.context.click(...second);await run('0');
|
|
26
|
+
expect(await run(`qmlRows.some(v=>v.row?.n==='second.txt'&&v.selected)`)).toBe(true);
|
|
27
|
+
const pt=await run(`(()=>{const e=[...document.querySelectorAll('[data-qml-type=Text]')].find(e=>e.textContent==='first.txt'&&e.getBoundingClientRect().width>0);if(!e)throw Error('row missing');const r=e.getBoundingClientRect();return [r.x+r.width/2,r.y+r.height/2]})()`);
|
|
28
|
+
await browser.context.click(...pt);
|
|
29
|
+
await browser.context.press(' ',{code:'Space'});
|
|
30
|
+
await until(async()=>(await run('document.body.innerText')).includes('First real Flea preview'));
|
|
31
|
+
await until(async()=>!(await run('document.body.innerText')).includes('reading'));
|
|
32
|
+
await Bun.write('/tmp/flea-real-preview-first.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
|
|
33
|
+
await run(`qmlReaders.find(r=>r.active&&r.list).list.contentY=400`);await run('0');
|
|
34
|
+
expect(await run(`qmlReaders.find(r=>r.active&&r.list).list.contentY`)).toBeGreaterThan(0);
|
|
35
|
+
await browser.context.press('ArrowDown',{code:'ArrowDown'});
|
|
36
|
+
await until(async()=>(await run('document.body.innerText')).includes('Second real Flea preview'));
|
|
37
|
+
await until(async()=>await run(`qmlReaders.some(r=>r.active&&r.path.endsWith('/second.txt')&&r.list&&r.list.contentY===0)`));
|
|
38
|
+
expect(await run(`qmlReaders.find(r=>r.active&&r.path.endsWith('/second.txt')).list.contentY`)).toBe(0);
|
|
39
|
+
await Bun.sleep(100);
|
|
40
|
+
await Bun.write('/tmp/flea-real-preview-second.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
|
|
41
|
+
await browser.context.press('ArrowDown',{code:'ArrowDown'});
|
|
42
|
+
await until(async()=>await run(`[...document.querySelectorAll('img')].some(e=>e.src.endsWith('/third.png')&&e.complete&&e.naturalWidth===320)`));
|
|
43
|
+
await until(async()=>!(await run('document.body.innerText')).includes('reading'));
|
|
44
|
+
const png=Buffer.from(await browser.context.screenshot({format:'png'}),'base64');
|
|
45
|
+
await Bun.write('/tmp/flea-real-preview-image.png',png);
|
|
46
|
+
const {CanvasKit:kit}=await import('../lib/renderer/page-renderer.js').then(m=>m.loadGraphics());
|
|
47
|
+
const image=kit.MakeImageFromEncoded(png);
|
|
48
|
+
try { expect([...image.readPixels(450,300,{width:1,height:1,colorType:kit.ColorType.RGBA_8888,alphaType:kit.AlphaType.Unpremul,colorSpace:kit.ColorSpace.SRGB})]).toEqual([255,0,0,255]); }
|
|
49
|
+
finally { image.delete(); }
|
|
50
|
+
expect(await run('errors')).toEqual([]);
|
|
51
|
+
await browser.context.press('Escape',{code:'Escape'});
|
|
52
|
+
}finally{browser?.close();if(oldPath===undefined)delete process.env.FLEA_PATH;else process.env.FLEA_PATH=oldPath;sandbox.close();}
|
|
53
|
+
},45000);
|