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,66 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { createBrowser } from '../lib/headless-shell.js';
|
|
4
|
+
import { loadGraphics } from '../lib/renderer/page-renderer.js';
|
|
5
|
+
|
|
6
|
+
test('ShaderEffectSource freezes, updates, hides sources and releases Loader references', async () => {
|
|
7
|
+
const dir = mkdtempSync('/tmp/qml-textures-');
|
|
8
|
+
const browser = await createBrowser({ spareRenderer: false });
|
|
9
|
+
const { CanvasKit: kit } = await loadGraphics();
|
|
10
|
+
async function pixel(x,y) {
|
|
11
|
+
const image=kit.MakeImageFromEncoded(Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
|
|
12
|
+
try { return [...image.readPixels(x,y,{width:1,height:1,colorType:kit.ColorType.RGBA_8888,alphaType:kit.AlphaType.Unpremul,colorSpace:kit.ColorSpace.SRGB})]; }
|
|
13
|
+
finally { image.delete(); }
|
|
14
|
+
}
|
|
15
|
+
const run = code => browser.context.evaluate(code);
|
|
16
|
+
async function until(fn) { for(let i=0;i<50;i++){if(await fn())return;await Bun.sleep(20);}throw Error(await run('document.body.innerText+JSON.stringify(errors)')); }
|
|
17
|
+
try {
|
|
18
|
+
await Bun.write(dir+'/main.qml', `import QtQuick
|
|
19
|
+
Rectangle {width:240;height:160;color:"white"
|
|
20
|
+
property int frames:0
|
|
21
|
+
Rectangle {id:source;width:60;height:40;color:"red"}
|
|
22
|
+
Loader {id:layer;x:80;width:60;height:40;active:true
|
|
23
|
+
sourceComponent:ShaderEffectSource {sourceItem:source;hideSource:true;live:false;onScheduledUpdateCompleted:frames+=1}}
|
|
24
|
+
Loader {id:mirror;x:160;width:60;height:40;active:false;sourceComponent:ShaderEffectSource {sourceItem:source;hideSource:true;live:false}}
|
|
25
|
+
Text {id:status;y:120;text:frames+":"+layer.active}
|
|
26
|
+
Button {id:toggle;x:210;y:80;text:"hide";onClicked:layer.item.hideSource=!layer.item.hideSource}
|
|
27
|
+
Button {id:blue;y:80;text:"blue";onClicked:source.color=source.color==="blue"?"red":"blue"}
|
|
28
|
+
Button {id:update;x:70;y:80;text:"update";onClicked:layer.item.scheduleUpdate()}
|
|
29
|
+
Button {id:live;x:140;y:80;text:"live";onClicked:layer.item.live=true}
|
|
30
|
+
Button {id:mirrorOn;y:140;text:"mirror";onClicked:mirror.active=true}
|
|
31
|
+
Button {id:clear;x:70;y:140;text:"clear";onClicked:mirror.item.sourceItem=null}
|
|
32
|
+
Button {id:release;x:140;y:120;text:"release";onClicked:layer.active=false}
|
|
33
|
+
}`);
|
|
34
|
+
await browser.context.addInitScript('globalThis.errors=[];addEventListener("error",e=>errors.push(String(e.error?.stack||e.message)))');
|
|
35
|
+
await browser.context.navigate(Bun.pathToFileURL(dir+'/main.qml').href);
|
|
36
|
+
await until(async()=> (await run('document.getElementById("status")?.textContent'))?.startsWith('1:'));
|
|
37
|
+
expect(await pixel(5,5)).toEqual([255,255,255,255]);
|
|
38
|
+
expect(await pixel(85,5)).toEqual([255,0,0,255]);
|
|
39
|
+
await run('document.getElementById("toggle").click()');await Bun.sleep(30);
|
|
40
|
+
expect(await pixel(5,5)).toEqual([255,0,0,255]);
|
|
41
|
+
await run('document.getElementById("toggle").click()');await Bun.sleep(30);
|
|
42
|
+
expect(await pixel(5,5)).toEqual([255,255,255,255]);
|
|
43
|
+
await run('document.getElementById("blue").click()');await Bun.sleep(50);
|
|
44
|
+
expect(await pixel(85,5)).toEqual([255,0,0,255]);
|
|
45
|
+
await run('document.getElementById("update").click()');
|
|
46
|
+
await until(async()=> (await run('document.getElementById("status").textContent')).startsWith('2:'));
|
|
47
|
+
expect(await pixel(85,5)).toEqual([0,0,255,255]);
|
|
48
|
+
await run('document.getElementById("live").click()');
|
|
49
|
+
await until(async()=> (await run('document.getElementById("status").textContent')).startsWith('3:'));
|
|
50
|
+
await run('document.getElementById("blue").click()');
|
|
51
|
+
await until(async()=> (await run('document.getElementById("status").textContent')).startsWith('4:'));
|
|
52
|
+
expect(await pixel(85,5)).toEqual([255,0,0,255]);
|
|
53
|
+
await Bun.sleep(120);
|
|
54
|
+
expect(await run('document.getElementById("status").textContent')).toBe('4:true');
|
|
55
|
+
await run('document.getElementById("mirrorOn").click()');
|
|
56
|
+
await until(async()=> (await pixel(165,5))[1]===0);
|
|
57
|
+
await run('document.getElementById("release").click()');await Bun.sleep(30);
|
|
58
|
+
expect(await pixel(5,5)).toEqual([255,255,255,255]);
|
|
59
|
+
expect(await pixel(165,5)).toEqual([255,0,0,255]);
|
|
60
|
+
await run('document.getElementById("clear").click()');await Bun.sleep(30);
|
|
61
|
+
expect(await pixel(165,5)).toEqual([255,255,255,255]);
|
|
62
|
+
expect(await pixel(5,5)).toEqual([255,0,0,255]);
|
|
63
|
+
expect(await pixel(85,5)).toEqual([255,255,255,255]);
|
|
64
|
+
expect(await run('errors')).toEqual([]);
|
|
65
|
+
} finally { browser.close();rmSync(dir,{recursive:true,force:true}); }
|
|
66
|
+
},30000);
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { RendererHost } from '../lib/renderer/host.js';
|
|
3
|
+
const wait = async (fn, predicate) => {
|
|
4
|
+
let value;
|
|
5
|
+
for (let i = 0; i < 80; i++) {
|
|
6
|
+
value = await fn();
|
|
7
|
+
if (predicate(value)) return value;
|
|
8
|
+
await Bun.sleep(15);
|
|
9
|
+
}
|
|
10
|
+
throw Error('View did not settle: ' + JSON.stringify(value));
|
|
11
|
+
};
|
|
12
|
+
test('ListView virtualizes a large DelegateModel, scrolls, updates count and reuses delegates', async () => {
|
|
13
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
14
|
+
try {
|
|
15
|
+
await host.start();
|
|
16
|
+
await host.call('loadDocument', {
|
|
17
|
+
url: 'data:text/x-qml,view',
|
|
18
|
+
contentType: 'text/x-qml',
|
|
19
|
+
source: `
|
|
20
|
+
import QtQuick
|
|
21
|
+
import QtQml.Models
|
|
22
|
+
Item {
|
|
23
|
+
id:root;width:300;height:200
|
|
24
|
+
ListView {id:view;width:200;height:100;reuseItems:true
|
|
25
|
+
model:DelegateModel {id:rows;model:100000000;filterOnGroup:"shown";groups:DelegateModelGroup {name:"shown";includeByDefault:false}
|
|
26
|
+
Component.onCompleted:items.addGroups(0,2000,"shown")
|
|
27
|
+
}
|
|
28
|
+
delegate:Text {required property int index;text:"row "+index;height:20;width:200}
|
|
29
|
+
}
|
|
30
|
+
Text {id:count;y:120;text:view.count}
|
|
31
|
+
Button {id:scroll;y:150;onClicked:view.positionViewAtIndex(1000,ListView.Beginning)}
|
|
32
|
+
Button {id:shrink;x:100;y:150;onClicked:{rows.groups.removeGroups(5,1995,"shown");view.positionViewAtBeginning()}}
|
|
33
|
+
}`,
|
|
34
|
+
});
|
|
35
|
+
await host.call('whenLoaded');
|
|
36
|
+
const read = async () =>
|
|
37
|
+
JSON.parse(
|
|
38
|
+
await host.call(
|
|
39
|
+
'evaluate',
|
|
40
|
+
'JSON.stringify({count:document.getElementById("count").textContent,text:document.body.innerText,n:document.querySelectorAll("[data-qml-type=Text]").length})',
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
await wait(read, (v) => v.count === '2000');
|
|
44
|
+
let result = await read();
|
|
45
|
+
expect(result.n).toBeLessThan(20);
|
|
46
|
+
expect(result.text).toContain('row 0');
|
|
47
|
+
await host.call('evaluate', 'document.getElementById("scroll").click()');
|
|
48
|
+
result = await wait(read, (v) => v.text.includes('row 1000'));
|
|
49
|
+
expect(result.n).toBeLessThan(25);
|
|
50
|
+
await host.call('evaluate', 'document.getElementById("shrink").click()');
|
|
51
|
+
result = await wait(
|
|
52
|
+
read,
|
|
53
|
+
(v) => v.count === '5' && v.text.includes('row 0'),
|
|
54
|
+
);
|
|
55
|
+
expect(result.n).toBeLessThan(25);
|
|
56
|
+
} finally {
|
|
57
|
+
host.close();
|
|
58
|
+
}
|
|
59
|
+
}, 30000);
|
|
60
|
+
|
|
61
|
+
test('Flickable routes default children to contentItem and responds to wheel without moving explicit overlays', async () => {
|
|
62
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
63
|
+
try {
|
|
64
|
+
await host.start();
|
|
65
|
+
await host.call('loadDocument', {
|
|
66
|
+
url: 'data:text/x-qml,flick',
|
|
67
|
+
contentType: 'text/x-qml',
|
|
68
|
+
source: `
|
|
69
|
+
import QtQuick
|
|
70
|
+
Flickable {id:root;width:200;height:100;contentWidth:200;contentHeight:500;clip:true
|
|
71
|
+
Rectangle {id:body;width:parent.width;height:500;color:"red"}
|
|
72
|
+
Text {id:overlay;parent:root;text:"fixed"}
|
|
73
|
+
Text {id:status;parent:root;y:20;text:root.contentY+":"+body.parent.height}
|
|
74
|
+
}`,
|
|
75
|
+
});
|
|
76
|
+
await host.call('whenLoaded');
|
|
77
|
+
expect(
|
|
78
|
+
await host.call(
|
|
79
|
+
'evaluate',
|
|
80
|
+
'document.getElementById("body").parentElement.hasAttribute("data-qml-content")',
|
|
81
|
+
),
|
|
82
|
+
).toBe(true);
|
|
83
|
+
expect(
|
|
84
|
+
await host.call(
|
|
85
|
+
'evaluate',
|
|
86
|
+
'document.getElementById("overlay").parentElement.id',
|
|
87
|
+
),
|
|
88
|
+
).toBe('root');
|
|
89
|
+
await host.call(
|
|
90
|
+
'evaluate',
|
|
91
|
+
'document.getElementById("root").dispatchEvent(new WheelEvent("wheel",{deltaY:50,bubbles:true,cancelable:true}))',
|
|
92
|
+
);
|
|
93
|
+
await wait(
|
|
94
|
+
() =>
|
|
95
|
+
host.call('evaluate', 'document.getElementById("status").textContent'),
|
|
96
|
+
(v) => v === '50:500',
|
|
97
|
+
);
|
|
98
|
+
expect(
|
|
99
|
+
await host.call(
|
|
100
|
+
'evaluate',
|
|
101
|
+
'document.getElementById("body").parentElement.style.top',
|
|
102
|
+
),
|
|
103
|
+
).toBe('-50px');
|
|
104
|
+
} finally {
|
|
105
|
+
host.close();
|
|
106
|
+
}
|
|
107
|
+
}, 30000);
|
|
108
|
+
|
|
109
|
+
test('empty Loaders, decoration Items and DropAreas do not swallow pointer activation', async () => {
|
|
110
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
111
|
+
try {
|
|
112
|
+
await host.start();
|
|
113
|
+
await host.call('loadDocument', {
|
|
114
|
+
url: 'data:text/x-qml,pointer',
|
|
115
|
+
contentType: 'text/x-qml',
|
|
116
|
+
source: `
|
|
117
|
+
import QtQuick
|
|
118
|
+
Item {width:200;height:100;property int clicks:0
|
|
119
|
+
Rectangle {width:80;height:40;color:"red";TapHandler {onTapped:clicks++}}
|
|
120
|
+
DropArea {anchors.fill:parent}
|
|
121
|
+
Loader {anchors.fill:parent;active:false;z:3}
|
|
122
|
+
Item {anchors.fill:parent;z:4}
|
|
123
|
+
Text {id:status;y:50;text:clicks}
|
|
124
|
+
}`,
|
|
125
|
+
});
|
|
126
|
+
await host.call('whenLoaded');
|
|
127
|
+
await host.call('click', 20, 20);
|
|
128
|
+
await wait(
|
|
129
|
+
() =>
|
|
130
|
+
host.call('evaluate', 'document.getElementById("status").textContent'),
|
|
131
|
+
(v) => v === '1',
|
|
132
|
+
);
|
|
133
|
+
} finally {
|
|
134
|
+
host.close();
|
|
135
|
+
}
|
|
136
|
+
}, 30000);
|
|
137
|
+
|
|
138
|
+
test('XmlListModel reads XBEL attributes through Qt role indices and reports unsupported queries', async () => {
|
|
139
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
140
|
+
try {
|
|
141
|
+
await host.start();
|
|
142
|
+
await host.call('loadDocument', {
|
|
143
|
+
url: 'data:text/x-qml,xml',
|
|
144
|
+
contentType: 'text/x-qml',
|
|
145
|
+
source: `
|
|
146
|
+
import QtQuick
|
|
147
|
+
import QtQml.XmlListModel
|
|
148
|
+
Item {
|
|
149
|
+
XmlListModel {id:model;query:"/xbel/bookmark";xml:"<xbel><bookmark href='file:///tmp/a' visited='2026-10-22'/><bookmark href='file:///tmp/b'/></xbel>"
|
|
150
|
+
XmlListModelRole {name:"href";attributeName:"href"}
|
|
151
|
+
XmlListModelRole {name:"visited";attributeName:"visited"}
|
|
152
|
+
}
|
|
153
|
+
Text {id:status;text:model.status+":"+model.count+":"+model.data(model.index(1,0),Qt.UserRole)}
|
|
154
|
+
Button {id:bad;onClicked:model.query="//bookmark[@bad]"}
|
|
155
|
+
}`,
|
|
156
|
+
});
|
|
157
|
+
await host.call('whenLoaded');
|
|
158
|
+
await wait(
|
|
159
|
+
() =>
|
|
160
|
+
host.call('evaluate', 'document.getElementById("status").textContent'),
|
|
161
|
+
(v) => v === '1:2:file:///tmp/b',
|
|
162
|
+
);
|
|
163
|
+
await host.call('evaluate', 'document.getElementById("bad").click()');
|
|
164
|
+
await wait(
|
|
165
|
+
() =>
|
|
166
|
+
host.call('evaluate', 'document.getElementById("status").textContent'),
|
|
167
|
+
(v) => v.startsWith('3:0:'),
|
|
168
|
+
);
|
|
169
|
+
} finally {
|
|
170
|
+
host.close();
|
|
171
|
+
}
|
|
172
|
+
}, 30000);
|
|
173
|
+
|
|
174
|
+
test('unchanged Flea StableModel resizes a virtualized view synchronously and warms reusable rows', async () => {
|
|
175
|
+
const { mkdtempSync, rmSync } = await import('node:fs');
|
|
176
|
+
const dir = mkdtempSync('/tmp/qml-stable-model-');
|
|
177
|
+
const ui = Bun.fileURLToPath(
|
|
178
|
+
new URL('./fixtures/qml/flea-theme/ui/', import.meta.url),
|
|
179
|
+
);
|
|
180
|
+
await Bun.write(
|
|
181
|
+
dir + '/main.qml',
|
|
182
|
+
`import QtQuick
|
|
183
|
+
import ${JSON.stringify(ui)} as Flea
|
|
184
|
+
Item {width:300;height:200;property int total:100
|
|
185
|
+
ListView {id:view;width:200;height:100;reuseItems:true
|
|
186
|
+
model:Flea.StableModel {id:stable;view:view;total:parent.parent.total;warm:true;warmRows:20}
|
|
187
|
+
delegate:Text {required property int index;width:200;height:20;text:"row "+index}
|
|
188
|
+
}
|
|
189
|
+
Text {id:status;y:120;text:view.count+":"+view.currentIndex}
|
|
190
|
+
Button {id:grow;y:150;onClicked:{total=105;stable.resize()}}
|
|
191
|
+
Button {id:scroll;x:80;y:150;onClicked:view.positionViewAtIndex(50,ListView.Beginning)}
|
|
192
|
+
}`,
|
|
193
|
+
);
|
|
194
|
+
const { createBrowser } = await import('../lib/headless-shell.js');
|
|
195
|
+
const browser = await createBrowser({ spareRenderer: false });
|
|
196
|
+
try {
|
|
197
|
+
await browser.context.navigate(Bun.pathToFileURL(dir + '/main.qml').href);
|
|
198
|
+
await wait(
|
|
199
|
+
() =>
|
|
200
|
+
browser.context.evaluate(
|
|
201
|
+
'document.getElementById("status").textContent',
|
|
202
|
+
),
|
|
203
|
+
(v) => v === '100:0',
|
|
204
|
+
);
|
|
205
|
+
await browser.context.evaluate('document.getElementById("scroll").click()');
|
|
206
|
+
await wait(
|
|
207
|
+
() => browser.context.evaluate('document.body.innerText'),
|
|
208
|
+
(v) => v.includes('row 50'),
|
|
209
|
+
);
|
|
210
|
+
await browser.context.evaluate('document.getElementById("grow").click()');
|
|
211
|
+
await wait(
|
|
212
|
+
() =>
|
|
213
|
+
browser.context.evaluate(
|
|
214
|
+
'document.getElementById("status").textContent',
|
|
215
|
+
),
|
|
216
|
+
(v) => v === '105:0',
|
|
217
|
+
);
|
|
218
|
+
expect(
|
|
219
|
+
await browser.context.evaluate(
|
|
220
|
+
'document.querySelectorAll("[data-qml-type=Text]").length',
|
|
221
|
+
),
|
|
222
|
+
).toBeLessThan(30);
|
|
223
|
+
} finally {
|
|
224
|
+
browser.close();
|
|
225
|
+
rmSync(dir, { recursive: true, force: true });
|
|
226
|
+
}
|
|
227
|
+
}, 30000);
|
|
228
|
+
|
|
229
|
+
test('passive HoverHandler observes a decoration without intercepting the button beneath', async () => {
|
|
230
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
231
|
+
try {
|
|
232
|
+
await host.start();
|
|
233
|
+
await host.call('loadDocument', {
|
|
234
|
+
url: 'data:text/x-qml,hover',
|
|
235
|
+
contentType: 'text/x-qml',
|
|
236
|
+
source: `
|
|
237
|
+
import QtQuick
|
|
238
|
+
Item {width:200;height:120;property int clicks:0
|
|
239
|
+
Rectangle {width:80;height:40;color:"red";TapHandler {onTapped:clicks++}}
|
|
240
|
+
Item {width:80;height:40;z:5;HoverHandler {id:hover}}
|
|
241
|
+
Text {id:status;y:60;text:hover.hovered+":"+clicks}
|
|
242
|
+
}`,
|
|
243
|
+
});
|
|
244
|
+
await host.call('whenLoaded');
|
|
245
|
+
await host.call('move', 20, 20);
|
|
246
|
+
await host.call('click', 20, 20);
|
|
247
|
+
await wait(
|
|
248
|
+
() =>
|
|
249
|
+
host.call('evaluate', 'document.getElementById("status").textContent'),
|
|
250
|
+
(v) => v === 'true:1',
|
|
251
|
+
);
|
|
252
|
+
await host.call('move', 110, 80);
|
|
253
|
+
await wait(
|
|
254
|
+
() =>
|
|
255
|
+
host.call('evaluate', 'document.getElementById("status").textContent'),
|
|
256
|
+
(v) => v === 'false:1',
|
|
257
|
+
);
|
|
258
|
+
} finally {
|
|
259
|
+
host.close();
|
|
260
|
+
}
|
|
261
|
+
}, 30000);
|
|
262
|
+
|
|
263
|
+
test('turning reuseItems off and back on in one handler immediately drains parked delegates', async () => {
|
|
264
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
265
|
+
try {
|
|
266
|
+
await host.start();
|
|
267
|
+
await host.call('loadDocument', {
|
|
268
|
+
url: 'data:text/x-qml,pool',
|
|
269
|
+
contentType: 'text/x-qml',
|
|
270
|
+
source: `import QtQuick
|
|
271
|
+
Item {width:300;height:200
|
|
272
|
+
ListView {id:view;width:200;height:100;reuseItems:true;cacheBuffer:0;model:100
|
|
273
|
+
delegate:Text {required property int index;width:200;height:20;text:"row "+index}
|
|
274
|
+
}
|
|
275
|
+
Button {id:park;y:120;onClicked:{view.displayMarginEnd=300;view.forceLayout();view.displayMarginEnd=0;view.forceLayout()}}
|
|
276
|
+
Button {id:drain;x:80;y:120;onClicked:{view.reuseItems=false;view.reuseItems=true}}
|
|
277
|
+
}`,
|
|
278
|
+
});
|
|
279
|
+
await host.call('whenLoaded');
|
|
280
|
+
const count = () =>
|
|
281
|
+
host.call(
|
|
282
|
+
'evaluate',
|
|
283
|
+
'document.querySelectorAll("[data-qml-type=Text]").length',
|
|
284
|
+
);
|
|
285
|
+
await host.call('evaluate', 'document.getElementById("park").click()');
|
|
286
|
+
const parked = await count();
|
|
287
|
+
expect(parked).toBeGreaterThan(10);
|
|
288
|
+
await host.call('evaluate', 'document.getElementById("drain").click()');
|
|
289
|
+
expect(await count()).toBeLessThan(10);
|
|
290
|
+
} finally {
|
|
291
|
+
host.close();
|
|
292
|
+
}
|
|
293
|
+
}, 30000);
|
|
294
|
+
|
|
295
|
+
test('replacing a numeric ListView model resets its scroll before creating new delegates',async()=>{
|
|
296
|
+
const host=new RendererHost({timeout:20000});
|
|
297
|
+
try{
|
|
298
|
+
await host.start();await host.call('loadDocument',{url:'data:text/x-qml,reset',contentType:'text/x-qml',source:`import QtQuick
|
|
299
|
+
Item {width:300;height:200
|
|
300
|
+
ListView {id:view;width:200;height:100;model:100;delegate:Text {required property int index;text:"row "+index;width:200;height:20}}
|
|
301
|
+
Text {id:offset;y:110;text:String(view.contentY)+","+view.model+","+view.count}
|
|
302
|
+
Button {id:scroll;y:140;onClicked:view.positionViewAtIndex(50,ListView.Beginning)}
|
|
303
|
+
Button {id:replace;x:100;y:140;onClicked:view.model=80}
|
|
304
|
+
}`});await host.call('whenLoaded');
|
|
305
|
+
await host.call('evaluate','document.getElementById("scroll").click()');
|
|
306
|
+
await wait(()=>host.call('evaluate','document.getElementById("offset").textContent'),v=>Number(v.split(",")[0])>0);
|
|
307
|
+
await host.call('evaluate','document.getElementById("replace").click()');
|
|
308
|
+
await wait(()=>host.call('evaluate','document.getElementById("offset").textContent'),v=>Number(v.split(",")[0])===0);
|
|
309
|
+
expect(await host.call('evaluate','document.body.innerText')).toContain('row 0');
|
|
310
|
+
}finally{host.close();}
|
|
311
|
+
},30000);
|
|
312
|
+
|
|
313
|
+
test('ListView positions unequal delegates by measured extent and updates following rows when a later row grows',async()=>{
|
|
314
|
+
const host=new RendererHost({timeout:20000});
|
|
315
|
+
try{
|
|
316
|
+
await host.start();await host.call('loadDocument',{url:'data:text/x-qml,variable',contentType:'text/x-qml',source:`import QtQuick
|
|
317
|
+
Item {id:root;width:400;height:400;property int extra:0
|
|
318
|
+
ListView {id:view;width:200;height:300;spacing:5;model:[20,80,35,60]
|
|
319
|
+
delegate:Rectangle {required property int index;required property var modelData;width:200;height:modelData+(index===1?root.extra:0)}
|
|
320
|
+
}
|
|
321
|
+
Text {id:report;x:220}
|
|
322
|
+
Button {id:inspect;y:330;onClicked:report.text=JSON.stringify({rows:[0,1,2,3].map(i=>{var c=view.itemAtIndex(i);return c?[c.y,c.height]:null}),index:view.indexAt(10,110),gap:view.indexAt(10,22),content:view.contentHeight})}
|
|
323
|
+
Button {id:grow;x:100;y:330;onClicked:root.extra=50}
|
|
324
|
+
}`});await host.call('whenLoaded');
|
|
325
|
+
const read=async()=>{await host.call('evaluate','document.getElementById("inspect").click()');return JSON.parse(await host.call('evaluate','document.getElementById("report").textContent'));};
|
|
326
|
+
let state=await wait(read,s=>s.rows[3]?.[0]===150);
|
|
327
|
+
expect(state.rows).toEqual([[0,20],[25,80],[110,35],[150,60]]);expect(state.index).toBe(2);expect(state.gap).toBe(-1);
|
|
328
|
+
await host.call('evaluate','document.getElementById("grow").click()');
|
|
329
|
+
state=await wait(read,s=>s.rows[3]?.[0]===200);
|
|
330
|
+
expect(state.rows).toEqual([[0,20],[25,130],[160,35],[200,60]]);expect(state.content).toBe(265);
|
|
331
|
+
}finally{host.close();}
|
|
332
|
+
},30000);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { createBrowser } from '../lib/headless-shell.js';
|
|
5
|
+
async function run(files, source, verify) {
|
|
6
|
+
const dir = mkdtempSync('/tmp/qml-worker-');
|
|
7
|
+
let browser;
|
|
8
|
+
try {
|
|
9
|
+
for (const [name, text] of Object.entries({ ...files, 'main.qml': source }))
|
|
10
|
+
await Bun.write(join(dir, name), text);
|
|
11
|
+
browser = await createBrowser({ spareRenderer: false });
|
|
12
|
+
await browser.context.navigate(
|
|
13
|
+
Bun.pathToFileURL(join(dir, 'main.qml')).href,
|
|
14
|
+
);
|
|
15
|
+
const read = () =>
|
|
16
|
+
browser.context.evaluate('document.getElementById("result").textContent');
|
|
17
|
+
let text;
|
|
18
|
+
for (let i = 0; i < 200; i++) {
|
|
19
|
+
text = await read();
|
|
20
|
+
if (text !== 'waiting') break;
|
|
21
|
+
await Bun.sleep(20);
|
|
22
|
+
}
|
|
23
|
+
await verify(text, browser);
|
|
24
|
+
} finally {
|
|
25
|
+
browser?.close();
|
|
26
|
+
rmSync(dir, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
test('WorkerScript includes shared script scope, clones messages and keeps independent worker state', async () => {
|
|
30
|
+
await run(
|
|
31
|
+
{
|
|
32
|
+
'worker.js':
|
|
33
|
+
'Qt.include("helper.js");var count=0;WorkerScript.onMessage=function(m){m.answer=twice(m.n)+ ++count;WorkerScript.sendMessage(m);};',
|
|
34
|
+
'helper.js': '.pragma library\nfunction twice(n){return n*2;}',
|
|
35
|
+
},
|
|
36
|
+
`import QtQuick
|
|
37
|
+
Text {id:result;text:"waiting";property var input:({n:4});property var replies:[]
|
|
38
|
+
WorkerScript {id:a;source:"worker.js";onMessage:function(m){result.replies=result.replies.concat([m.answer]);if(result.replies.length===2)result.text=JSON.stringify(result.replies)+":"+result.input.answer}}
|
|
39
|
+
WorkerScript {id:b;source:"worker.js";onMessage:function(m){result.replies=result.replies.concat([m.answer]);if(result.replies.length===2)result.text=JSON.stringify(result.replies)+":"+result.input.answer}}
|
|
40
|
+
Component.onCompleted:{a.sendMessage(input);b.sendMessage(input)}
|
|
41
|
+
}`,
|
|
42
|
+
(text) => expect(text).toBe('[9,9]:undefined'),
|
|
43
|
+
);
|
|
44
|
+
}, 30000);
|
|
45
|
+
test.skipIf(!process.env.BUNINU_FLEA_ROOT)(
|
|
46
|
+
'unchanged Flea MarkdownWorker parses headings and paragraphs in a real worker',
|
|
47
|
+
async () => {
|
|
48
|
+
const url = Bun.pathToFileURL(
|
|
49
|
+
resolve(process.env.BUNINU_FLEA_ROOT, 'ui/MarkdownWorker.js'),
|
|
50
|
+
).href;
|
|
51
|
+
await run(
|
|
52
|
+
{},
|
|
53
|
+
`import QtQuick
|
|
54
|
+
Text {id:result;text:"waiting"
|
|
55
|
+
WorkerScript {id:worker;source:${JSON.stringify(url)};onMessage:function(m){result.text=JSON.stringify(m)}}
|
|
56
|
+
Component.onCompleted:worker.sendMessage({seq:12,source:"# Hello\\n\\nA **bold** paragraph.",dir:"/tmp",chrome:"#000000",ink:"#ffffff"})
|
|
57
|
+
}`,
|
|
58
|
+
(text) => {
|
|
59
|
+
const result = JSON.parse(text);
|
|
60
|
+
expect(result.error).toBe('');
|
|
61
|
+
expect(result.seq).toBe(12);
|
|
62
|
+
expect(result.blocks.length).toBeGreaterThan(1);
|
|
63
|
+
expect(JSON.stringify(result.blocks)).toContain('Hello');
|
|
64
|
+
expect(JSON.stringify(result.blocks)).toContain('bold');
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
30000,
|
|
69
|
+
);
|
package/test/qml.test.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {parseQml} from '../lib/qml/parser.js';
|
|
3
|
+
import {createQmlDocument} from '../lib/qml/document.js';
|
|
4
|
+
import {isQmlDocument} from '../lib/qml/format.js';
|
|
5
|
+
import {displayDocument} from '../lib/document-types.js';
|
|
6
|
+
import {RendererHost} from '../lib/renderer/host.js';
|
|
7
|
+
const url='https://qml.test/hello.qml';
|
|
8
|
+
const source=await Bun.file(new URL('./fixtures/qml/hello.qml',import.meta.url)).text();
|
|
9
|
+
|
|
10
|
+
test('QML builds DOM directly, preserves source through response classification',()=>{
|
|
11
|
+
const response=displayDocument({url,contentType:'text/plain',body:new TextEncoder().encode(source)});
|
|
12
|
+
expect(response).toEqual({source,contentType:'text/x-qml'});
|
|
13
|
+
expect(isQmlDocument(url,'text/html')).toBe(false);
|
|
14
|
+
const {document}=createQmlDocument(source,url);
|
|
15
|
+
expect(document.querySelectorAll('[data-qml-node]').length).toBe(4);
|
|
16
|
+
expect(document.querySelector('#root')).not.toBeNull();
|
|
17
|
+
expect(document.querySelector('script').textContent).toContain('addEventListener');
|
|
18
|
+
});
|
|
19
|
+
test('unsupported imports/types are visible diagnostics and source is inert',()=>{
|
|
20
|
+
const {document}=createQmlDocument('import Evil\nRectangle { color: "<script>boom</script>" }',url);
|
|
21
|
+
expect(document.body.textContent).toContain('Unsupported QML import');
|
|
22
|
+
expect(document.querySelector('script')).toBeNull();
|
|
23
|
+
expect(()=>parseQml('Rectangle { width: }')).toThrow();
|
|
24
|
+
const disabled=createQmlDocument(source,url,{scripting:false}).document;
|
|
25
|
+
expect(disabled.querySelector('script')).toBeNull();
|
|
26
|
+
expect(disabled.body.textContent).toContain('requires page scripting');
|
|
27
|
+
});
|
|
28
|
+
test('Acorn handles modern JS expressions and braces inside strings',()=>{
|
|
29
|
+
const ast=parseQml('import QtQuick\nText { text: ({a: "}"})?.a ?? "fallback"; onClicked: { console.log("}"); } }');
|
|
30
|
+
expect(ast.root.properties.length).toBe(2);
|
|
31
|
+
});
|
|
32
|
+
test('QML uses normal realm, reactive DOM, input, viewport resize and navigation',async()=>{
|
|
33
|
+
const host=new RendererHost({timeout:20000});
|
|
34
|
+
try{
|
|
35
|
+
await host.start(); await host.call('resize',640,360,1);
|
|
36
|
+
await host.call('loadDocument',{url,source,contentType:'text/x-qml'}); await host.call('whenLoaded');
|
|
37
|
+
expect(await host.call('evaluate','document.title')).toBe('QML DOM experiment');
|
|
38
|
+
expect(await host.call('evaluate','document.getElementById("button").style.width')).toBe('320px');
|
|
39
|
+
expect(await host.call('evaluate','document.body.textContent.includes("Hello World! Clicks: 0")')).toBe(true);
|
|
40
|
+
await host.call('evaluate',`document.querySelector('[data-qml-node="3"]').click()`);
|
|
41
|
+
expect(await host.call('evaluate',`document.querySelector('[data-qml-node="2"]').textContent`)).toBe('Hello World! Clicks: 1');
|
|
42
|
+
await host.call('resize',800,400,1);
|
|
43
|
+
expect(await host.call('evaluate','document.getElementById("button").style.width')).toBe('400px');
|
|
44
|
+
await host.call('loadDocument',{url,contentType:'text/x-qml',source:`import QtQuick
|
|
45
|
+
Rectangle {
|
|
46
|
+
id: root
|
|
47
|
+
implicitWidth: 300
|
|
48
|
+
implicitHeight: 120
|
|
49
|
+
property int count: 1
|
|
50
|
+
Rectangle { id: child; anchors.fill: parent
|
|
51
|
+
Text { id: label; text: root.count + ":" + parent.width }
|
|
52
|
+
}
|
|
53
|
+
onClicked: { root.width = 222; root.implicitWidth = 500; root.count++; }
|
|
54
|
+
}`}); await host.call('whenLoaded');
|
|
55
|
+
expect(await host.call('evaluate','document.getElementById("label").textContent')).toBe('1:300');
|
|
56
|
+
await host.call('evaluate','document.getElementById("root").click()');
|
|
57
|
+
expect(await host.call('evaluate','document.getElementById("label").textContent')).toBe('2:222');
|
|
58
|
+
await host.call('loadDocument',{url:'https://qml.test/plain' ,source:'<p>ordinary HTML</p>',contentType:'text/html'});await host.call('whenLoaded');
|
|
59
|
+
expect(await host.call('evaluate','document.body.textContent')).toBe('ordinary HTML');
|
|
60
|
+
}finally{host.close();}
|
|
61
|
+
},30000);
|
|
62
|
+
|
|
63
|
+
test('Quickshell multiply fixture uses editable DOM controls and click handlers',async()=>{
|
|
64
|
+
const host=new RendererHost({timeout:20000});
|
|
65
|
+
const source=await Bun.file(new URL('./fixtures/qml/multiply.qml',import.meta.url)).text();
|
|
66
|
+
try{
|
|
67
|
+
await host.start();await host.call('resize',640,480,1);
|
|
68
|
+
await host.call('loadDocument',{url:'https://qml.test/multiply.qml',source,contentType:'text/x-qml'});await host.call('whenLoaded');
|
|
69
|
+
expect(await host.call('evaluate',"document.querySelectorAll('input').length")).toBe(3);
|
|
70
|
+
const point=async selector=>JSON.parse(await host.call('evaluate',`JSON.stringify((()=>{const r=document.querySelector(${JSON.stringify(selector)}).getBoundingClientRect();return [r.x+r.width/2,r.y+r.height/2]})())`));
|
|
71
|
+
const click=async selector=>host.call('click',...await point(selector));
|
|
72
|
+
const value=()=>host.call('evaluate',"document.getElementById('result').value");
|
|
73
|
+
await click('button');expect(await value()).toBe('42');
|
|
74
|
+
await click('#first');await host.call('press','a',{modifiers:2});await host.call('type','-2.5');
|
|
75
|
+
await click('#second');await host.call('press','a',{modifiers:2});await host.call('type','4');
|
|
76
|
+
await click('button');expect(await value()).toBe('-10');
|
|
77
|
+
await click('#result');await host.call('type','999');expect(await value()).toBe('-10');
|
|
78
|
+
await click('#first');await host.call('press','a',{modifiers:2});await host.call('press','Backspace');
|
|
79
|
+
await click('button');expect(await value()).toBe('Please enter two valid numbers');
|
|
80
|
+
const rects=JSON.parse(await host.call('evaluate',"JSON.stringify([...document.querySelectorAll('input,button')].map(e=>{const r=e.getBoundingClientRect();return [r.x,r.y,r.width,r.height]}))"));
|
|
81
|
+
for(let i=1;i<rects.length;i++)expect(rects[i][1]).toBeGreaterThanOrEqual(rects[i-1][1]+rects[i-1][3]);
|
|
82
|
+
}finally{host.close();}
|
|
83
|
+
},30000);
|