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,205 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { RendererHost } from '../lib/renderer/host.js';
|
|
3
|
+
async function fixture(source, run) {
|
|
4
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
5
|
+
try {
|
|
6
|
+
await host.start();
|
|
7
|
+
await host.call('loadDocument', {
|
|
8
|
+
url: 'data:text/x-qml,layout',
|
|
9
|
+
contentType: 'text/x-qml',
|
|
10
|
+
source,
|
|
11
|
+
});
|
|
12
|
+
await host.call('whenLoaded');
|
|
13
|
+
await run(host);
|
|
14
|
+
} finally {
|
|
15
|
+
host.close();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
test('Flow wraps dialog buttons and baseline anchors align different font sizes', async () => {
|
|
19
|
+
await fixture(
|
|
20
|
+
`import QtQuick
|
|
21
|
+
Item {width:400;height:300
|
|
22
|
+
Flow {id:flow;width:150;spacing:10
|
|
23
|
+
Rectangle {id:a;width:100;height:20}
|
|
24
|
+
Rectangle {id:b;width:80;height:30}
|
|
25
|
+
Rectangle {id:c;width:50;height:10}
|
|
26
|
+
}
|
|
27
|
+
Text {id:large;y:100;text:"Large";font.pixelSize:30}
|
|
28
|
+
Text {id:small;x:120;anchors.baseline:large.baseline;text:"Small";font.pixelSize:12}
|
|
29
|
+
Text {id:status;y:200;text:b.x+":"+b.y+":"+c.x+":"+c.y+":"+flow.height+":"+Math.round(small.y+small.baselineOffset-large.y-large.baselineOffset)}
|
|
30
|
+
}`,
|
|
31
|
+
async (h) => {
|
|
32
|
+
expect(
|
|
33
|
+
await h.call(
|
|
34
|
+
'evaluate',
|
|
35
|
+
'document.getElementById("status").textContent',
|
|
36
|
+
),
|
|
37
|
+
).toBe('0:30:90:30:60:0');
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
}, 30000);
|
|
41
|
+
test('RowLayout shrinks and fills bounded children without losing alignment', async () => {
|
|
42
|
+
await fixture(
|
|
43
|
+
`import QtQuick
|
|
44
|
+
import QtQuick.Layouts
|
|
45
|
+
Item {width:400;height:200
|
|
46
|
+
RowLayout {width:200;height:40;spacing:10
|
|
47
|
+
Rectangle {id:a;implicitWidth:40;implicitHeight:20;Layout.alignment:Qt.AlignVCenter}
|
|
48
|
+
Rectangle {id:b;implicitWidth:50;implicitHeight:10;Layout.fillWidth:true;Layout.maximumWidth:60;Layout.alignment:Qt.AlignBottom}
|
|
49
|
+
Rectangle {id:c;implicitWidth:10;implicitHeight:10;Layout.fillWidth:true;Layout.fillHeight:true}
|
|
50
|
+
}
|
|
51
|
+
Text {id:status;y:100;text:a.width+":"+a.y+":"+b.width+":"+b.y+":"+c.width+":"+c.height}
|
|
52
|
+
}`,
|
|
53
|
+
async (h) => {
|
|
54
|
+
expect(
|
|
55
|
+
await h.call(
|
|
56
|
+
'evaluate',
|
|
57
|
+
'document.getElementById("status").textContent',
|
|
58
|
+
),
|
|
59
|
+
).toBe('40:10:60:30:80:40');
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
}, 30000);
|
|
63
|
+
test('StyledText creates inert styled DOM, emits link activation and PlainText stays literal', async () => {
|
|
64
|
+
await fixture(
|
|
65
|
+
`import QtQuick
|
|
66
|
+
Item {id:root;width:400;height:200;property string link:""
|
|
67
|
+
Text {id:rich;width:300;height:40;textFormat:Text.StyledText;text:"<b>Bold</b> <a href='https://example.com'>link</a><script>globalThis.bad=true</script>";onLinkActivated:function(link){root.link=link}}
|
|
68
|
+
Text {id:plain;y:50;text:"<b>literal</b>";textFormat:Text.PlainText}
|
|
69
|
+
Text {id:status;y:100;text:root.link}
|
|
70
|
+
}`,
|
|
71
|
+
async (h) => {
|
|
72
|
+
expect(
|
|
73
|
+
await h.call(
|
|
74
|
+
'evaluate',
|
|
75
|
+
'document.getElementById("rich").querySelector("b").textContent',
|
|
76
|
+
),
|
|
77
|
+
).toBe('Bold');
|
|
78
|
+
expect(
|
|
79
|
+
await h.call(
|
|
80
|
+
'evaluate',
|
|
81
|
+
'document.getElementById("rich").querySelector("script")===null&&globalThis.bad===undefined',
|
|
82
|
+
),
|
|
83
|
+
).toBe(true);
|
|
84
|
+
expect(
|
|
85
|
+
await h.call(
|
|
86
|
+
'evaluate',
|
|
87
|
+
'document.getElementById("plain").textContent',
|
|
88
|
+
),
|
|
89
|
+
).toBe('<b>literal</b>');
|
|
90
|
+
await h.call('evaluate', 'document.querySelector("a").click()');
|
|
91
|
+
expect(
|
|
92
|
+
await h.call(
|
|
93
|
+
'evaluate',
|
|
94
|
+
'document.getElementById("status").textContent',
|
|
95
|
+
),
|
|
96
|
+
).toBe('https://example.com');
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
}, 30000);
|
|
100
|
+
|
|
101
|
+
test('ShapePath linear gradients draw through the existing page canvas while solid paths retain SVG', async () => {
|
|
102
|
+
await fixture(
|
|
103
|
+
`import QtQuick
|
|
104
|
+
import QtQuick.Shapes
|
|
105
|
+
Item {width:200;height:100
|
|
106
|
+
Shape {width:100;height:40
|
|
107
|
+
ShapePath {strokeWidth:0;fillGradient:LinearGradient {x1:0;y1:0;x2:100;y2:0
|
|
108
|
+
GradientStop {position:0;color:"red"} GradientStop {position:1;color:"blue"}}
|
|
109
|
+
PathSvg {path:"M0 0 H100 V40 H0 Z"}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
Shape {x:110;width:40;height:40;ShapePath {fillColor:"green";strokeWidth:0;PathSvg {path:"M0 0H40V40H0Z"}}}
|
|
113
|
+
}`,
|
|
114
|
+
async (h) => {
|
|
115
|
+
const pixels = JSON.parse(
|
|
116
|
+
await h.call(
|
|
117
|
+
'evaluate',
|
|
118
|
+
`(()=>{const c=document.querySelector('canvas[data-qml-gradient]').getContext('2d');return JSON.stringify([Array.from(c.getImageData(5,20,1,1).data),Array.from(c.getImageData(95,20,1,1).data)]);})()`,
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
expect(pixels[0][0]).toBeGreaterThan(220);
|
|
122
|
+
expect(pixels[0][2]).toBeLessThan(35);
|
|
123
|
+
expect(pixels[1][2]).toBeGreaterThan(220);
|
|
124
|
+
expect(
|
|
125
|
+
await h.call(
|
|
126
|
+
'evaluate',
|
|
127
|
+
'document.querySelectorAll("svg path").length',
|
|
128
|
+
),
|
|
129
|
+
).toBe(2);
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
}, 30000);
|
|
133
|
+
|
|
134
|
+
test('Text reports laid-out lineCount and keeps wrapped or explicit multiline captions when eliding', async () => {
|
|
135
|
+
await fixture(
|
|
136
|
+
`import QtQuick
|
|
137
|
+
Item {width:400;height:240
|
|
138
|
+
Text {id:empty;text:""}
|
|
139
|
+
Text {id:single;y:40;text:"one"}
|
|
140
|
+
Text {id:multi;y:80;text:"one\\ntwo"}
|
|
141
|
+
Text {id:clipped;y:140;text:"one\\ntwo\\nthree";maximumLineCount:2;elide:Text.ElideRight}
|
|
142
|
+
Text {id:wrapped;x:100;width:70;text:"one two three four five";wrapMode:Text.WordWrap;elide:Text.ElideRight}
|
|
143
|
+
Text {id:status;y:210;text:empty.implicitWidth+":"+empty.lineCount+":"+single.lineCount+":"+multi.lineCount+":"+clipped.lineCount+":"+clipped.truncated+":"+wrapped.truncated}
|
|
144
|
+
}`,
|
|
145
|
+
async (h) => {
|
|
146
|
+
expect(
|
|
147
|
+
await h.call(
|
|
148
|
+
'evaluate',
|
|
149
|
+
'document.getElementById("status").textContent',
|
|
150
|
+
),
|
|
151
|
+
).toBe('0:1:1:2:2:true:false');
|
|
152
|
+
expect(
|
|
153
|
+
await h.call(
|
|
154
|
+
'evaluate',
|
|
155
|
+
'document.getElementById("clipped").style.whiteSpace',
|
|
156
|
+
),
|
|
157
|
+
).toBe('pre');
|
|
158
|
+
expect(
|
|
159
|
+
await h.call(
|
|
160
|
+
'evaluate',
|
|
161
|
+
'document.getElementById("wrapped").style.whiteSpace',
|
|
162
|
+
),
|
|
163
|
+
).toBe('pre-wrap');
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
}, 30000);
|
|
167
|
+
|
|
168
|
+
test('Text defaults to AutoText, recognizes multi-letter tags and preserves plain trailing newlines', async () => {
|
|
169
|
+
await fixture(
|
|
170
|
+
`import QtQuick
|
|
171
|
+
Item {width:300;height:200
|
|
172
|
+
Text {id:auto;text:"<strong>Bold</strong><br/>next"}
|
|
173
|
+
Text {id:plain;y:60;text:"line\\n"}
|
|
174
|
+
Text {id:status;y:120;text:plain.lineCount}
|
|
175
|
+
}`,
|
|
176
|
+
async (h) => {
|
|
177
|
+
expect(
|
|
178
|
+
await h.call(
|
|
179
|
+
'evaluate',
|
|
180
|
+
'document.getElementById("auto").querySelector("strong").textContent',
|
|
181
|
+
),
|
|
182
|
+
).toBe('Bold');
|
|
183
|
+
expect(
|
|
184
|
+
await h.call('evaluate', 'document.getElementById("auto").textContent'),
|
|
185
|
+
).toBe('Boldnext');
|
|
186
|
+
expect(
|
|
187
|
+
await h.call(
|
|
188
|
+
'evaluate',
|
|
189
|
+
'document.getElementById("status").textContent',
|
|
190
|
+
),
|
|
191
|
+
).toBe('2');
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
}, 30000);
|
|
195
|
+
|
|
196
|
+
test('markup formatting newlines after blocks do not add phantom text lines',async()=>{
|
|
197
|
+
await fixture(`import QtQuick
|
|
198
|
+
Item {width:400;height:200
|
|
199
|
+
Text {id:rich;width:300;text:"<p>one</p>\\n<p>two</p>\\n";textFormat:Text.RichText}
|
|
200
|
+
Text {id:plain;y:80;text:"one\\ntwo\\n";textFormat:Text.PlainText}
|
|
201
|
+
Text {id:status;y:150;text:rich.lineCount+":"+plain.lineCount}
|
|
202
|
+
}`,async host=>{
|
|
203
|
+
expect(await host.call('evaluate','document.getElementById("status").textContent')).toBe('2:3');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {mkdtempSync,rmSync} from 'node:fs';
|
|
3
|
+
import {join} from 'node:path';
|
|
4
|
+
import {createBrowser} from '../lib/headless-shell.js';
|
|
5
|
+
import {applyQmlMediaGain} from '../lib/qml/media-renderer.js';
|
|
6
|
+
test('QML MP4/MP3 reuse HTML decoding, metadata, pause and millisecond seek',async()=>{
|
|
7
|
+
const dir=mkdtempSync('/tmp/qml-media-');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
|
+
await Bun.write(join(dir,'main.qml'),`import QtQuick
|
|
12
|
+
import QtMultimedia
|
|
13
|
+
import Quickshell.Wayland
|
|
14
|
+
Rectangle {width:320;height:240;color:"black"
|
|
15
|
+
AudioOutput {id:out;volume:0;muted:true}
|
|
16
|
+
VideoOutput {id:screen;anchors.fill:parent}
|
|
17
|
+
MediaPlayer {id:player;source:${JSON.stringify(video)};audioOutput:out;videoOutput:screen}
|
|
18
|
+
IdleInhibitor {enabled:true}
|
|
19
|
+
Text {id:status;text:player.mediaStatus+":"+player.duration+":"+player.position+":"+player.error}
|
|
20
|
+
Button {id:play;onClicked:player.play()}
|
|
21
|
+
Button {id:pause;onClicked:player.pause()}
|
|
22
|
+
Button {id:seek;onClicked:player.position=500}
|
|
23
|
+
Button {id:switchAudio;onClicked:player.source=${JSON.stringify(audio)}}
|
|
24
|
+
}`);
|
|
25
|
+
browser=await createBrowser({spareRenderer:false});
|
|
26
|
+
await browser.context.addInitScript(`globalThis.mediaNodes=[];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')||t.includes('VideoOutput'))mediaNodes.push({v,t});return d(v,t,...a)}}});`);
|
|
27
|
+
await browser.context.navigate(Bun.pathToFileURL(join(dir,'main.qml')).href);
|
|
28
|
+
const run=s=>browser.context.evaluate(s);
|
|
29
|
+
|
|
30
|
+
const status=()=>run("(()=>{const p=mediaNodes.find(n=>n.t.includes('MediaPlayer')).v;return {duration:p.duration,position:p.position,error:p.error,message:p.errorString,state:p.playbackState,ready:p.mediaStatus,hasVideo:p.hasVideo,hasAudio:p.hasAudio}})()");
|
|
31
|
+
async function until(fn){for(let i=0;i<200;i++){const s=await status();if(s.error)throw Error(JSON.stringify(s));if(fn(s))return s;await Bun.sleep(25)}throw Error(JSON.stringify(await status()));}
|
|
32
|
+
expect((await until(s=>s.duration>1000)).hasVideo).toBe(true);
|
|
33
|
+
await run("document.getElementById('play').click()");
|
|
34
|
+
await until(s=>s.state===1&&s.position>100);
|
|
35
|
+
await run("document.getElementById('pause').click()");
|
|
36
|
+
expect((await status()).state).toBe(2);
|
|
37
|
+
await run("document.getElementById('seek').click()");
|
|
38
|
+
await until(s=>Math.abs(s.position-500)<50);
|
|
39
|
+
const png=await browser.context.screenshot({format:'png'});
|
|
40
|
+
await Bun.write('/tmp/qml-media-video.png',Buffer.from(png,'base64'));
|
|
41
|
+
expect(await run("mediaNodes.find(n=>n.t.includes('VideoOutput')).v.contentRect.width")).toBeGreaterThan(0);
|
|
42
|
+
await run("document.getElementById('switchAudio').click()");
|
|
43
|
+
const s=await until(s=>s.hasAudio&&!s.hasVideo&&s.duration>500);
|
|
44
|
+
expect(s.error).toBe(0);
|
|
45
|
+
await run("document.getElementById('seek').click()");
|
|
46
|
+
await until(s=>Math.abs(s.position-500)<50);
|
|
47
|
+
}finally{browser?.close();rmSync(dir,{recursive:true,force:true})}
|
|
48
|
+
},30000);
|
|
49
|
+
|
|
50
|
+
test('QML PCM gain scales the output chunk without requiring a new audio backend',()=>{
|
|
51
|
+
const pcm=Buffer.alloc(6);pcm.writeInt16LE(-2000,0);pcm.writeInt16LE(1000,2);pcm.writeInt16LE(32767,4);
|
|
52
|
+
applyQmlMediaGain(pcm,0.5);
|
|
53
|
+
expect([pcm.readInt16LE(0),pcm.readInt16LE(2),pcm.readInt16LE(4)]).toEqual([-1000,500,16384]);
|
|
54
|
+
applyQmlMediaGain(pcm,0);expect([...pcm]).toEqual([0,0,0,0,0,0]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
test('QML MP3 playback sends real PCM, mutes and releases the shared audio output',async()=>{
|
|
59
|
+
const child=Bun.spawn([process.execPath,new URL('./fixtures/qml-media-audio.js',import.meta.url).pathname],{stdout:'pipe',stderr:'pipe'});
|
|
60
|
+
const [out,error,code]=await Promise.all([new Response(child.stdout).text(),new Response(child.stderr).text(),child.exited]);
|
|
61
|
+
expect({code,error}).toEqual({code:0,error:''});
|
|
62
|
+
expect(out).toContain('QML real MP3 PCM playback passed');
|
|
63
|
+
},30000);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { RendererHost } from '../lib/renderer/host.js';
|
|
3
|
+
test('created delegates expose persisted state before completion and are adopted without rebuilding', async () => {
|
|
4
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
5
|
+
try {
|
|
6
|
+
await host.start();
|
|
7
|
+
await host.call('loadDocument', {
|
|
8
|
+
url: 'data:text/x-qml,persisted',
|
|
9
|
+
contentType: 'text/x-qml',
|
|
10
|
+
source: `
|
|
11
|
+
import QtQuick
|
|
12
|
+
import QtQml.Models
|
|
13
|
+
Item { id:root; width:300;height:200
|
|
14
|
+
property int built:0; property int gone:0; property var held:null; property string observed:""
|
|
15
|
+
ListView {id:view;width:200;height:100;reuseItems:false
|
|
16
|
+
model:DelegateModel {id:rows;model:100;filterOnGroup:"shown";groups:DelegateModelGroup {id:shown;name:"shown";includeByDefault:false}}
|
|
17
|
+
delegate:Text {id:cell;required property int index; text:"row "+index;width:200;height:20
|
|
18
|
+
Component.onCompleted:{root.built++;root.observed=String(cell.DelegateModel.inPersistedItems)}
|
|
19
|
+
Component.onDestruction:root.gone++ }
|
|
20
|
+
}
|
|
21
|
+
Button {id:create;onClicked:root.held=rows.items.create(0)}
|
|
22
|
+
Button {id:adopt;onClicked:{rows.items.addGroups(0,1,"shown");view.forceLayout();root.held.DelegateModel.inPersistedItems=false}}
|
|
23
|
+
Button {id:shrink;onClicked:{shown.removeGroups(0,1,"shown");view.forceLayout()}}
|
|
24
|
+
Text {id:status;y:140;text:[root.built,root.gone,rows.persistedItems.count,root.observed,view.count].join(":")}
|
|
25
|
+
}`,
|
|
26
|
+
});
|
|
27
|
+
await host.call('whenLoaded');
|
|
28
|
+
const click = async (id) => {
|
|
29
|
+
await host.call('evaluate', `document.getElementById('${id}').click()`);
|
|
30
|
+
await host.call('evaluate', '0');
|
|
31
|
+
};
|
|
32
|
+
const status = () =>
|
|
33
|
+
host.call('evaluate', 'document.getElementById("status").textContent');
|
|
34
|
+
await click('create');
|
|
35
|
+
expect(await status()).toBe('1:0:1:true:0');
|
|
36
|
+
await click('create');
|
|
37
|
+
expect(await status()).toBe('1:0:1:true:0');
|
|
38
|
+
await click('adopt');
|
|
39
|
+
expect(await status()).toBe('1:0:0:true:1');
|
|
40
|
+
await click('shrink');
|
|
41
|
+
expect(await status()).toBe('1:1:0:true:0');
|
|
42
|
+
} finally {
|
|
43
|
+
host.close();
|
|
44
|
+
}
|
|
45
|
+
}, 30000);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Opt-in comparison against the genuine binary; no Quickshell implementation source.
|
|
2
|
+
import { test, expect } from 'bun:test';
|
|
3
|
+
import { withQuickshellFixture } from './helpers/quickshell-fixture.js';
|
|
4
|
+
test.skipIf(!process.env.BUNINU_QS_ROOT)(
|
|
5
|
+
'real Quickshell DialogButton activates by click, Enter and Space; disabled stays inert',
|
|
6
|
+
async () => {
|
|
7
|
+
await withQuickshellFixture(
|
|
8
|
+
'button.qml',
|
|
9
|
+
async ({ land, title, until, click, key }) => {
|
|
10
|
+
await until(() => title() === 'Flea buttons | 0');
|
|
11
|
+
await Bun.sleep(300);
|
|
12
|
+
await click(50, 36);
|
|
13
|
+
await until(() => title() === 'Flea buttons | 1');
|
|
14
|
+
await key('Enter', 'Enter');
|
|
15
|
+
await until(() => title() === 'Flea buttons | 2');
|
|
16
|
+
await key(' ', 'Space');
|
|
17
|
+
await until(() => title() === 'Flea buttons | 3');
|
|
18
|
+
await click(50, 86);
|
|
19
|
+
expect(title()).toBe('Flea buttons | 3');
|
|
20
|
+
await Bun.write(
|
|
21
|
+
'/tmp/quickshell-flea-buttons.png',
|
|
22
|
+
Buffer.from(
|
|
23
|
+
await land.context.screenshot({ format: 'png' }),
|
|
24
|
+
'base64',
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
},
|
|
30
|
+
30000,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
test.skipIf(!process.env.BUNINU_QS_ROOT)(
|
|
34
|
+
'real Quickshell DialogField forwards edits, Enter, Tab and Shift-Tab',
|
|
35
|
+
async () => {
|
|
36
|
+
await withQuickshellFixture('field.qml', async ({ title, until, key }) => {
|
|
37
|
+
await until(() => title() === 'Flea field | first | 0:0:0');
|
|
38
|
+
await key('a', 'KeyA', 2);
|
|
39
|
+
await key('h', 'KeyH');
|
|
40
|
+
await until(() => title() === 'Flea field | h | 1:0:0');
|
|
41
|
+
await key('Enter', 'Enter');
|
|
42
|
+
await key('Tab', 'Tab');
|
|
43
|
+
await until(() => title() === 'Flea field | h | 1:1:1');
|
|
44
|
+
await key('Tab', 'Tab', 8);
|
|
45
|
+
await until(() => title() === 'Flea field | h | 1:1:0');
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
30000,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
test.skipIf(!process.env.BUNINU_QS_ROOT)(
|
|
52
|
+
'real Quickshell reports empty and clipped multiline Text counts',
|
|
53
|
+
async () => {
|
|
54
|
+
await withQuickshellFixture(
|
|
55
|
+
'text-metrics.qml',
|
|
56
|
+
async ({ title, until }) => {
|
|
57
|
+
await until(() => title()?.startsWith('Text metrics | 0:'));
|
|
58
|
+
const fields = title().split(' | ')[1].split(':');
|
|
59
|
+
expect(Number(fields[1])).toBeGreaterThan(0);
|
|
60
|
+
expect(fields.slice(2).join(':')).toBe('1:1:2:2:true');
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
30000,
|
|
65
|
+
);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Opt-in binary comparison. No Quickshell source or dependency installation.
|
|
2
|
+
// BUNINU_QS_ROOT=/path/to/extracted/sid/root bun test test/qml-quickshell-settings.test.js
|
|
3
|
+
import {test,expect} from 'bun:test';
|
|
4
|
+
import {mkdtempSync,cpSync,readFileSync,writeFileSync,readdirSync,existsSync,mkdirSync,rmSync} from 'node:fs';
|
|
5
|
+
import {join} from 'node:path';
|
|
6
|
+
const qsRoot=process.env.BUNINU_QS_ROOT;
|
|
7
|
+
test.skipIf(!qsRoot)('real Quickshell SettingsRuler matches size, disabled tap and reset behavior',async()=>{
|
|
8
|
+
const {start}=await import('../../buninu-land/compositor.js');
|
|
9
|
+
const dir=mkdtempSync('/tmp/qs-settings-test-'),fixture=join(dir,'fixture'),home=join(dir,'home');
|
|
10
|
+
const oldRuntime=process.env.XDG_RUNTIME_DIR;
|
|
11
|
+
let land,child;
|
|
12
|
+
try{
|
|
13
|
+
cpSync(Bun.fileURLToPath(new URL('./fixtures/qml/flea-theme/',import.meta.url)),fixture,{recursive:true});
|
|
14
|
+
// Qt validates all declared types, including ones not used by this subset.
|
|
15
|
+
// Trim only the temporary module indexes; all actual QML/JS stays unchanged.
|
|
16
|
+
const trim=path=>{
|
|
17
|
+
for(const e of readdirSync(path,{withFileTypes:true})){
|
|
18
|
+
const p=join(path,e.name);
|
|
19
|
+
if(e.isDirectory())trim(p);
|
|
20
|
+
else if(e.name==='qmldir')writeFileSync(p,readFileSync(p,'utf8').split('\n').filter(line=>{
|
|
21
|
+
const last=line.trim().split(/\s+/).at(-1);
|
|
22
|
+
return !last.endsWith('.qml')||existsSync(join(path,last));
|
|
23
|
+
}).join('\n'));
|
|
24
|
+
}
|
|
25
|
+
};trim(fixture);
|
|
26
|
+
const theme=join(home,'.local/state/omarchy/current/theme');mkdirSync(theme,{recursive:true});
|
|
27
|
+
writeFileSync(join(theme,'shell.toml'),'[font]\nbase-size = 14\n');
|
|
28
|
+
writeFileSync(join(theme,'colors.toml'),'background="#101315"\nforeground="#eeeeee"\naccent="#70e1e8"\n');
|
|
29
|
+
writeFileSync(join(theme,'../theme.name'),'demo');
|
|
30
|
+
process.env.XDG_RUNTIME_DIR=dir;
|
|
31
|
+
land=await start(['--remote-debugging-port=0','--window-size=1000,700']);
|
|
32
|
+
const lib=join(qsRoot,'usr/lib/aarch64-linux-gnu');
|
|
33
|
+
child=Bun.spawn([join(lib,'ld-linux-aarch64.so.1'),'--library-path',lib+':'+join(qsRoot,'usr/lib'),join(qsRoot,'usr/bin/qs'),'-p',join(fixture,'settings.qml')],{
|
|
34
|
+
env:{...process.env,HOME:home,HYPRLAND_INSTANCE_SIGNATURE:'',WAYLAND_DISPLAY:'wayland-buninu',QT_PLUGIN_PATH:join(lib,'qt6/plugins'),QML_IMPORT_PATH:join(fixture,'imports')+':'+join(lib,'qt6/qml'),QT_QPA_PLATFORM:'wayland',QT_WAYLAND_DISABLE_WINDOWDECORATION:'1',QT_QUICK_BACKEND:'software'},
|
|
35
|
+
stdout:'ignore',stderr:'pipe'
|
|
36
|
+
});
|
|
37
|
+
const logs=new Response(child.stderr).text();
|
|
38
|
+
const title=()=>land.compositor.wm.visible[0]?.surface.xdg.toplevel.title;
|
|
39
|
+
const until=async fn=>{for(let i=0;i<300;i++){if(await fn())return;if(child.exitCode!==null)throw Error(await logs);await Bun.sleep(50);}throw Error('Quickshell state timed out: '+title());};
|
|
40
|
+
await until(()=>title()==='Flea — text size settings | 14 | 0');
|
|
41
|
+
const click=async(x,y)=>{
|
|
42
|
+
const surface=land.compositor.wm.renderSurfaces().find(s=>s.pixels);
|
|
43
|
+
const o=land.compositor.wm.origin(surface);
|
|
44
|
+
for(const type of ['mouseMoved','mousePressed','mouseReleased'])land.context.dispatchMouseEvent({type,x:o.x+x,y:o.y+y,button:'left'});
|
|
45
|
+
await Bun.sleep(120);
|
|
46
|
+
};
|
|
47
|
+
await Bun.sleep(500);
|
|
48
|
+
await Bun.write('/tmp/qs-settings-before.png',Buffer.from(await land.context.screenshot({format:'png'}),'base64'));
|
|
49
|
+
await click(712,100);await until(()=>title()==='Flea — text size settings | 20 | 1');
|
|
50
|
+
await click(100,300);await click(76,100);
|
|
51
|
+
expect(title()).toBe('Flea — text size settings | 20 | 1');
|
|
52
|
+
await click(100,300);await click(76,100);await until(()=>title()==='Flea — text size settings | 9 | 2');
|
|
53
|
+
await click(380,300);await until(()=>title()==='Flea — text size settings | 14 | 2');
|
|
54
|
+
await Bun.write('/tmp/quickshell-flea-settings.png',Buffer.from(await land.context.screenshot({format:'png'}),'base64'));
|
|
55
|
+
expect(title()).toBe('Flea — text size settings | 14 | 2');
|
|
56
|
+
}finally{
|
|
57
|
+
if(child){child.kill();await child.exited;}
|
|
58
|
+
land?.close();
|
|
59
|
+
if(oldRuntime===undefined)delete process.env.XDG_RUNTIME_DIR;else process.env.XDG_RUNTIME_DIR=oldRuntime;
|
|
60
|
+
rmSync(dir,{recursive:true,force:true});
|
|
61
|
+
}
|
|
62
|
+
},30000);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {test, expect} from 'bun:test';
|
|
2
|
+
import {RendererHost} from '../lib/renderer/host.js';
|
|
3
|
+
|
|
4
|
+
test('Repeater scopes delegates, updates models, preserves sibling order and disposes old handlers', async () => {
|
|
5
|
+
const host=new RendererHost({timeout:20000});
|
|
6
|
+
try {
|
|
7
|
+
await host.start();
|
|
8
|
+
await host.call('loadDocument',{url:'https://qml.test/repeater.qml',contentType:'text/x-qml',source:`
|
|
9
|
+
import QtQuick
|
|
10
|
+
Rectangle {
|
|
11
|
+
id: root; width: 600; height: 300
|
|
12
|
+
property var items: [10,20,30]
|
|
13
|
+
property int clicks: 0
|
|
14
|
+
property int factor: 2
|
|
15
|
+
Text {id: before; text: "before"}
|
|
16
|
+
Repeater {
|
|
17
|
+
id: list; model: root.items
|
|
18
|
+
delegate: Rectangle {
|
|
19
|
+
id: cell
|
|
20
|
+
required property var modelData
|
|
21
|
+
required property int index
|
|
22
|
+
x: index*100; y: 50; width: 90; height: 40
|
|
23
|
+
Text {text: cell.index+":"+cell.modelData*root.factor; anchors.horizontalCenter: parent.horizontalCenter}
|
|
24
|
+
TapHandler {gesturePolicy: TapHandler.ReleaseWithinBounds; onTapped: root.clicks++}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
Text {id: after; text: "after"}
|
|
28
|
+
Text {id: status; text: root.clicks+":"+list.count}
|
|
29
|
+
Button {id: replace; onClicked: {root.items=[7,8];root.factor=3}}
|
|
30
|
+
Button {id: clear; onClicked: root.items=[]}
|
|
31
|
+
}`});
|
|
32
|
+
await host.call('whenLoaded');
|
|
33
|
+
const texts=()=>host.call('evaluate','JSON.stringify([...document.querySelectorAll("[data-qml-type=Text]")].map(e=>e.textContent))');
|
|
34
|
+
expect(JSON.parse(await texts())).toEqual(['before','0:20','1:40','2:60','after','0:3']);
|
|
35
|
+
await host.call('evaluate',`globalThis.oldCell=document.querySelector('[data-qml-type=Repeater]').previousElementSibling;oldCell.click()`);
|
|
36
|
+
expect(await host.call('evaluate','document.getElementById("status").textContent')).toBe('1:3');
|
|
37
|
+
await host.call('evaluate','document.getElementById("replace").click()');
|
|
38
|
+
expect(JSON.parse(await texts())).toEqual(['before','0:21','1:24','after','1:2']);
|
|
39
|
+
await host.call('evaluate','oldCell.click()');
|
|
40
|
+
expect(await host.call('evaluate','document.getElementById("status").textContent')).toBe('1:2');
|
|
41
|
+
await host.call('evaluate','document.getElementById("clear").click()');
|
|
42
|
+
expect(JSON.parse(await texts())).toEqual(['before','after','1:0']);
|
|
43
|
+
} finally {host.close();}
|
|
44
|
+
},30000);
|
|
45
|
+
|
|
46
|
+
test('nested numeric Repeaters keep instance IDs local and allow later outer IDs', async () => {
|
|
47
|
+
const host=new RendererHost({timeout:20000});
|
|
48
|
+
try {
|
|
49
|
+
await host.start();
|
|
50
|
+
await host.call('loadDocument',{url:'https://qml.test/nested.qml',contentType:'text/x-qml',source:`
|
|
51
|
+
import QtQuick
|
|
52
|
+
Item {
|
|
53
|
+
Repeater {
|
|
54
|
+
model: 2
|
|
55
|
+
delegate: Item {
|
|
56
|
+
id: row
|
|
57
|
+
required property int index
|
|
58
|
+
Repeater {
|
|
59
|
+
model: 2
|
|
60
|
+
delegate: Text {
|
|
61
|
+
required property int index
|
|
62
|
+
text: suffix.text+":"+row.index+":"+index
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
Text {id: suffix; text: "item"}
|
|
68
|
+
}`});
|
|
69
|
+
await host.call('whenLoaded');
|
|
70
|
+
expect(JSON.parse(await host.call('evaluate','JSON.stringify([...document.querySelectorAll("[data-qml-type=Text]")].map(e=>e.textContent))'))).toEqual(['item:0:0','item:0:1','item:1:0','item:1:1','item']);
|
|
71
|
+
}finally{host.close();}
|
|
72
|
+
},30000);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { RendererHost } from '../lib/renderer/host.js';
|
|
3
|
+
|
|
4
|
+
test('QML style updates preserve final overrides and clear conditional styles', async () => {
|
|
5
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
6
|
+
try {
|
|
7
|
+
await host.start();
|
|
8
|
+
await host.call('loadDocument', {
|
|
9
|
+
url: 'data:text/x-qml,styles',
|
|
10
|
+
contentType: 'text/x-qml',
|
|
11
|
+
source: `import QtQuick
|
|
12
|
+
Rectangle {
|
|
13
|
+
id: root; width: 320; height: 160; property bool changed: false
|
|
14
|
+
onClicked: { changed = !changed; }
|
|
15
|
+
Rectangle { id: box; width: root.changed ? 120 : 80; height: 30; x: 10
|
|
16
|
+
opacity: root.changed ? 0.5 : 1; color: root.changed ? "blue" : "red"
|
|
17
|
+
gradient: root.changed ? null : ramp
|
|
18
|
+
Gradient { id: ramp; GradientStop { position: 0; color: "red" } GradientStop { position: 1; color: "blue" } }
|
|
19
|
+
}
|
|
20
|
+
Text { id: label; y:40; width:80; height:30; text:"long long text"
|
|
21
|
+
elide: root.changed ? Text.ElideNone : Text.ElideRight
|
|
22
|
+
verticalAlignment: Text.AlignVCenter
|
|
23
|
+
}
|
|
24
|
+
}`,
|
|
25
|
+
});
|
|
26
|
+
await host.call('whenLoaded');
|
|
27
|
+
const styles = () =>
|
|
28
|
+
host.call(
|
|
29
|
+
'evaluate',
|
|
30
|
+
`JSON.stringify({
|
|
31
|
+
width:box.style.width,opacity:box.style.opacity,
|
|
32
|
+
gradient:box.style.backgroundImage,overflow:label.style.overflow,
|
|
33
|
+
whiteSpace:label.style.whiteSpace,lineHeight:label.style.lineHeight,
|
|
34
|
+
elide:label.style.textOverflow
|
|
35
|
+
})`
|
|
36
|
+
.replaceAll('box.style', 'document.getElementById("box").style')
|
|
37
|
+
.replaceAll('label.style', 'document.getElementById("label").style'),
|
|
38
|
+
);
|
|
39
|
+
const before = JSON.parse(await styles());
|
|
40
|
+
expect(before.width).toBe('80px');
|
|
41
|
+
expect(before.gradient).toContain('linear-gradient');
|
|
42
|
+
expect(before.elide).toBe('ellipsis');
|
|
43
|
+
expect(before.whiteSpace).toBe('nowrap');
|
|
44
|
+
expect(before.lineHeight).toBe('30px');
|
|
45
|
+
await host.call(
|
|
46
|
+
'evaluate',
|
|
47
|
+
'document.getElementById("box").style.zIndex="99";document.getElementById("root").click()',
|
|
48
|
+
);
|
|
49
|
+
expect(
|
|
50
|
+
await host.call(
|
|
51
|
+
'evaluate',
|
|
52
|
+
'document.getElementById("box").style.zIndex',
|
|
53
|
+
),
|
|
54
|
+
).toBe('0');
|
|
55
|
+
const after = JSON.parse(await styles());
|
|
56
|
+
expect(after.width).toBe('120px');
|
|
57
|
+
expect(after.opacity).toBe('0.5');
|
|
58
|
+
expect(after.gradient).toBe('none');
|
|
59
|
+
expect(after.elide).toBe('clip');
|
|
60
|
+
expect(after.whiteSpace).toBe('pre');
|
|
61
|
+
expect(after.overflow).toBe('visible');
|
|
62
|
+
expect(after.lineHeight).toBe('30px');
|
|
63
|
+
await host.call('evaluate', 'document.getElementById("root").click()');
|
|
64
|
+
expect(JSON.parse(await styles())).toEqual(before);
|
|
65
|
+
} finally {
|
|
66
|
+
host.close();
|
|
67
|
+
}
|
|
68
|
+
}, 30000);
|
|
69
|
+
|
|
70
|
+
// Qt retains the long Wrap enum spelling used by styled code viewers.
|
|
71
|
+
test('StyledText can toggle WrapAtWordBoundaryOrAnywhere and grow its line count', async () => {
|
|
72
|
+
const host = new RendererHost({ timeout: 20000 });
|
|
73
|
+
try {
|
|
74
|
+
await host.start();
|
|
75
|
+
await host.call('loadDocument', {
|
|
76
|
+
url: 'data:text/x-qml,wrap-alias', contentType: 'text/x-qml',
|
|
77
|
+
source: `import QtQuick
|
|
78
|
+
Rectangle {
|
|
79
|
+
id: root; width: 320; height: 200; property bool wrapped: false
|
|
80
|
+
onClicked: wrapped = !wrapped
|
|
81
|
+
Text { id: code; width: 90; textFormat: Text.StyledText
|
|
82
|
+
text: "<font color='red'>const</font> averylongidentifierwithoutspaces = 123456789;"
|
|
83
|
+
wrapMode: root.wrapped ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap
|
|
84
|
+
}
|
|
85
|
+
Text { id: count; y: 180; text: code.lineCount }
|
|
86
|
+
}`,
|
|
87
|
+
});
|
|
88
|
+
await host.call('whenLoaded');
|
|
89
|
+
const read = () => host.call('evaluate', `({
|
|
90
|
+
lines: Number(document.getElementById('count').textContent),
|
|
91
|
+
whiteSpace: document.getElementById('code').style.whiteSpace,
|
|
92
|
+
overflowWrap: document.getElementById('code').style.overflowWrap
|
|
93
|
+
})`);
|
|
94
|
+
expect((await read()).lines).toBe(1);
|
|
95
|
+
await host.call('evaluate', "document.getElementById('root').click()");
|
|
96
|
+
const wrapped = await read();
|
|
97
|
+
expect(wrapped.lines).toBeGreaterThan(1);
|
|
98
|
+
expect(wrapped.whiteSpace).toBe('pre-wrap');
|
|
99
|
+
expect(wrapped.overflowWrap).toBe('anywhere');
|
|
100
|
+
await host.call('evaluate', "document.getElementById('root').click()");
|
|
101
|
+
expect((await read()).lines).toBe(1);
|
|
102
|
+
} finally { host.close(); }
|
|
103
|
+
}, 30000);
|
|
104
|
+
|
|
105
|
+
test('Qt.binding attaches to a default property and a plain write detaches it', async () => {
|
|
106
|
+
const host = new RendererHost({timeout:20000});
|
|
107
|
+
try {
|
|
108
|
+
await host.start();
|
|
109
|
+
await host.call('loadDocument',{url:'data:text/x-qml,default-binding',contentType:'text/x-qml',source:`import QtQuick
|
|
110
|
+
Item {id:root;width:100;height:100;property bool shown:true
|
|
111
|
+
Rectangle {id:box;width:40;height:40}
|
|
112
|
+
Component.onCompleted:box.visible=Qt.binding(function(){return root.shown})
|
|
113
|
+
Button {id:toggle;onClicked:root.shown=!root.shown}
|
|
114
|
+
Button {id:detach;onClicked:box.visible=true}
|
|
115
|
+
}`});
|
|
116
|
+
await host.call('whenLoaded');
|
|
117
|
+
const display=()=>host.call('evaluate','document.getElementById("box").style.display');
|
|
118
|
+
const click=id=>host.call('evaluate',`document.getElementById('${id}').click()`);
|
|
119
|
+
expect(await display()).toBe('block');
|
|
120
|
+
await click('toggle');expect(await display()).toBe('none');
|
|
121
|
+
await click('toggle');expect(await display()).toBe('block');
|
|
122
|
+
await click('detach');await click('toggle');expect(await display()).toBe('block');
|
|
123
|
+
}finally{host.close();}
|
|
124
|
+
},30000);
|