buninu-browser 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/jsmdcui.txt +22 -0
- package/README.md +40 -4
- package/frontends/wayland/LICENSE-land +21 -0
- package/frontends/wayland/README.md +152 -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 +130 -0
- package/frontends/wayland/input.js +75 -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 +162 -0
- package/index.js +28 -1
- package/lib/audio/shared-connection.js +31 -0
- package/lib/cdp-server.js +15 -2
- package/lib/clipboard-command.js +10 -0
- package/lib/clipboard-powershell.js +22 -0
- package/lib/clipboard.js +37 -2
- package/lib/document-types.js +11 -0
- package/lib/headless-shell.js +54 -12
- package/lib/layout/text-layout.js +17 -9
- package/lib/paint/display-list.js +6 -1
- package/lib/qml/README.md +1427 -0
- package/lib/qml/builtins.js +771 -0
- package/lib/qml/canvas-dom.js +115 -0
- package/lib/qml/canvas.js +76 -0
- package/lib/qml/document.js +1199 -0
- package/lib/qml/fonts.js +69 -0
- package/lib/qml/format.js +9 -0
- package/lib/qml/host.js +352 -0
- package/lib/qml/images.js +199 -0
- package/lib/qml/launcher.js +132 -0
- package/lib/qml/loader.js +521 -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 +182 -0
- package/lib/qml/runtime.js +3603 -0
- package/lib/qml/shader/fragment.js +83 -0
- package/lib/qml/shader/qsb.js +27 -0
- package/lib/qml/shader/renderer.js +39 -0
- package/lib/qml/shader/runtime.js +61 -0
- package/lib/qml/shapes.js +251 -0
- package/lib/qml/shortcuts.js +65 -0
- package/lib/qml/text.js +109 -0
- package/lib/qml/textures.js +126 -0
- package/lib/qml/wayland-clipboard.js +32 -0
- package/lib/qml/window-constraints.js +16 -0
- package/lib/qml/worker-thread.js +70 -0
- package/lib/render-tree/index.js +16 -1
- package/lib/renderer/bindings.js +16 -1
- package/lib/renderer/canvas-2d.js +29 -0
- package/lib/renderer/document-selection.js +19 -0
- package/lib/renderer/host.js +45 -17
- package/lib/renderer/main.js +21 -6
- package/lib/renderer/page-realm.js +20 -10
- package/lib/renderer/page-renderer.js +497 -42
- package/lib/renderer/pointer-move.js +5 -3
- package/lib/renderer/render-interval.js +9 -0
- package/lib/renderer/text-input-paint.js +54 -0
- package/lib/renderer/text-input.js +57 -0
- package/lib/style/computed-style.js +10 -4
- package/package.json +1 -1
- package/test/audio-connection-lifetime.test.js +46 -0
- package/test/canvas-paint.test.js +8 -0
- package/test/clipboard.test.js +19 -0
- package/test/computed-style.test.js +12 -0
- package/test/document-selection.test.js +27 -0
- package/test/double-click.test.js +85 -0
- package/test/fixtures/canvas-damage.js +40 -0
- package/test/fixtures/fake-renderer.js +1 -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/omastorm-helpers/Dockerfile +35 -0
- package/test/fixtures/omastorm-helpers/check.mjs +56 -0
- package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
- package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
- package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
- package/test/fixtures/pointer-paint-batching.js +23 -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-fonts/main.qml +10 -0
- package/test/fixtures/qml-locale/check.mjs +6 -0
- package/test/fixtures/qml-locale/main.qml +8 -0
- package/test/fixtures/qml-media-audio.js +39 -0
- package/test/fixtures/qml-shader/grab.qml +14 -0
- package/test/fixtures/qml-shader/gradient.frag +5 -0
- package/test/fixtures/qml-shader/main.qml +14 -0
- package/test/fixtures/qml-shader/texture-grab.qml +25 -0
- package/test/fixtures/qml-shader/texture-size.frag +5 -0
- package/test/fixtures/qml-shader-check.mjs +24 -0
- package/test/fixtures/title-no-paint.js +20 -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-double-click.html +21 -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-mouse-buttons.html +18 -0
- package/test/manual-omastorm-engine.mjs +19 -0
- package/test/manual-omastorm-shaders.mjs +28 -0
- package/test/manual-omastorm-software.mjs +25 -0
- package/test/manual-pointer-scroll.html +43 -0
- package/test/manual-qml-removal.js +46 -0
- package/test/manual-without-qml.js +45 -0
- package/test/pointer-drag.test.js +104 -0
- package/test/qml-canvas-dom.test.js +37 -0
- package/test/qml-canvas.test.js +21 -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 +95 -0
- package/test/qml-controls-window.test.js +182 -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-fonts.test.js +31 -0
- package/test/qml-grid-multiline.test.js +48 -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-locale.test.js +45 -0
- package/test/qml-media.test.js +63 -0
- package/test/qml-nonvisual-visible.test.js +19 -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-shader-runtime.test.js +6 -0
- package/test/qml-shortcuts.test.js +39 -0
- package/test/qml-socket.test.js +60 -0
- package/test/qml-software-shader.test.js +55 -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-window-update.test.js +36 -0
- package/test/qml-worker.test.js +69 -0
- package/test/qml.test.js +83 -0
- package/test/render-interval.test.js +33 -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/title-no-paint.test.js +6 -0
- package/test/wayland-frontend.test.js +83 -0
- package/test/wayland-scroll.test.js +40 -0
- package/test/wayland-urlbar.test.js +61 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Offline, CPU-only compatibility probe. Reads upstream GLSL without modifying it.
|
|
2
|
+
import {resolve} from 'node:path';
|
|
3
|
+
import init from '../usr/lib/canvaskit/canvaskit.js';
|
|
4
|
+
import {compileFragment,makeFragmentShader} from '../lib/qml/shader/fragment.js';
|
|
5
|
+
import {fragmentSource} from '../lib/qml/shader/qsb.js';
|
|
6
|
+
const root=resolve(process.argv[2]||'../omastorm');
|
|
7
|
+
const kit=await init({wasmBinary:await Bun.file(new URL('../usr/lib/canvaskit/canvaskit.wasm',import.meta.url)).arrayBuffer()});
|
|
8
|
+
for(const name of ['tile','texture-check','radar','grid']){
|
|
9
|
+
const c=compileFragment(kit,fragmentSource(await Bun.file(root+'/ui/shaders/'+name+'.frag.qsb').bytes()));
|
|
10
|
+
try{
|
|
11
|
+
console.log(name+': CPU-compatible RuntimeEffect compiled');
|
|
12
|
+
if(!['tile','texture-check'].includes(name))continue;
|
|
13
|
+
const image=kit.MakeImage({width:2,height:1,colorType:kit.ColorType.RGBA_8888,alphaType:kit.AlphaType.Premul,colorSpace:kit.ColorSpace.SRGB},new Uint8Array([255,0,0,255,255,0,0,255]),8);
|
|
14
|
+
const child=image.makeShaderOptions(kit.TileMode.Clamp,kit.TileMode.Clamp,kit.FilterMode.Nearest,kit.MipmapMode.None);
|
|
15
|
+
const shader=makeFragmentShader(c,{width:16,height:16,uniforms:{expected:[2,1],boundaries:[1,0,0,1],water:[0,0,0,0],minorRoads:[0,0,0,0],majorRoads:[0,0,0,0]},textures:{[name==='tile'?'mask':'sweep']:{shader:child,width:2,height:1}}});
|
|
16
|
+
const surface=kit.MakeSurface(16,16),paint=new kit.Paint();
|
|
17
|
+
try{
|
|
18
|
+
paint.setShader(shader);surface.getCanvas().drawRect(kit.XYWHRect(0,0,16,16),paint);surface.flush();
|
|
19
|
+
const result=surface.makeImageSnapshot();
|
|
20
|
+
try{
|
|
21
|
+
const pixel=Array.from(result.readPixels(0,0,{width:1,height:1,colorType:kit.ColorType.RGBA_8888,alphaType:kit.AlphaType.Unpremul,colorSpace:kit.ColorSpace.SRGB}));
|
|
22
|
+
const expected=name==='tile'?[255,0,0,255]:[0,255,0,255];
|
|
23
|
+
if(JSON.stringify(pixel)!==JSON.stringify(expected))throw Error(name+' wrong pixel: '+pixel);
|
|
24
|
+
console.log(name+': CPU pixel '+pixel+' verified');
|
|
25
|
+
}finally{result.delete();}
|
|
26
|
+
}finally{paint.delete();surface.delete();shader.delete();child.delete();image.delete();}
|
|
27
|
+
}finally{c.effect.delete();}
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Explicit experiment: copy the application and remove only its GPU policy.
|
|
2
|
+
// Does not alter the checkout, fake GraphicsInfo, or bypass shader/pixel checks.
|
|
3
|
+
import {mkdtemp,cp,readFile,writeFile} from 'node:fs/promises';
|
|
4
|
+
import {resolve} from 'node:path';
|
|
5
|
+
import {tmpdir} from 'node:os';
|
|
6
|
+
const root=resolve(process.argv[2]||'../omastorm');
|
|
7
|
+
const directory=await mkdtemp(resolve(tmpdir(),'omastorm-software-'));
|
|
8
|
+
await cp(root+'/ui',directory+'/ui',{recursive:true});
|
|
9
|
+
const replacements=[
|
|
10
|
+
['TextureCheck.qml', / if \(GraphicsInfo\.api === GraphicsInfo\.Software\) \{\n fail\("Radar requires GPU rendering \(OpenGL\/Vulkan\)\."\);\n return;\n \}\n/],
|
|
11
|
+
['RadarMap.qml', / Component\.onCompleted: if \(GraphicsInfo\.api === GraphicsInfo\.Software\) map\.graphicsError = "Radar requires GPU rendering \(OpenGL\/Vulkan\)\."\n/],
|
|
12
|
+
];
|
|
13
|
+
for(const [file,pattern] of replacements){
|
|
14
|
+
const path=directory+'/ui/'+file,source=await readFile(path,'utf8');
|
|
15
|
+
if(!pattern.test(source))throw Error('Upstream policy changed; review '+file+' before modifying it');
|
|
16
|
+
await writeFile(path,source.replace(pattern,''));
|
|
17
|
+
}
|
|
18
|
+
console.log('Software test copy: '+directory);
|
|
19
|
+
console.log('Original engine must already be running in XDG_RUNTIME_DIR/omastorm.');
|
|
20
|
+
if(process.argv.includes('--prepare-only'))process.exit(0);
|
|
21
|
+
const child=Bun.spawn([process.execPath,new URL('../lib/qml/qswayland',import.meta.url).pathname,'-p',directory+'/ui/shell.qml'],{
|
|
22
|
+
env:{...process.env,OMASTORM_ROOT:root},stdin:'inherit',stdout:'inherit',stderr:'inherit',
|
|
23
|
+
});
|
|
24
|
+
for(const signal of ['SIGINT','SIGTERM','SIGHUP'])process.on(signal,()=>child.kill(signal));
|
|
25
|
+
process.exitCode=await child.exited;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-Hant">
|
|
3
|
+
<meta charset="utf-8">
|
|
4
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5
|
+
<title>滑鼠與捲動測試</title>
|
|
6
|
+
<style>
|
|
7
|
+
*{box-sizing:border-box}body{margin:0;padding:16px;background:#101923;color:#e4edf5;font:16px sans-serif}h1{font-size:24px}h2{font-size:18px;margin:10px 0}section{padding:14px;margin:16px 0;background:#1c2a38;border:1px solid #446079;border-radius:8px}button,textarea{font:inherit}button{padding:10px;margin:4px;background:#bcebea;color:#102c35;border:0;border-radius:4px}code,pre{font:13px monospace}pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:240px;overflow:auto}p{line-height:1.5}.box{width:100%;height:220px;border:2px solid #69c7dd;background:#172431}.v{overflow-x:hidden;overflow-y:auto}.h{overflow-x:auto;overflow-y:hidden}.both{overflow:auto}.wide{width:1800px;height:180px;background:linear-gradient(90deg,#375f8b,#426e4d,#9c4f49);padding:20px}.plane{width:1800px;height:1200px;background:repeating-linear-gradient(0deg,transparent,transparent 99px,#7cabc4 100px),repeating-linear-gradient(90deg,#263d52,#263d52 99px,#7cabc4 100px);padding:20px}.row{padding:18px;border-bottom:1px solid #436479}.readout{color:#8de1e8;margin:8px 0}#target{display:block;width:100%;min-height:100px;background:#286a75;color:white}canvas{display:block;width:100%;height:220px;background:#f5fafb;touch-action:none}textarea{width:100%;height:110px}label{display:block;margin:8px 0}
|
|
8
|
+
</style>
|
|
9
|
+
<h1>滑鼠與捲動測試</h1>
|
|
10
|
+
<p>各區獨立捲動。先把游標放在指定區域再操作,觀察位置與事件紀錄。Shift/Ctrl/Alt/Super 是否到達頁面取決於外層終端與系統;本頁不自行交換滾輪軸向。</p>
|
|
11
|
+
<section><h2>1. 垂直捲動</h2><div class="readout" id="v-state"></div><div id="v" class="box v"></div></section>
|
|
12
|
+
<section><h2>2. 水平捲動</h2><div class="readout" id="h-state"></div><div id="h" class="box h"><div class="wide">左端 0 ← 水平 1800 px → 右端<br><br>試水平手勢或 Shift+滾輪;若事件仍是 deltaY,請看外層終端是否轉換。</div></div></section>
|
|
13
|
+
<section><h2>3. 雙向捲動</h2><div class="readout" id="both-state"></div><div id="both" class="box both"><div class="plane">左上角 (0, 0) — 每格約 100 px;寬 1800、高 1200。</div></div><button id="reset">重設三區捲動</button></section>
|
|
14
|
+
<section><h2>4. 按鍵、雙擊與修飾鍵</h2><p>此按鈕攔截原生右鍵選單,只在這裡記錄 contextmenu。中鍵不捲頁。可按住修飾鍵再點擊。</p><button id="target">左鍵/右鍵/中鍵/雙擊這裡</button><div id="counts" class="readout"></div></section>
|
|
15
|
+
<section><h2>5. 按住左鍵拖曳畫線</h2><p>手機可雙擊並按住第二下拖曳。放開後再移動不應畫線;也可測移出畫布後放開。按住狀態與座標列在下方。</p><canvas id="paint" width="1000" height="300"></canvas><div id="drag" class="readout">尚未按下</div><button id="clear-paint">清除畫布</button></section>
|
|
16
|
+
<section id="text-selection"><h2>6. 文字選取與編輯</h2><p>這段可拖曳選取:春天的河水流過城市,0123456789,The quick brown fox。</p><textarea>第一行:滑鼠點擊定位,Shift+方向鍵選取。
|
|
17
|
+
第二行:測試 Ctrl-C/Ctrl-V、原生長按貼上。
|
|
18
|
+
第三行:這裡不自行實作 PRIMARY 選取或中鍵貼上。</textarea></section>
|
|
19
|
+
<section><h2>事件紀錄(最多 40 筆)</h2><label><input id="moves" type="checkbox"> 記錄 mousemove(會產生大量事件)</label><button id="clear-log">清除紀錄</button><pre id="log" aria-live="off"></pre></section>
|
|
20
|
+
<script>
|
|
21
|
+
const $=id=>document.getElementById(id), records=[], counts={left:0,right:0,middle:0,double:0};
|
|
22
|
+
function log(e){const mods=['shiftKey','ctrlKey','altKey','metaKey'].filter(k=>e[k]).map(k=>k.replace('Key','')).join('+')||'-';let msg=e.type+' target='+(e.target.id||e.target.tagName)+' button='+e.button+' buttons='+e.buttons+' xy='+Math.round(e.clientX||0)+','+Math.round(e.clientY||0)+' modifiers='+mods;if(e.type==='wheel')msg+=' delta='+e.deltaX+','+e.deltaY+' mode='+e.deltaMode;if(e.type==='click'||e.type==='dblclick'||e.type==='auxclick')msg+=' detail='+e.detail;records.unshift(msg);records.length=Math.min(40,records.length);$('log').textContent=records.join('\n')}
|
|
23
|
+
for(const type of ['mousedown','mouseup','click','dblclick','auxclick','contextmenu','wheel'])document.addEventListener(type,log,{capture:true,passive:true});
|
|
24
|
+
document.addEventListener('mousemove',e=>{if($('moves').checked)log(e)},{passive:true});
|
|
25
|
+
for(let i=0;i<20;i++){const r=document.createElement('div');r.className='row';r.textContent='垂直第 '+(i+1)+' 列';$('v').appendChild(r)}
|
|
26
|
+
for(const id of ['v','h','both']){const box=$(id),update=()=>{$(id+'-state').textContent='scrollLeft='+Math.round(box.scrollLeft)+' / '+Math.max(0,box.scrollWidth-box.clientWidth)+' · scrollTop='+Math.round(box.scrollTop)+' / '+Math.max(0,box.scrollHeight-box.clientHeight)};box.addEventListener('scroll',update);window.addEventListener('resize',update);update()}
|
|
27
|
+
$('reset').onclick=()=>{for(const id of ['v','h','both']){$(id).scrollLeft=0;$(id).scrollTop=0}};
|
|
28
|
+
const count=()=>{$('counts').textContent='左鍵 '+counts.left+' · 右鍵 '+counts.right+' · 中鍵 '+counts.middle+' · 雙擊 '+counts.double};count();
|
|
29
|
+
$('target').onclick=e=>{if(e.button===0){counts.left++;count()}};
|
|
30
|
+
$('target').oncontextmenu=e=>{e.preventDefault();counts.right++;count()};
|
|
31
|
+
$('target').onauxclick=e=>{e.preventDefault();if(e.button===1){counts.middle++;count()}};
|
|
32
|
+
$('target').onmousedown=e=>{if(e.button===1)e.preventDefault()};
|
|
33
|
+
$('target').ondblclick=()=>{counts.double++;count()};
|
|
34
|
+
const canvas=$('paint'),ctx=canvas.getContext('2d');let drawing=false,last=null;
|
|
35
|
+
const point=e=>{const r=canvas.getBoundingClientRect();return [(e.clientX-r.left)*canvas.width/r.width,(e.clientY-r.top)*canvas.height/r.height]};
|
|
36
|
+
canvas.onmousedown=e=>{if(e.button!==0)return;e.preventDefault();drawing=true;last=point(e);$('drag').textContent='按住左鍵'};
|
|
37
|
+
document.addEventListener('mousemove',e=>{if(!drawing)return;if(!(e.buttons&1)){drawing=false;$('drag').textContent='左鍵已放開';return}const p=point(e);ctx.strokeStyle='#136a8b';ctx.lineWidth=3;ctx.beginPath();ctx.moveTo(...last);ctx.lineTo(...p);ctx.stroke();last=p;$('drag').textContent='拖曳 '+p.map(Math.round).join(',')});
|
|
38
|
+
document.addEventListener('mouseup',e=>{if(e.button===0){drawing=false;$('drag').textContent='左鍵已放開'}});
|
|
39
|
+
window.addEventListener('blur',()=>{drawing=false;$('drag').textContent='失焦,停止畫線'});
|
|
40
|
+
$('clear-paint').onclick=()=>ctx.clearRect(0,0,canvas.width,canvas.height);
|
|
41
|
+
$('clear-log').onclick=()=>{records.length=0;$('log').textContent=''};
|
|
42
|
+
</script>
|
|
43
|
+
</html>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Run explicitly: bun test/manual-qml-removal.js
|
|
2
|
+
// Test the ordinary browser after physically removing every QML integration.
|
|
3
|
+
import { cpSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const source = Bun.fileURLToPath(new URL('../', import.meta.url));
|
|
8
|
+
const copy = mkdtempSync(join(tmpdir(), 'browser-without-qml-'));
|
|
9
|
+
const files = [
|
|
10
|
+
'frontends/wayland/index.js',
|
|
11
|
+
'lib/document-types.js',
|
|
12
|
+
'lib/headless-shell.js',
|
|
13
|
+
'lib/renderer/page-renderer.js',
|
|
14
|
+
'lib/renderer/canvas-2d.js',
|
|
15
|
+
'lib/render-tree/index.js',
|
|
16
|
+
'lib/renderer/host.js',
|
|
17
|
+
'lib/renderer/main.js',
|
|
18
|
+
];
|
|
19
|
+
try {
|
|
20
|
+
for (const entry of readdirSync(source, { withFileTypes: true })) {
|
|
21
|
+
if (entry.name.startsWith('.')) continue;
|
|
22
|
+
const from = join(source, entry.name), to = join(copy, entry.name);
|
|
23
|
+
if (entry.isDirectory() && !['lib', 'test', 'frontends'].includes(entry.name)) symlinkSync(from, to, 'dir');
|
|
24
|
+
else cpSync(from, to, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
let blocks = 0;
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
const path = join(copy, file);
|
|
29
|
+
const stripped = readFileSync(path, 'utf8').replace(
|
|
30
|
+
/^[ \t]*\/\/ qml handle begin:[^\n]*\n[\s\S]*?^[ \t]*\/\/ qml handle end[^\n]*(?:\n|$)/gm,
|
|
31
|
+
() => { blocks++; return ''; },
|
|
32
|
+
);
|
|
33
|
+
if (/qml/i.test(stripped)) throw Error(`QML reference remains in ${file}`);
|
|
34
|
+
writeFileSync(path, stripped);
|
|
35
|
+
}
|
|
36
|
+
rmSync(join(copy, 'lib/qml'), { recursive: true });
|
|
37
|
+
console.log(`Removed ${blocks} integration blocks and lib/qml in ${copy}`);
|
|
38
|
+
const child = Bun.spawn([
|
|
39
|
+
process.execPath, 'test',
|
|
40
|
+
'test/renderer-host.test.js', 'test/navigation.test.js', 'test/headless-shell.test.js',
|
|
41
|
+
], { cwd: copy, stdout: 'inherit', stderr: 'inherit' });
|
|
42
|
+
const code = await child.exited;
|
|
43
|
+
if (code) throw Error(`Browser regression tests exited ${code}`);
|
|
44
|
+
} finally {
|
|
45
|
+
rmSync(copy, { recursive: true, force: true });
|
|
46
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Run from the browser checkout: bun test/manual-without-qml.js
|
|
2
|
+
// Retains an isolated copy, logs and screenshots for review. No dependencies installed.
|
|
3
|
+
import {cpSync,existsSync,mkdtempSync,readFileSync,readdirSync,rmSync,symlinkSync,writeFileSync} from 'node:fs';
|
|
4
|
+
import {join,relative,resolve} from 'node:path';
|
|
5
|
+
import {tmpdir} from 'node:os';
|
|
6
|
+
const original=resolve(import.meta.dir,'..');
|
|
7
|
+
const dir=mkdtempSync(join(tmpdir(),'browser-without-qml-'));
|
|
8
|
+
const copy=join(dir,'buninu-browser');
|
|
9
|
+
cpSync(original,copy,{recursive:true,filter:path=>!['.git','node_modules'].includes(relative(original,path).split('/')[0])});
|
|
10
|
+
let blocks=0,changed=0;
|
|
11
|
+
function strip(dir){
|
|
12
|
+
for(const entry of readdirSync(dir,{withFileTypes:true})){
|
|
13
|
+
const path=join(dir,entry.name);
|
|
14
|
+
if(entry.isDirectory()){strip(path);continue;}
|
|
15
|
+
if(!entry.isFile()||!entry.name.endsWith('.js'))continue;
|
|
16
|
+
const source=readFileSync(path,'utf8');let depth=0,count=0;const lines=[];
|
|
17
|
+
for(const line of source.split('\n')){
|
|
18
|
+
if(/^\s*\/\/ qml handle begin\b/.test(line)){depth++;count++;continue;}
|
|
19
|
+
if(/^\s*\/\/ qml handle end\b/.test(line)){if(--depth<0)throw Error('Unmatched end: '+path);continue;}
|
|
20
|
+
if(!depth)lines.push(line);
|
|
21
|
+
}
|
|
22
|
+
if(depth)throw Error('Unclosed handle: '+path);
|
|
23
|
+
if(count){writeFileSync(path,lines.join('\n'));blocks+=count;changed++;}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
strip(copy);
|
|
27
|
+
rmSync(join(copy,'lib/qml'),{recursive:true});
|
|
28
|
+
if(existsSync(join(copy,'lib/qml')))throw Error('QML directory still exists');
|
|
29
|
+
// Only the test compositor is external; every browser module comes from the copy.
|
|
30
|
+
symlinkSync(resolve(original,'../buninu-land'),join(dir,'buninu-land'),'dir');
|
|
31
|
+
console.log(JSON.stringify({dir,copy,removedBlocks:blocks,changedFiles:changed,qmlDirectoryRemoved:true}));
|
|
32
|
+
async function command(args,cwd,log){
|
|
33
|
+
const out=Bun.file(join(dir,log));
|
|
34
|
+
const p=Bun.spawn([process.execPath,...args],{cwd,env:{...process.env,BUNINU_TEST_WAYLAND:'1'},stdout:out,stderr:out});
|
|
35
|
+
const code=await p.exited;console.log(log,code);if(code!==0)throw Error('Failed: '+log);
|
|
36
|
+
}
|
|
37
|
+
await command(['test','test/text-input.test.js','test/text-input-pointer.test.js','test/text-input-scroll.test.js','test/clipboard.test.js','test/html-clipboard-wayland.test.js','test/wayland-urlbar.test.js','test/wayland-frontend.test.js'],copy,'tests.log');
|
|
38
|
+
// Deterministic rendered fixture: compare full PNG bytes with and without QML.
|
|
39
|
+
const fixture=`<!doctype html><meta charset="utf-8"><title>HTML rendering without QML</title><style>body{margin:24px;background:#eef4f7;color:#183440;font:18px sans-serif}input,textarea{display:block;box-sizing:border-box;width:440px;margin:12px 0;padding:8px;border:1px solid #65899b;font:18px monospace}textarea{height:140px}canvas{border:1px solid #65899b}</style><h1>一般網頁 · HTML</h1><p>中文排版、選取、游標與 Canvas</p><input id="field" value="abc 中文 😀 xyz"><textarea>第一行:Hello\n第二行:選取與貼上\n第三行:12345</textarea><canvas id="picture" width="220" height="70"></canvas><script>const c=document.getElementById('picture').getContext('2d');c.fillStyle='#19a7a0';c.fillRect(0,0,220,70);c.fillStyle='white';c.font='20px sans-serif';c.fillText('Canvas 正常',12,42);const f=document.getElementById('field');f.focus();f.setSelectionRange(4,6);</script>`;
|
|
40
|
+
const fixturePath=join(dir,'fixture.html');writeFileSync(fixturePath,fixture);
|
|
41
|
+
await command(['index.js','--headless','--window-size=640,480','--screenshot='+join(dir,'original.png'),Bun.pathToFileURL(fixturePath).href],original,'original-render.log');
|
|
42
|
+
await command(['index.js','--headless','--window-size=640,480','--screenshot='+join(dir,'without-qml.png'),Bun.pathToFileURL(fixturePath).href],copy,'without-qml-render.log');
|
|
43
|
+
const before=readFileSync(join(dir,'original.png')),after=readFileSync(join(dir,'without-qml.png'));
|
|
44
|
+
if(!before.equals(after))throw Error('Rendering differs: compare screenshots in '+dir);
|
|
45
|
+
console.log('PASS: HTML tests and byte-identical rendered PNG; artifacts retained at '+dir);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {mkdtempSync,rmSync} from 'node:fs';
|
|
3
|
+
import {createBrowser} from '../lib/headless-shell.js';
|
|
4
|
+
import {CdpServer} from '../lib/cdp-server.js';
|
|
5
|
+
import {CDP,launch} from '../tools/browser-lab.js';
|
|
6
|
+
import {WaylandUrlBar} from '../frontends/wayland/urlbar.js';
|
|
7
|
+
|
|
8
|
+
const source=`<style>body{margin:0}#box{width:300px;height:160px}#a,#b{display:inline-block;width:100px;height:100px}canvas{display:block}</style>
|
|
9
|
+
<div id="box"><span id="a">A</span><span id="b">B</span></div><input id="field" value="hello"><canvas id="paint" width="300" height="100"></canvas>
|
|
10
|
+
<script>
|
|
11
|
+
globalThis.paint=document.getElementById("paint");globalThis.a=document.getElementById("a");globalThis.field=document.getElementById("field");
|
|
12
|
+
globalThis.events=[];globalThis.cancelDown=false;globalThis.cancelPointer=false;
|
|
13
|
+
for(const type of ['pointerdown','mousedown','pointermove','mousemove','pointerup','mouseup','click','auxclick'])document.addEventListener(type,e=>{events.push([type,e.target.id,e.button,e.buttons,e.shiftKey]);if(type==='mousedown'&&cancelDown)e.preventDefault();if(type==='pointerdown'&&cancelPointer)e.preventDefault()});
|
|
14
|
+
let drawing=false;const ctx=paint.getContext('2d');paint.onmousedown=()=>drawing=true;document.addEventListener('mouseup',()=>drawing=false);paint.onmousemove=e=>{if(drawing&&(e.buttons&1)){ctx.fillStyle='red';ctx.fillRect(0,0,20,20)}};
|
|
15
|
+
</script>`;
|
|
16
|
+
async function check(c){
|
|
17
|
+
await c.send('Page.navigate',{url:'data:text/html,'+encodeURIComponent(source)});
|
|
18
|
+
for(let i=0;i<200;i++){if(await c.evaluate('!!document.getElementById("paint")'))break;await Bun.sleep(20)}
|
|
19
|
+
const send=(type,x=20,y=20,extra={})=>c.send('Input.dispatchMouseEvent',{type,x,y,button:'left',buttons:type==='mouseReleased'?0:1,clickCount:1,...extra});
|
|
20
|
+
await send('mouseMoved',20,20,{buttons:0,button:'none'});await c.evaluate('events=[]');
|
|
21
|
+
await send('mousePressed',20,20,{modifiers:8});
|
|
22
|
+
expect(await c.evaluate('events')).toEqual([['pointerdown','a',0,1,true],['mousedown','a',0,1,true]]);
|
|
23
|
+
await send('mouseMoved',40,40,{modifiers:8});
|
|
24
|
+
expect((await c.evaluate('events')).slice(-2)).toEqual([['pointermove','a',-1,1,true],['mousemove','a',0,1,true]]);
|
|
25
|
+
await send('mouseReleased',40,40,{modifiers:8});
|
|
26
|
+
expect((await c.evaluate('events')).slice(-3)).toEqual([['pointerup','a',0,0,true],['mouseup','a',0,0,true],['click','a',0,0,true]]);
|
|
27
|
+
// Sibling press/release activates the common ancestor, never the release sibling.
|
|
28
|
+
await c.evaluate('events=[]');await send('mousePressed');await send('mouseReleased',140,20);
|
|
29
|
+
expect((await c.evaluate('events')).at(-1)).toEqual(['click','box',0,0,false]);
|
|
30
|
+
// A disconnected press target cannot activate another element on release.
|
|
31
|
+
await send('mousePressed');await c.evaluate('a.remove();events=[]');await send('mouseReleased');
|
|
32
|
+
expect((await c.evaluate('events')).some(e=>e[0]==='click')).toBe(false);
|
|
33
|
+
// Canceled mousedown must not focus the editor; normal focus happens on press.
|
|
34
|
+
const rect=await c.evaluate('field.getBoundingClientRect().toJSON()');
|
|
35
|
+
await c.evaluate('cancelDown=true');await send('mousePressed',rect.x+10,rect.y+8);
|
|
36
|
+
expect(await c.evaluate('document.activeElement.id')).not.toBe('field');await send('mouseReleased',rect.x+10,rect.y+8);
|
|
37
|
+
await c.evaluate('cancelDown=false');await send('mousePressed',rect.x+10,rect.y+8);
|
|
38
|
+
expect(await c.evaluate('document.activeElement.id')).toBe('field');await send('mouseReleased',rect.x+10,rect.y+8);
|
|
39
|
+
// pointerdown cancellation suppresses compatibility mouse events, not click.
|
|
40
|
+
await c.evaluate('cancelPointer=true;events=[]');await send('mousePressed',20,20);await send('mouseMoved',30,30);await send('mouseReleased',30,30);
|
|
41
|
+
expect((await c.evaluate('events')).map(e=>e[0])).toEqual(['pointerdown','pointermove','pointerup','click']);
|
|
42
|
+
await c.evaluate('cancelPointer=false;events=[]');
|
|
43
|
+
// The same canvas drawing contract as the manual test, checked before release.
|
|
44
|
+
const paint=await c.evaluate('paint.getBoundingClientRect().toJSON()');
|
|
45
|
+
await send('mousePressed',paint.x+20,paint.y+20);await send('mouseMoved',paint.x+40,paint.y+40);
|
|
46
|
+
expect(await c.evaluate('Array.from(paint.getContext("2d").getImageData(5,5,1,1).data)')).toEqual([255,0,0,255]);
|
|
47
|
+
await send('mouseReleased',paint.x+40,paint.y+40);await send('mouseMoved',20,20,{buttons:0,button:'none'});
|
|
48
|
+
expect((await c.evaluate('events')).at(-1)[3]).toBe(0);
|
|
49
|
+
}
|
|
50
|
+
test('CDP real pointer phases preserve dragging, focus and click targets',async()=>{
|
|
51
|
+
const b=await createBrowser({spareRenderer:false}),server=CdpServer.create(b.context).listen(0),c=new CDP(`ws://127.0.0.1:${server.port}/devtools/browser/cdp-server`);
|
|
52
|
+
try{await check(c)}finally{c.close();server.stop(true);b.close()}
|
|
53
|
+
},30000);
|
|
54
|
+
test.skipIf(process.env.BUNINU_TEST_CHROMIUM!=='1')('Chromium reference uses the same pointer event contract',async()=>{
|
|
55
|
+
const dir=mkdtempSync('/tmp/pointer-chromium-');let app;
|
|
56
|
+
try{app=await launch('chromium',dir);await check(app.cdp)}finally{await app?.stop();rmSync(dir,{recursive:true,force:true})}
|
|
57
|
+
},30000);
|
|
58
|
+
test('Wayland toolbar sends real press and motion before release, including over chrome',async()=>{
|
|
59
|
+
const b=await createBrowser({spareRenderer:false});const bar=new WaylandUrlBar(b.context,{history:{list:()=>[],add(){}}});
|
|
60
|
+
try{
|
|
61
|
+
await b.context.navigate('data:text/html,'+encodeURIComponent(source));
|
|
62
|
+
await bar.pointerButton(20,bar.height+20,272,1,{clickCount:1});
|
|
63
|
+
expect((await b.context.evaluate('events')).map(e=>e[0])).toEqual(['pointerdown','mousedown']);
|
|
64
|
+
await bar.move(40,bar.height+40);
|
|
65
|
+
expect((await b.context.evaluate('events')).at(-1)[3]).toBe(1);
|
|
66
|
+
await bar.pointerButton(40,0,272,0,{clickCount:1});
|
|
67
|
+
await bar.move(40,bar.height+40);
|
|
68
|
+
expect((await b.context.evaluate('events')).at(-1)[3]).toBe(0);
|
|
69
|
+
expect(bar.editor.editing).toBe(false);
|
|
70
|
+
}finally{bar.dispose();b.close()}
|
|
71
|
+
},30000);
|
|
72
|
+
|
|
73
|
+
test.skipIf(process.env.BUNINU_TEST_WAYLAND!=='1')('real Wayland pointer draws on the unchanged manual test page',async()=>{
|
|
74
|
+
const {start}=await import('../../buninu-land/compositor.js');
|
|
75
|
+
const {launch:wayland}=await import('../frontends/wayland/index.js');
|
|
76
|
+
const dir=mkdtempSync('/tmp/pointer-wayland-'),saved=process.env.XDG_RUNTIME_DIR;
|
|
77
|
+
process.env.XDG_RUNTIME_DIR=dir;let land,app;
|
|
78
|
+
const until=async fn=>{for(let n=0;n<200;n++){if(await fn())return;await Bun.sleep(20)}throw Error('Wayland pointer timed out')};
|
|
79
|
+
try{
|
|
80
|
+
land=await start(['--remote-debugging-port=0','--window-size=800,700']);
|
|
81
|
+
app=await wayland({socket:dir+'/wayland-buninu',url:Bun.pathToFileURL(new URL('./manual-pointer-scroll.html',import.meta.url).pathname).href});
|
|
82
|
+
const c=app.browser.context;
|
|
83
|
+
await until(()=>land.compositor.wm.renderSurfaces().some(s=>s.pixels));
|
|
84
|
+
const top=await c.evaluate('document.getElementById("paint").getBoundingClientRect().top');
|
|
85
|
+
await c.scroll(0,top-100);
|
|
86
|
+
const rect=await c.evaluate('document.getElementById("paint").getBoundingClientRect().toJSON()');
|
|
87
|
+
const surface=land.compositor.wm.renderSurfaces().find(s=>s.pixels),origin=land.compositor.wm.origin(surface);
|
|
88
|
+
const x=origin.x+rect.x+40,y=origin.y+app.toolbar.height+rect.y+40;
|
|
89
|
+
land.context.dispatchMouseEvent({type:'mouseMoved',x,y});
|
|
90
|
+
land.context.dispatchMouseEvent({type:'mousePressed',x,y,button:'left',buttons:1});
|
|
91
|
+
await until(async()=>(await c.evaluate('document.getElementById("drag").textContent'))==='按住左鍵');
|
|
92
|
+
land.context.dispatchMouseEvent({type:'mouseMoved',x:x+80,y:y+40,button:'left',buttons:1});
|
|
93
|
+
await until(async()=>await c.evaluate('(()=>{const c=document.getElementById("paint");return c.getContext("2d").getImageData(0,0,c.width,c.height).data.some((v,i)=>i%4===3&&v>0)})()'));
|
|
94
|
+
land.context.dispatchMouseEvent({type:'mouseReleased',x:x+80,y:y+40,button:'left',buttons:0});
|
|
95
|
+
await until(async()=>(await c.evaluate('document.getElementById("drag").textContent'))==='左鍵已放開');
|
|
96
|
+
expect(await c.evaluate('document.getElementById("log").textContent')).toContain('mousedown');
|
|
97
|
+
}finally{app?.close();land?.close();if(saved===undefined)delete process.env.XDG_RUNTIME_DIR;else process.env.XDG_RUNTIME_DIR=saved;rmSync(dir,{recursive:true,force:true})}
|
|
98
|
+
},30000);
|
|
99
|
+
|
|
100
|
+
test('pointer bursts preserve drawing points while batching raster work',async()=>{
|
|
101
|
+
const child=Bun.spawn([process.execPath,new URL('./fixtures/pointer-paint-batching.js',import.meta.url).pathname],{stdout:'pipe',stderr:'pipe'});
|
|
102
|
+
const [exit,stderr]=await Promise.all([child.exited,new Response(child.stderr).text()]);
|
|
103
|
+
expect(stderr).toBe('');expect(exit).toBe(0);
|
|
104
|
+
},15000);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
for (const mode of ['local', 'html', 'remote']) test(`DOM canvas snapshots: ${mode}`, async () => {
|
|
4
|
+
const child = Bun.spawn([process.execPath, new URL('./fixtures/qml-canvas-dom.js', import.meta.url).pathname, mode],
|
|
5
|
+
{ stdout: 'pipe', stderr: 'pipe' });
|
|
6
|
+
const [exit, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()]);
|
|
7
|
+
expect({ exit, stdout, stderr }).toEqual({ exit: 0, stdout: '', stderr: '' });
|
|
8
|
+
}, 30000);
|
|
9
|
+
|
|
10
|
+
// Native ownership must be released on both painting and argument failures.
|
|
11
|
+
import { captureQmlDomImage, drawQmlDomImage } from '../lib/qml/canvas-dom.js';
|
|
12
|
+
test('DOM snapshot surfaces and images are released on success and exceptions', () => {
|
|
13
|
+
const document = {};
|
|
14
|
+
const element = { nodeType: 1, ownerDocument: document, isConnected: true, contains: target => target === element };
|
|
15
|
+
const root = { id: 1, domNode: element, children: [], style: {} };
|
|
16
|
+
let disposed = 0, deleted = 0, drawn = 0;
|
|
17
|
+
const options = {
|
|
18
|
+
document, tree: { root, nodesById: new Map([[1, root]]) },
|
|
19
|
+
layout: { boxes: [{ nodeId: 1, x: 0, y: 0, width: 10, height: 10, style: {} }], fragments: [], width: 10, height: 10 },
|
|
20
|
+
resources: new Map(), isOriginClean: () => true, paint: () => {},
|
|
21
|
+
kit: { TRANSPARENT: 0, MakeSurface: () => ({ getCanvas: () => ({ clear() {} }), flush() {},
|
|
22
|
+
makeImageSnapshot: () => ({ delete() { deleted++; } }), dispose() { disposed++; } }) },
|
|
23
|
+
};
|
|
24
|
+
const first = captureQmlDomImage(element, options);
|
|
25
|
+
expect(disposed).toBe(1);expect(deleted).toBe(0);
|
|
26
|
+
expect(drawQmlDomImage(first, {}, { drawImage() { drawn++; } }, [element, 0, 0])).toBe(true);
|
|
27
|
+
expect(drawn).toBe(1);expect(deleted).toBe(1);
|
|
28
|
+
expect(drawQmlDomImage(first, {}, {}, [])).toBe(false);
|
|
29
|
+
const second = captureQmlDomImage(element, options);
|
|
30
|
+
expect(() => drawQmlDomImage(second, {}, { drawImage() { throw Error('invalid arguments'); } }, [])).toThrow('invalid arguments');
|
|
31
|
+
expect(deleted).toBe(2);
|
|
32
|
+
const third = captureQmlDomImage(element, options);
|
|
33
|
+
expect(() => drawQmlDomImage(third, element, {}, [])).toThrow('contains the destination');
|
|
34
|
+
expect(deleted).toBe(3);
|
|
35
|
+
expect(() => captureQmlDomImage(element, { ...options, paint() { throw Error('paint failed'); } })).toThrow('paint failed');
|
|
36
|
+
expect(disposed).toBe(4);expect(deleted).toBe(3);
|
|
37
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {RendererHost} from '../lib/renderer/host.js';
|
|
3
|
+
test('QML Canvas paints into HTML canvas and repaints after resize and property changes',async()=>{
|
|
4
|
+
const host=new RendererHost({timeout:20000});
|
|
5
|
+
try{
|
|
6
|
+
await host.start();
|
|
7
|
+
await host.call('loadDocument',{url:'data:text/x-qml,canvas',contentType:'text/x-qml',source:`import QtQuick
|
|
8
|
+
Canvas {id: c; width:16;height:16;property color ink:"red"
|
|
9
|
+
onInkChanged:requestPaint()
|
|
10
|
+
onPaint:{var ctx=getContext("2d");ctx.reset();ctx.fillStyle=ink;ctx.fillRect(0,0,width,height);globalThis.paints=(globalThis.paints||0)+1;}
|
|
11
|
+
Component.onCompleted:globalThis.canvasObject=c
|
|
12
|
+
}`});
|
|
13
|
+
await host.call('whenLoaded');
|
|
14
|
+
for(let i=0;i<100;i++){if(await host.call('evaluate','globalThis.paints>0'))break;await Bun.sleep(10);}
|
|
15
|
+
expect(await host.call('evaluate','Array.from(document.querySelector("canvas").getContext("2d").getImageData(8,8,1,1).data)')).toEqual([255,0,0,255]);
|
|
16
|
+
await host.call('evaluate','canvasObject.ink="blue";canvasObject.width=32;canvasObject.requestPaint();canvasObject.requestPaint()');
|
|
17
|
+
for(let i=0;i<100;i++){if(await host.call('evaluate','document.querySelector("canvas").width===32 && globalThis.paints>1'))break;await Bun.sleep(10);}
|
|
18
|
+
expect(await host.call('evaluate','Array.from(document.querySelector("canvas").getContext("2d").getImageData(24,8,1,1).data)')).toEqual([0,0,255,255]);
|
|
19
|
+
expect(await host.call('evaluate','canvasObject.available')).toBe(true);
|
|
20
|
+
}finally{host.close();}
|
|
21
|
+
},20000);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {attachQmlClipboard} from '../lib/qml/wayland-clipboard.js';
|
|
3
|
+
test('QML clipboard endpoint is lazy, reused and closed; HTML navigation needs none',async()=>{
|
|
4
|
+
let finish;const done=new Promise(r=>finish=r),scripts=[],visits=[];
|
|
5
|
+
const navigate=async url=>visits.push(url);
|
|
6
|
+
const browser={context:{navigate,addInitScript:async s=>scripts.push(s)}};
|
|
7
|
+
const client={clipboard:{read:async()=> 'text',write:async()=>{}}};
|
|
8
|
+
try{
|
|
9
|
+
await attachQmlClipboard(client,browser,done);
|
|
10
|
+
await browser.context.navigate('about:blank');
|
|
11
|
+
await browser.context.navigate('https://example.test/');
|
|
12
|
+
expect(scripts.length).toBe(0);
|
|
13
|
+
await browser.context.navigate('file:///tmp/app.qml');
|
|
14
|
+
expect(scripts.length).toBe(1);
|
|
15
|
+
await browser.context.navigate('file:///tmp/other.qml');
|
|
16
|
+
expect(scripts.length).toBe(1);
|
|
17
|
+
const endpoint=JSON.parse(scripts[0].match(/setClipboardEndpoint\((.*)\);/)[1]);
|
|
18
|
+
const response=await fetch(endpoint,{method:'POST',body:JSON.stringify({operation:'read'})});
|
|
19
|
+
expect(await response.json()).toEqual({text:'text'});
|
|
20
|
+
expect(visits.length).toBe(4);
|
|
21
|
+
finish();await Bun.sleep(5);
|
|
22
|
+
expect(browser.context.navigate).toBe(navigate);
|
|
23
|
+
await expect(fetch(endpoint,{method:'POST',body:'{}'})).rejects.toThrow();
|
|
24
|
+
}finally{finish();}
|
|
25
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {mkdtempSync,rmSync} from 'node:fs';
|
|
3
|
+
import {join} from 'node:path';
|
|
4
|
+
import {launch} from '../frontends/wayland/index.js';
|
|
5
|
+
test.skipIf(process.env.BUNINU_TEST_WAYLAND!=='1')('QML uses shared input defaults and exchanges text through Land on demand',async()=>{
|
|
6
|
+
const {start}=await import('../../buninu-land/compositor.js');
|
|
7
|
+
const dir=mkdtempSync('/tmp/qml-clipboard-'),old=process.env.XDG_RUNTIME_DIR;
|
|
8
|
+
let land,app;const writes=[];
|
|
9
|
+
const wait=async fn=>{for(let i=0;i<200;i++){if(await fn())return;await Bun.sleep(25);}throw Error('Clipboard did not settle');};
|
|
10
|
+
try {
|
|
11
|
+
process.env.XDG_RUNTIME_DIR=dir;
|
|
12
|
+
await Bun.write(join(dir,'main.qml'),`import QtQuick
|
|
13
|
+
Rectangle {width:400;height:150;color:"white"
|
|
14
|
+
TextInput {id:field;x:10;y:10;width:300;height:30;text:"abc中文"}
|
|
15
|
+
Button {id:all;x:10;y:70;onClicked:{field.forceActiveFocus();field.selectAll()}}
|
|
16
|
+
Button {id:copy;x:100;y:70;onClicked:field.copy()}
|
|
17
|
+
}`);
|
|
18
|
+
land=await start(['--remote-debugging-port=0','--window-size=640,480']);
|
|
19
|
+
land.compositor.clipboard.host={write:async text=>writes.push(text),read:async()=>{throw Error('Unexpected external read');},close(){}};
|
|
20
|
+
app=await launch({url:'about:blank',chrome:false,socket:join(dir,'wayland-buninu')});
|
|
21
|
+
await app.browser.context.navigate(Bun.pathToFileURL(join(dir,'main.qml')).href);
|
|
22
|
+
const run=s=>app.browser.context.evaluate(s);
|
|
23
|
+
await wait(()=>app.client.clipboard.serial>0);
|
|
24
|
+
await run(`document.getElementById('all').click()`);
|
|
25
|
+
const key=async(key,modifiers=0)=>{
|
|
26
|
+
for(const type of ['keyDown','keyUp'])land.context.dispatchKeyEvent({type,key,code:'Key'+key.toUpperCase(),modifiers});
|
|
27
|
+
await Bun.sleep(100);
|
|
28
|
+
};
|
|
29
|
+
await key('c',2);await wait(()=>writes.length>0);expect(writes.at(-1)).toBe('abc中文');
|
|
30
|
+
await key('ArrowRight');await key('v',2);
|
|
31
|
+
await wait(async()=>await run(`document.getElementById('field').value`)==='abc中文abc中文');
|
|
32
|
+
await key('a',2);await key('x',2);
|
|
33
|
+
await wait(async()=>await run(`document.getElementById('field').value`)==='');
|
|
34
|
+
expect(writes.at(-1)).toBe('abc中文abc中文');
|
|
35
|
+
await key('v',2);await wait(async()=>await run(`document.getElementById('field').value`)==='abc中文abc中文');
|
|
36
|
+
await run(`document.getElementById('all').click()`);
|
|
37
|
+
const before=writes.length;await run(`document.getElementById('copy').click()`);
|
|
38
|
+
await wait(()=>writes.length>before);expect(writes.at(-1)).toBe('abc中文abc中文');
|
|
39
|
+
}finally{app?.close();land?.close();if(old===undefined)delete process.env.XDG_RUNTIME_DIR;else process.env.XDG_RUNTIME_DIR=old;rmSync(dir,{recursive:true,force:true});}
|
|
40
|
+
},30000);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {RendererHost} from '../lib/renderer/host.js';
|
|
3
|
+
|
|
4
|
+
test('Connections catches first synchronous Loader request and follows same-handler target/enabled changes',async()=>{
|
|
5
|
+
const host=new RendererHost({timeout:20000});
|
|
6
|
+
try{
|
|
7
|
+
await host.start();
|
|
8
|
+
await host.call('loadDocument',{url:'data:text/x-qml,connections-timing',contentType:'text/x-qml',source:`import QtQuick
|
|
9
|
+
Rectangle {
|
|
10
|
+
id:root;width:300;height:200;property int received:0;property bool enabledLink:true;property int phase:0
|
|
11
|
+
Component {id:dialog;Item {signal requested();function open(){requested()}}}
|
|
12
|
+
Loader {id:loader;active:false;sourceComponent:dialog}
|
|
13
|
+
Connections {target:loader.item;enabled:root.enabledLink;function onRequested(){root.received++}}
|
|
14
|
+
Text {id:status;text:root.received}
|
|
15
|
+
onClicked:{
|
|
16
|
+
if(phase===0){loader.active=true;loader.item.open()}
|
|
17
|
+
else if(phase===1){root.enabledLink=false;loader.item.open()}
|
|
18
|
+
else if(phase===2){root.enabledLink=true;loader.item.open()}
|
|
19
|
+
else if(phase===3){loader.active=false;loader.active=true;loader.item.open()}
|
|
20
|
+
else loader.item.open()
|
|
21
|
+
phase++
|
|
22
|
+
}
|
|
23
|
+
}`});
|
|
24
|
+
await host.call('whenLoaded');
|
|
25
|
+
for(const count of [1,1,2,3,4]){
|
|
26
|
+
await host.call('evaluate',"document.getElementById('root').click()");
|
|
27
|
+
expect(await host.call('evaluate',"Number(document.getElementById('status').textContent)")).toBe(count);
|
|
28
|
+
}
|
|
29
|
+
}finally{host.close()}
|
|
30
|
+
},30000);
|
|
31
|
+
|
|
32
|
+
test('synchronous Loader reaches indirect item bindings before open returns',async()=>{
|
|
33
|
+
const host=new RendererHost({timeout:20000});
|
|
34
|
+
try{
|
|
35
|
+
await host.start();
|
|
36
|
+
await host.call('loadDocument',{url:'data:text/x-qml,loader-chain',contentType:'text/x-qml',source:`import QtQuick
|
|
37
|
+
Rectangle {
|
|
38
|
+
id:root;width:300;height:200;property int received:0
|
|
39
|
+
readonly property var quickLook:loader.item
|
|
40
|
+
property var preview:root.quickLook
|
|
41
|
+
Component {id:dialog;Item {signal requested();function open(){requested()}}}
|
|
42
|
+
Loader {id:loader;active:false;sourceComponent:dialog}
|
|
43
|
+
Connections {target:loader.item;function onRequested(){root.received++}}
|
|
44
|
+
Text {id:status;text:root.received}
|
|
45
|
+
onClicked:{loader.active=true;root.preview.open()}
|
|
46
|
+
}`});
|
|
47
|
+
await host.call('whenLoaded');
|
|
48
|
+
await host.call('evaluate',"document.getElementById('root').click()");
|
|
49
|
+
expect(await host.call('evaluate',"Number(document.getElementById('status').textContent)")).toBe(1);
|
|
50
|
+
}finally{host.close()}
|
|
51
|
+
},30000);
|
|
52
|
+
|
|
53
|
+
test('initial property handlers can call child ListView methods in a lazy component',async()=>{
|
|
54
|
+
const host=new RendererHost({timeout:20000});
|
|
55
|
+
try{
|
|
56
|
+
await host.start();
|
|
57
|
+
await host.call('loadDocument',{url:'data:text/x-qml,loader-child-methods',contentType:'text/x-qml',source:`import QtQuick
|
|
58
|
+
Rectangle {
|
|
59
|
+
id:root;width:300;height:200;property int result:-9
|
|
60
|
+
Component {id:preview;Item {
|
|
61
|
+
property bool wrap:true
|
|
62
|
+
onWrapChanged:root.result=list.indexAt(0,1)
|
|
63
|
+
ListView {id:list;width:100;height:100;model:2;delegate:Rectangle {height:20;width:100}}
|
|
64
|
+
}}
|
|
65
|
+
Loader {id:loader;active:false;sourceComponent:preview}
|
|
66
|
+
Text {id:status;text:root.result}
|
|
67
|
+
onClicked:loader.active=true
|
|
68
|
+
}`});
|
|
69
|
+
await host.call('whenLoaded');
|
|
70
|
+
await host.call('evaluate',"document.getElementById('root').click()");
|
|
71
|
+
expect(await host.call('evaluate',"Number(document.getElementById('status').textContent)")).toBe(0);
|
|
72
|
+
}finally{host.close()}
|
|
73
|
+
},30000);
|
|
74
|
+
|
|
75
|
+
test('property Changed.connect synchronizes menu state before same-handler validation',async()=>{
|
|
76
|
+
const host=new RendererHost({timeout:20000});
|
|
77
|
+
try{
|
|
78
|
+
await host.start();
|
|
79
|
+
await host.call('loadDocument',{url:'data:text/x-qml,menu-validation',contentType:'text/x-qml',source:`import QtQuick
|
|
80
|
+
Rectangle {
|
|
81
|
+
id:root;width:300;height:200;property int result:-1;property int changes:0
|
|
82
|
+
property bool preparing:false
|
|
83
|
+
readonly property bool live:root.preparing || menu.opened
|
|
84
|
+
Item {id:menu;property bool preparing:false;property bool opened:false;property int count:root.live ? 1 : 0}
|
|
85
|
+
Component.onCompleted:menu.preparingChanged.connect(function(){root.preparing=menu.preparing;root.changes++})
|
|
86
|
+
Text {id:status;text:root.result+":"+root.changes+":"+menu.count}
|
|
87
|
+
onClicked:{menu.preparing=true;root.result=menu.count;menu.preparing=false}
|
|
88
|
+
}`});
|
|
89
|
+
await host.call('whenLoaded');
|
|
90
|
+
for(const changes of [2,4,6]){
|
|
91
|
+
await host.call('evaluate',"document.getElementById('root').click()");
|
|
92
|
+
expect(await host.call('evaluate',"document.getElementById('status').textContent")).toBe('1:'+changes+':0');
|
|
93
|
+
}
|
|
94
|
+
}finally{host.close()}
|
|
95
|
+
},30000);
|