tests(e2e): improve coverage

This commit is contained in:
Lucas Nogueira
2026-09-22 21:54:21 -03:00
parent 684feb2510
commit c8c373b48b
50 changed files with 1893 additions and 183 deletions
@@ -0,0 +1,11 @@
---
notification: patch
notification-js: patch
---
Fixed broken mobile APIs:
- `channels()` invoked the `listChannels` command, which the `allow-list-channels` permission does not match; it now invokes `list_channels`.
- Unregistering an `onNotificationReceived` or `onAction` listener invokes the `remove_listener` command, which had no permission. It is now declared and part of the `default` permission set (`allow-remove-listener`).
- `cancelAll()` was rejected on Android and iOS, which required the list of notifications to cancel. It now cancels every pending notification.
- `active()` resolved with `{ values: [...] }` instead of an array on Android.
@@ -130,6 +130,9 @@ jobs:
working-directory: ./examples/api working-directory: ./examples/api
env: env:
NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
# a debug build without debug info, which would otherwise grow the APK
# past what the emulator can install (see androidBuildEnv in wdio.mobile.ts)
CARGO_PROFILE_DEV_DEBUG: '0'
run: pnpm tauri android build --debug --apk --target $E2E_ANDROID_TARGET run: pnpm tauri android build --debug --apk --target $E2E_ANDROID_TARGET
- name: save gradle cache - name: save gradle cache
+3 -1
View File
@@ -79,7 +79,9 @@ jobs:
librsvg2-dev \ librsvg2-dev \
dbus \ dbus \
xvfb \ xvfb \
fluxbox fluxbox \
desktop-file-utils \
xdg-utils
# msedgedriver has to match the installed WebView2 runtime, so msedgedriver-tool # msedgedriver has to match the installed WebView2 runtime, so msedgedriver-tool
# downloads the right version at run time. The tool itself is built from a pinned git # downloads the right version at run time. The tool itself is built from a pinned git
Generated
+88 -52
View File
@@ -215,10 +215,12 @@ dependencies = [
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-automation", "tauri-plugin-automation",
"tauri-plugin-autostart",
"tauri-plugin-barcode-scanner", "tauri-plugin-barcode-scanner",
"tauri-plugin-biometric", "tauri-plugin-biometric",
"tauri-plugin-cli", "tauri-plugin-cli",
"tauri-plugin-clipboard-manager", "tauri-plugin-clipboard-manager",
"tauri-plugin-deep-link",
"tauri-plugin-dialog", "tauri-plugin-dialog",
"tauri-plugin-fs", "tauri-plugin-fs",
"tauri-plugin-geolocation", "tauri-plugin-geolocation",
@@ -230,11 +232,15 @@ dependencies = [
"tauri-plugin-notification", "tauri-plugin-notification",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-os", "tauri-plugin-os",
"tauri-plugin-positioner",
"tauri-plugin-process", "tauri-plugin-process",
"tauri-plugin-shell", "tauri-plugin-shell",
"tauri-plugin-sql",
"tauri-plugin-store", "tauri-plugin-store",
"tauri-plugin-stronghold",
"tauri-plugin-updater", "tauri-plugin-updater",
"tauri-plugin-upload", "tauri-plugin-upload",
"tauri-plugin-websocket",
"tauri-plugin-window-state", "tauri-plugin-window-state",
"time", "time",
"tiny_http", "tiny_http",
@@ -298,7 +304,7 @@ dependencies = [
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-core-graphics", "objc2-core-graphics",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
"parking_lot", "parking_lot",
"percent-encoding", "percent-encoding",
"windows-sys 0.59.0", "windows-sys 0.59.0",
@@ -1554,7 +1560,7 @@ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users 0.5.0", "redox_users 0.5.0",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -1597,7 +1603,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [ dependencies = [
"libloading 0.7.4", "libloading 0.8.9",
] ]
[[package]] [[package]]
@@ -3509,7 +3515,7 @@ checksum = "0b95dfb34071d1592b45622bf93e315e3a72d414b6782aca9a015c12bec367ef"
dependencies = [ dependencies = [
"cc", "cc",
"objc2 0.6.4", "objc2 0.6.4",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
"time", "time",
] ]
@@ -3645,12 +3651,12 @@ dependencies = [
"objc2 0.6.4", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
"once_cell", "once_cell",
"png 0.18.1", "png 0.18.1",
"serde", "serde",
"thiserror 2.0.12", "thiserror 2.0.12",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -3900,9 +3906,9 @@ dependencies = [
[[package]] [[package]]
name = "objc2-app-kit" name = "objc2-app-kit"
version = "0.3.0" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"block2 0.6.2", "block2 0.6.2",
@@ -3913,49 +3919,53 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
"objc2-core-graphics", "objc2-core-graphics",
"objc2-core-image", "objc2-core-image",
"objc2-foundation 0.3.0", "objc2-core-text",
"objc2-quartz-core 0.3.0", "objc2-core-video",
"objc2-foundation 0.3.2",
"objc2-quartz-core 0.3.2",
] ]
[[package]] [[package]]
name = "objc2-cloud-kit" name = "objc2-cloud-kit"
version = "0.3.0" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c1948a9be5f469deadbd6bcb86ad7ff9e47b4f632380139722f7d9840c0d42c" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"objc2 0.6.4", "objc2 0.6.4",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
] ]
[[package]] [[package]]
name = "objc2-core-data" name = "objc2-core-data"
version = "0.3.0" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"objc2 0.6.4", "objc2 0.6.4",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
] ]
[[package]] [[package]]
name = "objc2-core-foundation" name = "objc2-core-foundation"
version = "0.3.0" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"dispatch2",
"objc2 0.6.4", "objc2 0.6.4",
] ]
[[package]] [[package]]
name = "objc2-core-graphics" name = "objc2-core-graphics"
version = "0.3.0" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"dispatch2",
"objc2 0.6.4", "objc2 0.6.4",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-io-surface", "objc2-io-surface",
@@ -3963,12 +3973,12 @@ dependencies = [
[[package]] [[package]]
name = "objc2-core-image" name = "objc2-core-image"
version = "0.3.0" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ffa6bea72bf42c78b0b34e89c0bafac877d5f80bf91e159a5d96ea7f693ca56" checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
dependencies = [ dependencies = [
"objc2 0.6.4", "objc2 0.6.4",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
] ]
[[package]] [[package]]
@@ -3978,7 +3988,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d31f4c5b5192304996badc466aeadffe1411d73a9bbd3b18b6b2ee9d048b07bd" checksum = "d31f4c5b5192304996badc466aeadffe1411d73a9bbd3b18b6b2ee9d048b07bd"
dependencies = [ dependencies = [
"objc2 0.6.4", "objc2 0.6.4",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
]
[[package]]
name = "objc2-core-text"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-core-graphics",
]
[[package]]
name = "objc2-core-video"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-io-surface",
] ]
[[package]] [[package]]
@@ -4010,9 +4045,9 @@ dependencies = [
[[package]] [[package]]
name = "objc2-foundation" name = "objc2-foundation"
version = "0.3.0" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"block2 0.6.2", "block2 0.6.2",
@@ -4023,9 +4058,9 @@ dependencies = [
[[package]] [[package]]
name = "objc2-io-surface" name = "objc2-io-surface"
version = "0.3.0" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"objc2 0.6.4", "objc2 0.6.4",
@@ -4053,7 +4088,7 @@ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"objc2 0.6.4", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
] ]
[[package]] [[package]]
@@ -4071,14 +4106,14 @@ dependencies = [
[[package]] [[package]]
name = "objc2-quartz-core" name = "objc2-quartz-core"
version = "0.3.0" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb3794501bb1bee12f08dcad8c61f2a5875791ad1c6f47faa71a0f033f20071" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"objc2 0.6.4", "objc2 0.6.4",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
] ]
[[package]] [[package]]
@@ -4096,8 +4131,8 @@ dependencies = [
"objc2-core-graphics", "objc2-core-graphics",
"objc2-core-image", "objc2-core-image",
"objc2-core-location", "objc2-core-location",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
"objc2-quartz-core 0.3.0", "objc2-quartz-core 0.3.2",
"objc2-user-notifications", "objc2-user-notifications",
] ]
@@ -4108,7 +4143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670fe793adbf3b5e93686d48a05a7ed7ee53dfa65d106ced4805fae8969059b2" checksum = "670fe793adbf3b5e93686d48a05a7ed7ee53dfa65d106ced4805fae8969059b2"
dependencies = [ dependencies = [
"objc2 0.6.4", "objc2 0.6.4",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
] ]
[[package]] [[package]]
@@ -4122,7 +4157,7 @@ dependencies = [
"objc2 0.6.4", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
] ]
[[package]] [[package]]
@@ -4266,7 +4301,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [ dependencies = [
"objc2 0.6.4", "objc2 0.6.4",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
"objc2-osa-kit", "objc2-osa-kit",
"serde", "serde",
"serde_json", "serde_json",
@@ -5044,7 +5079,7 @@ dependencies = [
"objc2 0.6.4", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
"pollster", "pollster",
"raw-window-handle", "raw-window-handle",
"urlencoding", "urlencoding",
@@ -5218,7 +5253,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.11.0", "linux-raw-sys 0.11.0",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -5275,7 +5310,7 @@ dependencies = [
"security-framework 3.5.1", "security-framework 3.5.1",
"security-framework-sys", "security-framework-sys",
"webpki-root-certs", "webpki-root-certs",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -6297,7 +6332,7 @@ dependencies = [
"ndk-sys", "ndk-sys",
"objc2 0.6.4", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
"objc2-ui-kit", "objc2-ui-kit",
"once_cell", "once_cell",
"parking_lot", "parking_lot",
@@ -6372,7 +6407,7 @@ dependencies = [
"muda", "muda",
"objc2 0.6.4", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
"objc2-ui-kit", "objc2-ui-kit",
"objc2-web-kit", "objc2-web-kit",
"percent-encoding", "percent-encoding",
@@ -6483,12 +6518,13 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-automation" name = "tauri-plugin-automation"
version = "0.1.4" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "314d4ba88ce4a5e17c5f1e2dad990edb5956643b6e04a6d127d39aa64d0f2a7d" checksum = "40015ce24bdb1204983f83ac37c5469d55028de1e7fc56cdd0a6a7e15a1b5375"
dependencies = [ dependencies = [
"libloading 0.8.9", "libloading 0.8.9",
"objc2-app-kit", "objc2-app-kit",
"serde",
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
@@ -6604,7 +6640,7 @@ dependencies = [
"log", "log",
"notify", "notify",
"notify-debouncer-full", "notify-debouncer-full",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
"percent-encoding", "percent-encoding",
"schemars", "schemars",
"serde", "serde",
@@ -6701,7 +6737,7 @@ dependencies = [
"fern", "fern",
"log", "log",
"objc2 0.6.4", "objc2 0.6.4",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
"serde", "serde",
"serde_json", "serde_json",
"serde_repr", "serde_repr",
@@ -6752,7 +6788,7 @@ dependencies = [
"dunce", "dunce",
"glob", "glob",
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
"open", "open",
"schemars", "schemars",
"serde", "serde",
@@ -7108,7 +7144,7 @@ dependencies = [
"getrandom 0.3.2", "getrandom 0.3.2",
"once_cell", "once_cell",
"rustix 1.1.3", "rustix 1.1.3",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -7535,12 +7571,12 @@ dependencies = [
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-core-graphics", "objc2-core-graphics",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
"once_cell", "once_cell",
"png 0.18.1", "png 0.18.1",
"serde", "serde",
"thiserror 2.0.12", "thiserror 2.0.12",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -8261,7 +8297,7 @@ dependencies = [
"objc2 0.6.4", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
"raw-window-handle", "raw-window-handle",
"windows-sys 0.59.0", "windows-sys 0.59.0",
"windows-version", "windows-version",
@@ -8840,7 +8876,7 @@ dependencies = [
"objc2 0.6.4", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation 0.3.0", "objc2-foundation 0.3.2",
"objc2-ui-kit", "objc2-ui-kit",
"objc2-web-kit", "objc2-web-kit",
"once_cell", "once_cell",
+17
View File
@@ -32,6 +32,23 @@ license = "Apache-2.0 OR MIT"
rust-version = "1.77.2" rust-version = "1.77.2"
repository = "https://github.com/tauri-apps/plugins-workspace" repository = "https://github.com/tauri-apps/plugins-workspace"
# Stronghold encrypts its snapshots with scrypt: unoptimized, saving or loading one
# takes ~45s instead of ~1s, which stalls the (debug) example app and its e2e suite.
[profile.dev.package.scrypt]
opt-level = 3
[profile.dev.package.salsa20]
opt-level = 3
[profile.dev.package.pbkdf2]
opt-level = 3
[profile.dev.package.sha2]
opt-level = 3
[profile.dev.package.hmac]
opt-level = 3
[profile.dev.package.iota-crypto]
opt-level = 3
[profile.dev.package.rust-argon2]
opt-level = 3
# default to small, optimized release binaries # default to small, optimized release binaries
[profile.release] [profile.release]
panic = "abort" panic = "abort"
+6
View File
@@ -11,10 +11,12 @@
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.11.0", "@tauri-apps/api": "^2.11.0",
"@tauri-apps/plugin-autostart": "workspace:*",
"@tauri-apps/plugin-barcode-scanner": "workspace:*", "@tauri-apps/plugin-barcode-scanner": "workspace:*",
"@tauri-apps/plugin-biometric": "workspace:*", "@tauri-apps/plugin-biometric": "workspace:*",
"@tauri-apps/plugin-cli": "workspace:*", "@tauri-apps/plugin-cli": "workspace:*",
"@tauri-apps/plugin-clipboard-manager": "workspace:*", "@tauri-apps/plugin-clipboard-manager": "workspace:*",
"@tauri-apps/plugin-deep-link": "workspace:*",
"@tauri-apps/plugin-dialog": "workspace:*", "@tauri-apps/plugin-dialog": "workspace:*",
"@tauri-apps/plugin-fs": "workspace:*", "@tauri-apps/plugin-fs": "workspace:*",
"@tauri-apps/plugin-geolocation": "workspace:*", "@tauri-apps/plugin-geolocation": "workspace:*",
@@ -25,11 +27,15 @@
"@tauri-apps/plugin-notification": "workspace:*", "@tauri-apps/plugin-notification": "workspace:*",
"@tauri-apps/plugin-opener": "workspace:*", "@tauri-apps/plugin-opener": "workspace:*",
"@tauri-apps/plugin-os": "workspace:*", "@tauri-apps/plugin-os": "workspace:*",
"@tauri-apps/plugin-positioner": "workspace:*",
"@tauri-apps/plugin-process": "workspace:*", "@tauri-apps/plugin-process": "workspace:*",
"@tauri-apps/plugin-shell": "workspace:*", "@tauri-apps/plugin-shell": "workspace:*",
"@tauri-apps/plugin-sql": "workspace:*",
"@tauri-apps/plugin-store": "workspace:*", "@tauri-apps/plugin-store": "workspace:*",
"@tauri-apps/plugin-stronghold": "workspace:*",
"@tauri-apps/plugin-updater": "workspace:*", "@tauri-apps/plugin-updater": "workspace:*",
"@tauri-apps/plugin-upload": "workspace:*", "@tauri-apps/plugin-upload": "workspace:*",
"@tauri-apps/plugin-websocket": "workspace:*",
"@zerodevx/svelte-json-view": "2.0.0" "@zerodevx/svelte-json-view": "2.0.0"
}, },
"devDependencies": { "devDependencies": {
+11 -1
View File
@@ -39,12 +39,18 @@ tauri-plugin-opener = { path = "../../../plugins/opener", version = "2.5.5" }
tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.3.6" } tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.3.6" }
tauri-plugin-store = { path = "../../../plugins/store", version = "2.4.5" } tauri-plugin-store = { path = "../../../plugins/store", version = "2.4.5" }
tauri-plugin-upload = { path = "../../../plugins/upload", version = "2.3.0" } tauri-plugin-upload = { path = "../../../plugins/upload", version = "2.3.0" }
tauri-plugin-deep-link = { path = "../../../plugins/deep-link", version = "2.4.10" }
tauri-plugin-sql = { path = "../../../plugins/sql", version = "2.4.1", features = [
"sqlite",
] }
tauri-plugin-stronghold = { path = "../../../plugins/stronghold", version = "2.3.2" }
tauri-plugin-websocket = { path = "../../../plugins/websocket", version = "2.4.3" }
# WebDriver automation bridge, used by the plugins e2e suite (packages/api-e2e). # WebDriver automation bridge, used by the plugins e2e suite (packages/api-e2e).
# Desktop-only and behind the off-by-default `automation` feature so it never ships in a # Desktop-only and behind the off-by-default `automation` feature so it never ships in a
# regular build. # regular build.
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-automation = { version = "0.1.4", optional = true } tauri-plugin-automation = { version = "0.2.0", optional = true }
[dependencies.tauri] [dependencies.tauri]
workspace = true workspace = true
@@ -61,8 +67,12 @@ features = [
] ]
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
tauri-plugin-autostart = { path = "../../../plugins/autostart", version = "2.5.1" }
tauri-plugin-cli = { path = "../../../plugins/cli", version = "2.4.1" } tauri-plugin-cli = { path = "../../../plugins/cli", version = "2.4.1" }
tauri-plugin-global-shortcut = { path = "../../../plugins/global-shortcut", version = "2.3.2" } tauri-plugin-global-shortcut = { path = "../../../plugins/global-shortcut", version = "2.3.2" }
tauri-plugin-positioner = { path = "../../../plugins/positioner", version = "2.3.4", features = [
"tray-icon",
] }
tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.12.0" } tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.12.0" }
tauri-plugin-window-state = { path = "../../../plugins/window-state", version = "2.2.0" } tauri-plugin-window-state = { path = "../../../plugins/window-state", version = "2.2.0" }
+12 -1
View File
@@ -132,6 +132,17 @@
"identifier": "opener:allow-open-path", "identifier": "opener:allow-open-path",
"allow": [{ "path": "$APPDATA" }, { "path": "$APPDATA/**" }] "allow": [{ "path": "$APPDATA" }, { "path": "$APPDATA/**" }]
}, },
"upload:default" "upload:default",
"deep-link:default",
"deep-link:allow-register",
"deep-link:allow-unregister",
"deep-link:allow-is-registered",
"sql:default",
"sql:allow-execute",
"stronghold:default",
"stronghold:allow-destroy",
"stronghold:allow-remove-secret",
"stronghold:allow-remove-store-record",
"websocket:default"
] ]
} }
@@ -5,7 +5,9 @@
"windows": ["main"], "windows": ["main"],
"platforms": ["linux", "macOS", "windows"], "platforms": ["linux", "macOS", "windows"],
"permissions": [ "permissions": [
"autostart:default",
"cli:default", "cli:default",
"positioner:default",
"updater:default", "updater:default",
"global-shortcut:allow-unregister", "global-shortcut:allow-unregister",
"global-shortcut:allow-register", "global-shortcut:allow-register",
+30 -1
View File
@@ -9,7 +9,7 @@ mod tray;
use serde::Serialize; use serde::Serialize;
use tauri::{ use tauri::{
webview::{PageLoadEvent, WebviewWindowBuilder}, webview::{PageLoadEvent, WebviewWindowBuilder},
App, AppHandle, Emitter, Listener, RunEvent, WebviewUrl, App, AppHandle, Emitter, Listener, Manager, RunEvent, WebviewUrl,
}; };
#[derive(Clone, Serialize)] #[derive(Clone, Serialize)]
@@ -55,11 +55,40 @@ pub fn run() {
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_store::Builder::default().build()) .plugin(tauri_plugin_store::Builder::default().build())
.plugin(tauri_plugin_upload::init()) .plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_websocket::init())
.plugin(
tauri_plugin_sql::Builder::new()
.add_migrations(
"sqlite:api.db",
vec![tauri_plugin_sql::Migration {
version: 1,
description: "create_todos_table",
sql: "CREATE TABLE todos (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0);",
kind: tauri_plugin_sql::MigrationKind::Up,
}],
)
.build(),
)
.setup(move |app| { .setup(move |app| {
// the argon2 salt lives next to the snapshots the frontend creates
let local_data_dir = app.path().app_local_data_dir()?;
std::fs::create_dir_all(&local_data_dir)?;
app.handle().plugin(
tauri_plugin_stronghold::Builder::with_argon2(&local_data_dir.join("salt.txt"))
.build(),
)?;
#[cfg(desktop)] #[cfg(desktop)]
{ {
// registered before the tray, whose events it tracks
app.handle().plugin(tauri_plugin_positioner::init())?;
tray::create_tray(app.handle())?; tray::create_tray(app.handle())?;
app.handle().plugin(tauri_plugin_cli::init())?; app.handle().plugin(tauri_plugin_cli::init())?;
app.handle().plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
None,
))?;
app.handle() app.handle()
.plugin(tauri_plugin_global_shortcut::Builder::new().build())?; .plugin(tauri_plugin_global_shortcut::Builder::new().build())?;
app.handle() app.handle()
+2
View File
@@ -107,6 +107,8 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
_ => {} _ => {}
}) })
.on_tray_icon_event(|tray, event| { .on_tray_icon_event(|tray, event| {
// lets the positioner plugin place windows relative to the tray icon
tauri_plugin_positioner::on_tray_event(tray.app_handle(), &event);
if let TrayIconEvent::Click { if let TrayIconEvent::Click {
button_state: MouseButtonState::Down, button_state: MouseButtonState::Down,
button: MouseButton::Left, button: MouseButton::Left,
+36
View File
@@ -33,6 +33,12 @@
import Geolocation from './views/Geolocation.svelte' import Geolocation from './views/Geolocation.svelte'
import Haptics from './views/Haptics.svelte' import Haptics from './views/Haptics.svelte'
import Nfc from './views/Nfc.svelte' import Nfc from './views/Nfc.svelte'
import Autostart from './views/Autostart.svelte'
import DeepLink from './views/DeepLink.svelte'
import Positioner from './views/Positioner.svelte'
import Sql from './views/Sql.svelte'
import Stronghold from './views/Stronghold.svelte'
import WebSocket from './views/WebSocket.svelte'
import TitleBar from './lib/TitleBar.svelte' import TitleBar from './lib/TitleBar.svelte'
@@ -131,6 +137,36 @@
component: WebRTC, component: WebRTC,
icon: 'i-ph-broadcast' icon: 'i-ph-broadcast'
}, },
{
label: 'WebSocket',
component: WebSocket,
icon: 'i-ph-plugs-connected'
},
{
label: 'SQL',
component: Sql,
icon: 'i-ph-database'
},
{
label: 'Stronghold',
component: Stronghold,
icon: 'i-ph-lock-key'
},
{
label: 'Deep link',
component: DeepLink,
icon: 'i-ph-link'
},
!isMobile && {
label: 'Autostart',
component: Autostart,
icon: 'i-ph-power'
},
!isMobile && {
label: 'Positioner',
component: Positioner,
icon: 'i-ph-arrows-out-cardinal'
},
isMobile && { isMobile && {
label: 'Scanner', label: 'Scanner',
component: Scanner, component: Scanner,
+30
View File
@@ -0,0 +1,30 @@
<script>
import { enable, disable, isEnabled } from '@tauri-apps/plugin-autostart'
import { onMount } from 'svelte'
export let onMessage
let enabled = false
async function refresh() {
enabled = await isEnabled()
}
function toggle() {
;(enabled ? disable() : enable())
.then(refresh)
.then(() => onMessage(`Autostart ${enabled ? 'enabled' : 'disabled'}`))
.catch(onMessage)
}
onMount(() => {
refresh().catch(onMessage)
})
</script>
<div class="flex flex-row gap-2 items-center">
<button class="btn" on:click={toggle}>
{enabled ? 'Disable' : 'Enable'} autostart
</button>
<span>Launching at login is {enabled ? 'enabled' : 'disabled'}</span>
</div>
+67
View File
@@ -0,0 +1,67 @@
<script>
import {
getCurrent,
onOpenUrl,
register,
unregister,
isRegistered
} from '@tauri-apps/plugin-deep-link'
import { onMount, onDestroy } from 'svelte'
export let onMessage
let current = null
let protocol = 'tauri-api'
let unlisten
onMount(async () => {
try {
current = await getCurrent()
unlisten = await onOpenUrl((urls) => {
current = urls
onMessage(`Opened with ${urls.join(', ')}`)
})
} catch (error) {
onMessage(error)
}
})
onDestroy(() => {
unlisten?.()
})
function registerProtocol() {
register(protocol)
.then(() => onMessage(`Registered ${protocol}://`))
.catch(onMessage)
}
function unregisterProtocol() {
unregister(protocol)
.then(() => onMessage(`Unregistered ${protocol}://`))
.catch(onMessage)
}
function checkProtocol() {
isRegistered(protocol)
.then((registered) =>
onMessage(
`${protocol}:// is ${registered ? '' : 'not '}handled by this app`
)
)
.catch(onMessage)
}
</script>
<div class="flex flex-col gap-2">
<div>
Current deep link: <code>{current ? current.join(', ') : 'none'}</code>
</div>
<div class="flex flex-row gap-2 items-center">
<input class="input grow" placeholder="Scheme" bind:value={protocol} />
<button class="btn" on:click={registerProtocol}>Register</button>
<button class="btn" on:click={unregisterProtocol}>Unregister</button>
<button class="btn" on:click={checkProtocol}>Is registered?</button>
</div>
</div>
+40
View File
@@ -0,0 +1,40 @@
<script>
import {
moveWindow,
moveWindowConstrained,
Position
} from '@tauri-apps/plugin-positioner'
export let onMessage
// `Position` is a numeric enum, so it also maps the values back to their names
const positions = Object.keys(Position).filter((key) => isNaN(Number(key)))
let position = 'Center'
let constrained = false
function move() {
;(constrained ? moveWindowConstrained : moveWindow)(Position[position])
.then(() => onMessage(`Moved the window to ${position}`))
.catch(onMessage)
}
</script>
<div class="flex flex-col gap-2">
<p>
The tray positions only resolve once the tray icon has been clicked, which
reports its location to the plugin.
</p>
<div class="flex flex-row gap-2 items-center">
<select class="input" bind:value={position}>
{#each positions as name}
<option value={name}>{name}</option>
{/each}
</select>
<label>
<input type="checkbox" bind:checked={constrained} />
Constrain to the tray icon's monitor
</label>
<button class="btn" on:click={move}>Move window</button>
</div>
</div>
+68
View File
@@ -0,0 +1,68 @@
<script>
import Database from '@tauri-apps/plugin-sql'
import { onMount } from 'svelte'
export let onMessage
// the `todos` table is created by the migration the app registers for this database
let db
let todos = []
let title = ''
async function refresh() {
todos = await db.select('SELECT * FROM todos ORDER BY id')
}
onMount(async () => {
try {
db = await Database.load('sqlite:api.db')
await refresh()
} catch (error) {
onMessage(error)
}
})
function add() {
db.execute('INSERT INTO todos (title) VALUES ($1)', [title])
.then((result) => {
onMessage(result)
title = ''
return refresh()
})
.catch(onMessage)
}
function toggle(todo) {
db.execute('UPDATE todos SET done = $1 WHERE id = $2', [
todo.done ? 0 : 1,
todo.id
])
.then(refresh)
.catch(onMessage)
}
function remove(todo) {
db.execute('DELETE FROM todos WHERE id = $1', [todo.id])
.then(refresh)
.catch(onMessage)
}
</script>
<div class="flex flex-col gap-2">
<form class="flex flex-row gap-2" on:submit|preventDefault={add}>
<input class="input grow" placeholder="New todo" bind:value={title} />
<button class="btn" type="submit" disabled={!db || !title}>Add</button>
</form>
{#each todos as todo (todo.id)}
<div class="flex flex-row gap-2 items-center">
<input
type="checkbox"
checked={todo.done === 1}
on:change={() => toggle(todo)}
/>
<span class="grow" class:line-through={todo.done === 1}>{todo.title}</span>
<button class="btn" on:click={() => remove(todo)}>Delete</button>
</div>
{/each}
</div>
+71
View File
@@ -0,0 +1,71 @@
<script>
import { Stronghold } from '@tauri-apps/plugin-stronghold'
import { appLocalDataDir, join } from '@tauri-apps/api/path'
export let onMessage
const clientName = 'api-example'
const storeKey = 'secret'
let password = 'password'
let stronghold
let store
let record = ''
async function load() {
try {
const path = await join(await appLocalDataDir(), 'api.stronghold')
stronghold = await Stronghold.load(path, password)
let client
try {
client = await stronghold.loadClient(clientName)
} catch {
client = await stronghold.createClient(clientName)
}
store = client.getStore()
const value = await store.get(storeKey)
record = value ? new TextDecoder().decode(value) : ''
onMessage('Stronghold loaded')
} catch (error) {
stronghold = null
onMessage(error)
}
}
async function save() {
try {
await store.insert(storeKey, Array.from(new TextEncoder().encode(record)))
await stronghold.save()
onMessage('Record saved')
} catch (error) {
onMessage(error)
}
}
async function unload() {
await stronghold.unload().catch(onMessage)
stronghold = null
store = null
record = ''
}
</script>
<div class="flex flex-col gap-2">
{#if stronghold}
<div class="flex flex-row gap-2">
<input class="input grow" placeholder="Secret" bind:value={record} />
<button class="btn" on:click={save}>Save</button>
<button class="btn" on:click={unload}>Lock</button>
</div>
{:else}
<form class="flex flex-row gap-2" on:submit|preventDefault={load}>
<input
class="input grow"
type="password"
placeholder="Password"
bind:value={password}
/>
<button class="btn" type="submit">Unlock</button>
</form>
{/if}
</div>
+55
View File
@@ -0,0 +1,55 @@
<script>
import WebSocket from '@tauri-apps/plugin-websocket'
import { onDestroy } from 'svelte'
export let onMessage
let url = 'wss://echo.websocket.org'
let message = ''
let ws
async function connect() {
try {
ws = await WebSocket.connect(url)
ws.addListener((received) => {
onMessage(received)
if (received.type === 'Close') {
ws = null
}
})
onMessage(`Connected to ${url}`)
} catch (error) {
onMessage(error)
}
}
function send() {
ws.send(message)
.then(() => (message = ''))
.catch(onMessage)
}
function disconnect() {
ws.disconnect().catch(onMessage)
ws = null
}
onDestroy(() => {
ws?.disconnect().catch(() => {})
})
</script>
<div class="flex flex-col gap-2">
{#if ws}
<form class="flex flex-row gap-2" on:submit|preventDefault={send}>
<input class="input grow" placeholder="Message" bind:value={message} />
<button class="btn" type="submit">Send</button>
<button class="btn" type="button" on:click={disconnect}>Disconnect</button>
</form>
{:else}
<form class="flex flex-row gap-2" on:submit|preventDefault={connect}>
<input class="input grow" placeholder="ws:// or wss:// URL" bind:value={url} />
<button class="btn" type="submit">Connect</button>
</form>
{/if}
</div>
+30 -21
View File
@@ -55,7 +55,8 @@ in the core repository; the `@tauri-apps/api` modules themselves are covered the
on the host's loopback; the iOS simulator shares that network stack, and on Android the on the host's loopback; the iOS simulator shares that network stack, and on Android the
mobile config runs `adb reverse` so the same `127.0.0.1` URL works on the device. The mobile config runs `adb reverse` so the same `127.0.0.1` URL works on the device. The
`http` specs use the echo server the example app itself spawns on port 3003, since that `http` specs use the echo server the example app itself spawns on port 3003, since that
is the only `http://` origin in the example's http scope. is the only `http://` origin in the example's http scope. The fixture server also has a
WebSocket echo endpoint (`ws://127.0.0.1:3004/ws`) for the `websocket` specs.
## What is (not) covered ## What is (not) covered
@@ -63,28 +64,36 @@ The example only registers each plugin on the platforms it supports, so the suit
split three ways: desktop-only plugins are skipped on mobile, mobile-only plugins are split three ways: desktop-only plugins are skipped on mobile, mobile-only plugins are
skipped on desktop, and the rest run everywhere with the odd test gated. skipped on desktop, and the rest run everywhere with the odd test gated.
| Plugin | Coverage | On mobile | | Plugin | Coverage | On mobile |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `cli` | `getMatches` shape for an argument-less launch. | Skipped — desktop-only plugin. | | `autostart` | enable/disable/isEnabled, restoring the host's original state. | Skipped — desktop-only plugin. |
| `clipboard-manager` | text, HTML and image round-trips, `clear`, error paths. | Text only; HTML and images are unsupported there. | | `cli` | `getMatches` shape for an argument-less launch. | Skipped — desktop-only plugin. |
| `dialog` | Only that the API is registered — every dialog blocks on native UI the driver cannot operate. | Same. | | `clipboard-manager` | text, HTML and image round-trips, `clear`, error paths. | Text only; HTML and images are unsupported there. |
| `fs` | read/write/stat/copy/rename/remove/truncate, `FileHandle`, line iteration, watchers, scope. | All but the watchers (`fs:allow-watch` is desktop-only here). | | `deep-link` | `getCurrent` on a plain launch, `onOpenUrl` delivery, runtime `register`/`isRegistered`/`unregister` on Linux and Windows. | Only that runtime registration is unsupported. |
| `global-shortcut` | register/unregister/isRegistered/unregisterAll and error paths. Shortcuts cannot be triggered. | Skipped — desktop-only plugin. | | `dialog` | Only that the API is registered — every dialog blocks on native UI the driver cannot operate. | Same. |
| `http` | `fetch` methods, headers, JSON/bytes/multipart bodies, the cookie jar, abort, scope, failures. | Same. | | `fs` | read/write/stat/copy/rename/remove/truncate, `FileHandle`, line iteration, watchers, scope. | All but the watchers (`fs:allow-watch` is desktop-only here). |
| `log` | `attachLogger`/`attachConsole` for webview and Rust records, level filtering. | Same. | | `global-shortcut` | register/unregister/isRegistered/unregisterAll and error paths. Shortcuts cannot be triggered. | Skipped — desktop-only plugin. |
| `notification` | Permission model, `window.Notification` override, `sendNotification`. Display is not observable. | Sending only — see the note below. | | `http` | `fetch` methods, headers, JSON/bytes/multipart bodies, the cookie jar, abort, scope, failures. | Same. |
| `opener` | Scope enforcement only — a successful open launches an external app the suite cannot close. | Same. | | `log` | `attachLogger`/`attachConsole` for webview and Rust records, level filtering. | Same. |
| `os` | Every function, compared against what the app was built for. | Same, minus the hostname/host comparison. | | `notification` | Permission model and the `window.Notification` override. Sending is fire-and-forget and display is not observable. | Action types, pending/active lists after cancel/remove, channels, listeners — see below. |
| `process` | Only that the API is registered — `exit`/`relaunch` terminate the app under test. | Same. | | `opener` | Scope enforcement only — a successful open launches an external app the suite cannot close. | Same. |
| `shell` | `execute`, `spawn` with stdout/stderr/close events, stdin, `kill`, scope enforcement. | Scope only on iOS, which cannot spawn a process at all. | | `os` | Every function but `version` (which has nothing to compare against), checked against what the app was built for and the host. | Same, minus `hostname`. |
| `store` | CRUD, persistence, auto-save, defaults/reset, reload, `getStore`, `LazyStore`, change events. | Same. | | `positioner` | Screen and tray positions (with a tray rect handed in through `handleIconState`), `moveWindowConstrained`, the missing-tray error (WM dependent). | Skipped — desktop-only plugin. |
| `updater` | `check` against the fixture manifest (update / 204 / older release). Installing is never exercised. | Skipped — desktop-only plugin. | | `process` | Only that the API is registered — `exit`/`relaunch` terminate the app under test. | Same. |
| `upload` | `download` and `upload` with progress, methods, headers and error paths. | Same (through `adb reverse` on Android). | | `shell` | `execute`, `spawn` with stdout/stderr/close events, stdin, `kill`, scope enforcement. | Scope only on iOS, which cannot spawn a process at all. |
| `window-state` | `filename`, save/restore, and that a re-created window gets its saved size (WM dependent). | Skipped — desktop-only plugin. | | `sql` | SQLite `load`/`execute`/`select`/`close`, bound values, column types, app-registered migrations, error paths. | Same. |
| mobile-only plugins | `barcode-scanner`, `biometric`, `geolocation`, `haptics` and `nfc` need hardware or native UI the driver cannot operate, so only their global API surface is asserted. | Only there. | | `store` | CRUD, persistence, auto-save, defaults/reset, reload, `getStore`, `LazyStore`, change events. | Same. |
| `stronghold` | Store records (persisted across reloads), wrong-password and unknown-client errors, vault secrets, BIP39/SLIP10 derivation and Ed25519 signing. | Same. |
| `updater` | `check` against the fixture manifest (update / 204 / older release). Installing is never exercised. | Skipped — desktop-only plugin. |
| `upload` | `download` and `upload` with progress, methods, headers and error paths. | Same (through `adb reverse` on Android). |
| `websocket` | Text/binary echo, ping/pong, handshake headers, listener removal, server and client close, connection and argument errors, against the fixture server. | Same (through `adb reverse` on Android). |
| `window-state` | `filename`, save/restore, and that a re-created window gets its saved size (WM dependent). | Skipped — desktop-only plugin. |
| mobile-only plugins | `barcode-scanner`, `biometric`, `geolocation`, `haptics` and `nfc` need hardware or native UI the driver cannot operate, so only their global API surface is asserted. | Only there. |
The notification permission specs are desktop-only: a mobile app starts out ungranted and The notification permission specs are desktop-only: a mobile app starts out ungranted and
`requestPermission` puts up a system dialog the session would then block on. `requestPermission` puts up a system dialog the session would then block on. The rest of the
notification API (action types, channels, the pending/active lists, listeners) only exists on
mobile, and is covered there without the permission.
The [`plugins.spec.ts`](test/specs/plugins.spec.ts) spec additionally asserts that every The [`plugins.spec.ts`](test/specs/plugins.spec.ts) spec additionally asserts that every
plugin the platform registers injects its global API with its documented members, and that plugin the platform registers injects its global API with its documented members, and that
+10 -2
View File
@@ -14,12 +14,14 @@
}, },
"devDependencies": { "devDependencies": {
"@crabnebula/tauri-driver": "^2.0.9", "@crabnebula/tauri-driver": "^2.0.9",
"@crabnebula/test-runner-backend": "^0.2.9", "@crabnebula/test-runner-backend": "^0.3.0",
"@tauri-apps/api": "^2.11.0", "@tauri-apps/api": "^2.11.0",
"@tauri-apps/plugin-autostart": "workspace:*",
"@tauri-apps/plugin-barcode-scanner": "workspace:*", "@tauri-apps/plugin-barcode-scanner": "workspace:*",
"@tauri-apps/plugin-biometric": "workspace:*", "@tauri-apps/plugin-biometric": "workspace:*",
"@tauri-apps/plugin-cli": "workspace:*", "@tauri-apps/plugin-cli": "workspace:*",
"@tauri-apps/plugin-clipboard-manager": "workspace:*", "@tauri-apps/plugin-clipboard-manager": "workspace:*",
"@tauri-apps/plugin-deep-link": "workspace:*",
"@tauri-apps/plugin-dialog": "workspace:*", "@tauri-apps/plugin-dialog": "workspace:*",
"@tauri-apps/plugin-fs": "workspace:*", "@tauri-apps/plugin-fs": "workspace:*",
"@tauri-apps/plugin-geolocation": "workspace:*", "@tauri-apps/plugin-geolocation": "workspace:*",
@@ -31,13 +33,18 @@
"@tauri-apps/plugin-notification": "workspace:*", "@tauri-apps/plugin-notification": "workspace:*",
"@tauri-apps/plugin-opener": "workspace:*", "@tauri-apps/plugin-opener": "workspace:*",
"@tauri-apps/plugin-os": "workspace:*", "@tauri-apps/plugin-os": "workspace:*",
"@tauri-apps/plugin-positioner": "workspace:*",
"@tauri-apps/plugin-process": "workspace:*", "@tauri-apps/plugin-process": "workspace:*",
"@tauri-apps/plugin-shell": "workspace:*", "@tauri-apps/plugin-shell": "workspace:*",
"@tauri-apps/plugin-sql": "workspace:*",
"@tauri-apps/plugin-store": "workspace:*", "@tauri-apps/plugin-store": "workspace:*",
"@tauri-apps/plugin-stronghold": "workspace:*",
"@tauri-apps/plugin-updater": "workspace:*", "@tauri-apps/plugin-updater": "workspace:*",
"@tauri-apps/plugin-upload": "workspace:*", "@tauri-apps/plugin-upload": "workspace:*",
"@tauri-apps/plugin-websocket": "workspace:*",
"@tauri-apps/plugin-window-state": "workspace:*", "@tauri-apps/plugin-window-state": "workspace:*",
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
"@types/ws": "^8.18.1",
"@wdio/appium-service": "^9.31.9", "@wdio/appium-service": "^9.31.9",
"@wdio/cli": "^9.31.9", "@wdio/cli": "^9.31.9",
"@wdio/globals": "^9.31.3", "@wdio/globals": "^9.31.3",
@@ -49,6 +56,7 @@
"appium-xcuitest-driver": "^12.12.4", "appium-xcuitest-driver": "^12.12.4",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"typescript": "6.0.3", "typescript": "6.0.3",
"webdriverio": "^9.31.9" "webdriverio": "^9.31.9",
"ws": "^8.18.0"
} }
} }
+17 -3
View File
@@ -4,10 +4,12 @@
import { browser } from '@wdio/globals' import { browser } from '@wdio/globals'
import type * as TauriApi from '@tauri-apps/api' import type * as TauriApi from '@tauri-apps/api'
import type * as Autostart from '@tauri-apps/plugin-autostart'
import type * as BarcodeScanner from '@tauri-apps/plugin-barcode-scanner' import type * as BarcodeScanner from '@tauri-apps/plugin-barcode-scanner'
import type * as Biometric from '@tauri-apps/plugin-biometric' import type * as Biometric from '@tauri-apps/plugin-biometric'
import type * as Cli from '@tauri-apps/plugin-cli' import type * as Cli from '@tauri-apps/plugin-cli'
import type * as ClipboardManager from '@tauri-apps/plugin-clipboard-manager' import type * as ClipboardManager from '@tauri-apps/plugin-clipboard-manager'
import type * as DeepLink from '@tauri-apps/plugin-deep-link'
import type * as Dialog from '@tauri-apps/plugin-dialog' import type * as Dialog from '@tauri-apps/plugin-dialog'
import type * as Fs from '@tauri-apps/plugin-fs' import type * as Fs from '@tauri-apps/plugin-fs'
import type * as Geolocation from '@tauri-apps/plugin-geolocation' import type * as Geolocation from '@tauri-apps/plugin-geolocation'
@@ -19,20 +21,26 @@ import type * as Nfc from '@tauri-apps/plugin-nfc'
import type * as Notification from '@tauri-apps/plugin-notification' import type * as Notification from '@tauri-apps/plugin-notification'
import type * as Opener from '@tauri-apps/plugin-opener' import type * as Opener from '@tauri-apps/plugin-opener'
import type * as Os from '@tauri-apps/plugin-os' import type * as Os from '@tauri-apps/plugin-os'
import type * as Positioner from '@tauri-apps/plugin-positioner'
import type * as Process from '@tauri-apps/plugin-process' import type * as Process from '@tauri-apps/plugin-process'
import type * as Shell from '@tauri-apps/plugin-shell' import type * as Shell from '@tauri-apps/plugin-shell'
import type * as Sql from '@tauri-apps/plugin-sql'
import type * as Store from '@tauri-apps/plugin-store' import type * as Store from '@tauri-apps/plugin-store'
import type * as Stronghold from '@tauri-apps/plugin-stronghold'
import type * as Updater from '@tauri-apps/plugin-updater' import type * as Updater from '@tauri-apps/plugin-updater'
import type * as Upload from '@tauri-apps/plugin-upload' import type * as Upload from '@tauri-apps/plugin-upload'
import type * as WebSocket from '@tauri-apps/plugin-websocket'
import type * as WindowState from '@tauri-apps/plugin-window-state' import type * as WindowState from '@tauri-apps/plugin-window-state'
/** /**
* The plugin APIs the example registers on every platform, keyed by the name * The plugin APIs the example registers on every platform, keyed by the name
* each plugin's `api-iife.js` defines on `window.__TAURI__` (the package name * each plugin's `api-iife.js` defines on `window.__TAURI__` (the package name
* without the `@tauri-apps/plugin-` prefix, camel-cased). * without the `@tauri-apps/plugin-` prefix, camel-cased). `sql` and `websocket`
* only have a default export, so their global is that class itself.
*/ */
export interface CommonPluginApi { export interface CommonPluginApi {
clipboardManager: typeof ClipboardManager clipboardManager: typeof ClipboardManager
deepLink: typeof DeepLink
dialog: typeof Dialog dialog: typeof Dialog
fs: typeof Fs fs: typeof Fs
http: typeof Http http: typeof Http
@@ -42,14 +50,19 @@ export interface CommonPluginApi {
os: typeof Os os: typeof Os
process: typeof Process process: typeof Process
shell: typeof Shell shell: typeof Shell
sql: typeof Sql.default
store: typeof Store store: typeof Store
stronghold: typeof Stronghold
upload: typeof Upload upload: typeof Upload
websocket: typeof WebSocket.default
} }
/** The plugin APIs the example only registers on desktop (`#[cfg(desktop)]`). */ /** The plugin APIs the example only registers on desktop (`#[cfg(desktop)]`). */
export interface DesktopPluginApi { export interface DesktopPluginApi {
autostart: typeof Autostart
cli: typeof Cli cli: typeof Cli
globalShortcut: typeof GlobalShortcut globalShortcut: typeof GlobalShortcut
positioner: typeof Positioner
updater: typeof Updater updater: typeof Updater
windowState: typeof WindowState windowState: typeof WindowState
} }
@@ -231,8 +244,9 @@ const skippedModules = (process.env.E2E_SKIP ?? '')
export interface DescribePluginOptions { export interface DescribePluginOptions {
/** /**
* The example only registers the plugin on desktop (`cli`, `global-shortcut`, * The example only registers the plugin on desktop (`autostart`, `cli`,
* `updater`, `window-state`), so the whole suite is skipped on mobile. * `global-shortcut`, `positioner`, `updater`, `window-state`), so the whole
* suite is skipped on mobile.
*/ */
desktopOnly?: boolean desktopOnly?: boolean
/** /**
+39
View File
@@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import http from 'node:http' import http from 'node:http'
import { WebSocketServer } from 'ws'
/** /**
* Where the fixture server listens. The port is fixed because the updater * Where the fixture server listens. The port is fixed because the updater
@@ -23,6 +24,13 @@ export const UPDATER_FIXTURE_OLDER_VERSION = '1.0.0'
export const DOWNLOAD_FIXTURE_BODY = export const DOWNLOAD_FIXTURE_BODY =
'hello from the plugins e2e fixture server\n'.repeat(64) 'hello from the plugins e2e fixture server\n'.repeat(64)
/** WebSocket endpoint of the fixture server (the `ws://` twin of {@link FIXTURE_SERVER_URL}). */
export const WEBSOCKET_FIXTURE_URL = `ws://127.0.0.1:${FIXTURE_SERVER_PORT}/ws`
/** Text message that makes the `/ws` endpoint close the connection with {@link WEBSOCKET_CLOSE_CODE}. */
export const WEBSOCKET_CLOSE_REQUEST = 'close-me'
export const WEBSOCKET_CLOSE_CODE = 4000
export const WEBSOCKET_CLOSE_REASON = 'closed by the fixture server'
export interface FixtureServer { export interface FixtureServer {
close(): void close(): void
} }
@@ -38,6 +46,10 @@ export interface FixtureServer {
* - `GET /download` — {@link DOWNLOAD_FIXTURE_BODY} with a `Content-Length`. * - `GET /download` — {@link DOWNLOAD_FIXTURE_BODY} with a `Content-Length`.
* - `* /echo` — a JSON description of the request (`method`, `url`, `headers` * - `* /echo` — a JSON description of the request (`method`, `url`, `headers`
* and the utf-8 `body`). * and the utf-8 `body`).
* - `ws /ws` — a WebSocket echo endpoint: text and binary messages are sent
* back as-is, and {@link WEBSOCKET_CLOSE_REQUEST} makes the server close the
* connection with {@link WEBSOCKET_CLOSE_CODE}. On `/ws/headers` the server
* first sends the upgrade request's headers as a JSON text message.
*/ */
export function startFixtureServer(): Promise<FixtureServer> { export function startFixtureServer(): Promise<FixtureServer> {
const server = http.createServer((req, res) => { const server = http.createServer((req, res) => {
@@ -91,12 +103,39 @@ export function startFixtureServer(): Promise<FixtureServer> {
}) })
}) })
const wss = new WebSocketServer({ noServer: true })
server.on('upgrade', (req, socket, head) => {
const { pathname } = new URL(req.url ?? '/', FIXTURE_SERVER_URL)
if (pathname !== '/ws' && pathname !== '/ws/headers') {
socket.destroy()
return
}
wss.handleUpgrade(req, socket, head, (ws) => {
if (pathname === '/ws/headers') {
ws.send(JSON.stringify(req.headers))
}
ws.on('message', (data, isBinary) => {
if (
!isBinary
&& Buffer.isBuffer(data)
&& data.toString('utf8') === WEBSOCKET_CLOSE_REQUEST
) {
ws.close(WEBSOCKET_CLOSE_CODE, WEBSOCKET_CLOSE_REASON)
} else {
ws.send(data, { binary: isBinary })
}
})
})
})
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
server.once('error', reject) server.once('error', reject)
server.listen(FIXTURE_SERVER_PORT, '127.0.0.1', () => { server.listen(FIXTURE_SERVER_PORT, '127.0.0.1', () => {
server.off('error', reject) server.off('error', reject)
resolve({ resolve({
close: () => { close: () => {
for (const client of wss.clients) client.terminate()
wss.close()
server.closeAllConnections() server.closeAllConnections()
server.close() server.close()
} }
@@ -0,0 +1,49 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import { expect } from '@wdio/globals'
import { tauri, describePlugin } from '../helpers/index.js'
// Enabling autostart registers the app with the host for real (a Launch Agent
// on macOS, an XDG autostart entry on Linux, a `Run` registry value on
// Windows), so the spec puts back whatever state it found.
//
// `disable` is only called while autostart is enabled: on Windows it fails
// when there is no `Run` value to delete.
describePlugin('autostart', { desktopOnly: true }, () => {
let initiallyEnabled = false
before(async () => {
initiallyEnabled = await tauri((api) => api.autostart.isEnabled())
})
after(async () => {
await tauri(async (api, enabled) => {
if ((await api.autostart.isEnabled()) === enabled) return
await (enabled ? api.autostart.enable() : api.autostart.disable())
}, initiallyEnabled)
})
it('enable and disable toggle isEnabled', async () => {
const states = await tauri(async (api) => {
await api.autostart.enable()
const enabled = await api.autostart.isEnabled()
await api.autostart.disable()
return { enabled, disabled: await api.autostart.isEnabled() }
})
expect(states).toEqual({ enabled: true, disabled: false })
})
it('enabling twice keeps it enabled', async () => {
const states = await tauri(async (api) => {
await api.autostart.enable()
await api.autostart.enable()
const enabled = await api.autostart.isEnabled()
await api.autostart.disable()
return { enabled, disabled: await api.autostart.isEnabled() }
})
expect(states).toEqual({ enabled: true, disabled: false })
})
})
@@ -7,7 +7,8 @@ import {
tauri, tauri,
tauriError, tauriError,
describePlugin, describePlugin,
itDesktop itDesktop,
isMobile
} from '../helpers/index.js' } from '../helpers/index.js'
// The mobile implementation only carries plain text: `write_html`, `write_image` // The mobile implementation only carries plain text: `write_html`, `write_image`
@@ -133,7 +134,11 @@ describePlugin('clipboard-manager', () => {
await api.clipboardManager.writeText('not an image') await api.clipboardManager.writeText('not an image')
await api.clipboardManager.readImage() await api.clipboardManager.readImage()
}) })
expect(message.length).toBeGreaterThan(0) expect(message).toMatch(
isMobile
? /Unsupported on this platform/
: /not available in the requested format/
)
}) })
it('clear empties the clipboard', async () => { it('clear empties the clipboard', async () => {
@@ -0,0 +1,99 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import { expect } from '@wdio/globals'
import {
tauri,
tauriError,
describePlugin,
itOn,
platform
} from '../helpers/index.js'
// The suite launches the app without a URL, so there is no current deep link,
// and it cannot open one through the OS either. `onOpenUrl` is exercised by
// emitting the event the plugin itself emits when the app is opened with one.
//
// Registering a scheme at runtime is only implemented on Linux (a `.desktop`
// handler plus `xdg-mime`) and Windows (the registry); macOS and mobile read
// the schemes from the bundle and report the runtime APIs as unsupported.
/** A scheme no real app handles, so registering it cannot clobber anything. */
const scheme = 'tauri-plugins-e2e'
describePlugin('deep-link', () => {
it('getCurrent is null when the app was not opened with a URL', async () => {
expect(await tauri((api) => api.deepLink.getCurrent())).toBeNull()
})
it('onOpenUrl delivers the opened URLs until unlistened', async () => {
const received = await tauri(
async (api, urls) => {
const received: string[][] = []
const unlisten = await api.deepLink.onOpenUrl((urls) =>
received.push(urls)
)
await api.event.emit('deep-link://new-url', urls)
await new Promise((resolve) => setTimeout(resolve, 500))
unlisten()
await api.event.emit('deep-link://new-url', ['ignored://'])
await new Promise((resolve) => setTimeout(resolve, 500))
return received
},
[`${scheme}://open/path?query=1`]
)
expect(received).toEqual([[`${scheme}://open/path?query=1`]])
})
itOn(
['linux', 'win32'],
'register makes the app the scheme handler',
async () => {
const result = await tauri(async (api, scheme) => {
await api.deepLink.register(scheme)
const registered = await api.deepLink.isRegistered(scheme)
await api.deepLink.unregister(scheme)
return {
registered,
afterUnregister: await api.deepLink.isRegistered(scheme)
}
}, scheme)
expect(result.registered).toBe(true)
// On Linux `xdg-mime` falls back to the desktop database, which still lists
// the handler after its `mimeapps.list` default is removed.
if (platform === 'win32') {
expect(result.afterUnregister).toBe(false)
}
}
)
itOn('win32', 'isRegistered is false for an unknown scheme', async () => {
expect(
await tauri(
(api, scheme) => api.deepLink.isRegistered(scheme),
`${scheme}-unknown`
)
).toBe(false)
})
itOn(
['darwin', 'android', 'ios'],
'runtime registration is unsupported',
async () => {
for (const command of [
'register',
'unregister',
'isRegistered'
] as const) {
const error = await tauriError(
// eslint-disable-next-line security/detect-object-injection
(api, command, scheme) => api.deepLink[command](scheme),
command,
scheme
)
expect(error).toMatch(/unsupported platform/i)
}
}
)
})
+4 -3
View File
@@ -276,7 +276,7 @@ describePlugin('fs', () => {
await file.close() await file.close()
await file.stat() await file.stat()
}, `${dir}/closed.txt`) }, `${dir}/closed.txt`)
expect(message.length).toBeGreaterThan(0) expect(message).toMatch(/resource id \d+ is invalid/)
}) })
it('rejects paths outside the configured scope', async () => { it('rejects paths outside the configured scope', async () => {
@@ -339,8 +339,9 @@ describePlugin('fs', () => {
unwatch() unwatch()
return events return events
}, `${dir}/watched`) }, `${dir}/watched`)
expect(result.length).toBeGreaterThan(0) expect(
expect(result.every((e) => typeof e.kind === 'string')).toBe(true) result.some((e) => e.paths.some((p) => p.endsWith('touched.txt')))
).toBe(true)
} }
) )
@@ -3,7 +3,12 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { expect } from '@wdio/globals' import { expect } from '@wdio/globals'
import { tauri, tauriError, describePlugin } from '../helpers/index.js' import {
tauri,
tauriError,
describePlugin,
platform
} from '../helpers/index.js'
// Triggering a shortcut needs OS-level synthetic input the WebDriver session // Triggering a shortcut needs OS-level synthetic input the WebDriver session
// cannot produce, so the specs cover the registry (register / isRegistered / // cannot produce, so the specs cover the registry (register / isRegistered /
@@ -74,9 +79,19 @@ describePlugin('global-shortcut', { desktopOnly: true }, () => {
it('registering the same shortcut twice rejects', async () => { it('registering the same shortcut twice rejects', async () => {
const message = await tauriError(async (api) => { const message = await tauriError(async (api) => {
await api.globalShortcut.register('Alt+Shift+F11', () => {}) await api.globalShortcut.register('Alt+Shift+F11', () => {})
await api.globalShortcut.register('Alt+Shift+F11', () => {}) try {
await api.globalShortcut.register('Alt+Shift+F11', () => {})
} finally {
await api.globalShortcut.unregister('Alt+Shift+F11')
}
}) })
expect(message).toMatch(/already registered/i) // macOS does not check for duplicates itself: the OS rejects the second
// `RegisterEventHotKey` call and the error is the generic one
expect(message).toMatch(
platform === 'darwin'
? /RegisterEventHotKey failed for F11/
: /already registered/i
)
}) })
it('rejects shortcuts that cannot be parsed', async () => { it('rejects shortcuts that cannot be parsed', async () => {
+1 -1
View File
@@ -144,6 +144,6 @@ describePlugin('http', () => {
api.http.fetch(url, { proxy: { all: 'http://127.0.0.1:1' } }), api.http.fetch(url, { proxy: { all: 'http://127.0.0.1:1' } }),
echoServer echoServer
) )
expect(message.length).toBeGreaterThan(0) expect(message).toMatch(/error sending request/)
}) })
}) })
+105 -22
View File
@@ -3,15 +3,25 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { expect } from '@wdio/globals' import { expect } from '@wdio/globals'
import { tauri, describePlugin, itDesktop } from '../helpers/index.js' import {
tauri,
tauriError,
describePlugin,
itDesktop,
itOn
} from '../helpers/index.js'
// Whether a notification actually shows up depends on the desktop session // Whether a notification actually shows up depends on the desktop session
// (a notification daemon on Linux, the app's registration on Windows and // (a notification daemon on Linux, the app's registration on Windows and
// macOS), which the suite cannot observe. The specs cover the permission // macOS), which the suite cannot observe, and sending is fire-and-forget, so
// model and that sending does not error out synchronously. // only the permission model is covered on desktop.
// //
// The permission specs are desktop-only: mobile starts out ungranted and // The permission specs are desktop-only: mobile starts out ungranted and
// `requestPermission` puts up a system dialog the session would then block on. // `requestPermission` puts up a system dialog the session would then block on.
//
// Scheduling, action types, channels, the pending/active lists and the
// listeners are only implemented on mobile. None of them need the permission,
// so they are covered there on an app that has not been granted it.
describePlugin('notification', () => { describePlugin('notification', () => {
itDesktop('permission is granted on desktop', async () => { itDesktop('permission is granted on desktop', async () => {
@@ -30,29 +40,102 @@ describePlugin('notification', () => {
expect(result).toEqual({ permission: 'granted', requested: 'granted' }) expect(result).toEqual({ permission: 'granted', requested: 'granted' })
}) })
it('sendNotification accepts a title string and an options object', async () => { itOn(
const result = await tauri((api) => { ['android', 'ios'],
api.notification.sendNotification('notification from e2e') 'action types register, and cancel/remove leave nothing pending or active',
api.notification.sendNotification({ async () => {
title: 'notification from e2e', const result = await tauri(async (api) => {
body: 'with a body', await api.notification.registerActionTypes([
sound: 'default' {
id: 'e2e-actions',
actions: [
{ id: 'reply', title: 'Reply', input: true },
{ id: 'dismiss', title: 'Dismiss', destructive: true }
]
}
])
await api.notification.cancel([424242])
await api.notification.cancelAll()
await api.notification.removeActive([{ id: 424242 }])
await api.notification.removeAllActive()
return {
pending: await api.notification.pending(),
active: await api.notification.active()
}
}) })
return true expect(result).toEqual({ pending: [], active: [] })
}
)
itOn('android', 'channels can be created, listed and removed', async () => {
const result = await tauri(async (api) => {
const { Importance, Visibility } = api.notification
await api.notification.createChannel({
id: 'e2e-channel',
name: 'e2e channel',
description: 'created by the e2e suite',
importance: Importance.High,
visibility: Visibility.Public,
vibration: true
})
const created = (await api.notification.channels()).find(
(channel) => channel.id === 'e2e-channel'
)
await api.notification.removeChannel('e2e-channel')
const removed = (await api.notification.channels()).some(
(channel) => channel.id === 'e2e-channel'
)
return {
created: created && {
name: created.name,
description: created.description,
importance: created.importance,
vibration: created.vibration
},
removed
}
})
expect(result).toEqual({
created: {
name: 'e2e channel',
description: 'created by the e2e suite',
importance: 4, // Importance.High
vibration: true
},
removed: false
}) })
expect(result).toBe(true)
}) })
it('new Notification() goes through the plugin', async () => { itOn('ios', 'channels are not implemented', async () => {
const result = await tauri(() => { for (const call of ['create', 'remove', 'list'] as const) {
const notification = new window.Notification( const error = await tauriError(
'window.Notification from e2e', (api, call) =>
{ call === 'create'
body: 'created through the DOM API' ? api.notification.createChannel({ id: 'e2e', name: 'e2e' })
} : call === 'remove'
? api.notification.removeChannel('e2e')
: api.notification.channels(),
call
) )
return typeof notification === 'object' expect(error).toMatch(/not implemented/)
}) }
expect(result).toBe(true)
}) })
itOn(
['android', 'ios'],
'onNotificationReceived and onAction listeners register and unregister',
async () => {
const result = await tauri(async (api) => {
const received = await api.notification.onNotificationReceived(() => {})
const action = await api.notification.onAction(() => {})
await received.unregister()
await action.unregister()
return { received: received.event, action: action.event }
})
expect(result).toEqual({
received: 'notification',
action: 'actionPerformed'
})
}
)
}) })
+1 -1
View File
@@ -41,6 +41,6 @@ describePlugin('opener', () => {
await api.path.join(await api.path.appDataDir(), 'does-not-exist-e2e') await api.path.join(await api.path.appDataDir(), 'does-not-exist-e2e')
) )
) )
expect(message.length).toBeGreaterThan(0) expect(message).toMatch(/os error 2/)
}) })
}) })
-11
View File
@@ -86,11 +86,6 @@ describePlugin('os', () => {
expect(info.exeExtension).toBe(platform === 'win32' ? 'exe' : '') expect(info.exeExtension).toBe(platform === 'win32' ? 'exe' : '')
}) })
it('version reports a non-empty OS version', async () => {
const version = await tauri((api) => api.os.version())
expect(version.length).toBeGreaterThan(0)
})
it('locale is null or a language tag', async () => { it('locale is null or a language tag', async () => {
const locale = await tauri((api) => api.os.locale()) const locale = await tauri((api) => api.os.locale())
// The plugin forwards the environment's POSIX locale as-is, so a host with // The plugin forwards the environment's POSIX locale as-is, so a host with
@@ -106,12 +101,6 @@ describePlugin('os', () => {
} }
}) })
it('hostname reports a name', async () => {
const hostname = await tauri((api) => api.os.hostname())
expect(hostname).not.toBeNull()
expect(hostname!.length).toBeGreaterThan(0)
})
itDesktop('hostname matches the host', async () => { itDesktop('hostname matches the host', async () => {
const hostname = await tauri((api) => api.os.hostname()) const hostname = await tauri((api) => api.os.hostname())
// Windows can report the name in a different case than Node does. // Windows can report the name in a different case than Node does.
+26 -7
View File
@@ -31,6 +31,13 @@ const commonSurface: Surface<CommonPluginApi> = {
'readImage', 'readImage',
'writeImage' 'writeImage'
], ],
deepLink: [
'getCurrent',
'register',
'unregister',
'isRegistered',
'onOpenUrl'
],
dialog: ['open', 'save', 'message', 'ask', 'confirm'], dialog: ['open', 'save', 'message', 'ask', 'confirm'],
fs: [ fs: [
'BaseDirectory', 'BaseDirectory',
@@ -98,14 +105,26 @@ const commonSurface: Surface<CommonPluginApi> = {
], ],
process: ['exit', 'relaunch'], process: ['exit', 'relaunch'],
shell: ['Command', 'Child', 'EventEmitter', 'open'], shell: ['Command', 'Child', 'EventEmitter', 'open'],
// the `Database` class itself
sql: ['load', 'get'],
store: ['load', 'getStore', 'LazyStore', 'Store'], store: ['load', 'getStore', 'LazyStore', 'Store'],
upload: ['download', 'upload', 'HttpMethod'] stronghold: ['Location', 'Client', 'Store', 'Vault', 'Stronghold'],
upload: ['download', 'upload', 'HttpMethod'],
// the `WebSocket` class itself
websocket: ['connect']
} }
/** Plugins the example only registers on desktop. */ /** Plugins the example only registers on desktop. */
const desktopSurface: Surface<DesktopPluginApi> = { const desktopSurface: Surface<DesktopPluginApi> = {
autostart: ['enable', 'disable', 'isEnabled'],
cli: ['getMatches'], cli: ['getMatches'],
globalShortcut: ['register', 'unregister', 'unregisterAll', 'isRegistered'], globalShortcut: ['register', 'unregister', 'unregisterAll', 'isRegistered'],
positioner: [
'Position',
'moveWindow',
'moveWindowConstrained',
'handleIconState'
],
updater: ['check', 'Update'], updater: ['check', 'Update'],
windowState: [ windowState: [
'StateFlags', 'StateFlags',
@@ -168,12 +187,12 @@ describe('plugin globals', () => {
it('every plugin this platform registers exposes its API on window.__TAURI__', async () => { it('every plugin this platform registers exposes its API on window.__TAURI__', async () => {
const missing = await tauri( const missing = await tauri(
(api, plugins) => (api, plugins) =>
plugins.filter( plugins.filter((plugin) => {
(plugin) => // eslint-disable-next-line security/detect-object-injection
// eslint-disable-next-line security/detect-object-injection const global = (api as unknown as Record<string, unknown>)[plugin]
typeof (api as unknown as Record<string, unknown>)[plugin] // a module namespace, or the class a default-export-only module (sql, websocket) defines
!== 'object' return typeof global !== 'object' && typeof global !== 'function'
), }),
Object.keys(surface) Object.keys(surface)
) )
expect(missing).toEqual([]) expect(missing).toEqual([])
@@ -0,0 +1,137 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import { expect } from '@wdio/globals'
import { tauri, tauriError, describePlugin, itWm } from '../helpers/index.js'
// Positions are computed from the window's current monitor and outer size, so
// the specs compute the expected physical position the same way and compare
// once the window manager has applied the move. The tray positions need the
// tray icon's rect, which normally comes from a click on the icon; the spec
// hands one in through `handleIconState`, as a JS tray event handler would.
// The moved window is not put back: each spec file gets a fresh app.
interface Geometry {
monitor: { x: number; y: number; width: number; height: number }
window: { width: number; height: number }
}
/** Moves the main window and reports where it ended up along with the geometry the position is derived from. */
function moveAndMeasure(position: string, constrained = false) {
return tauri(
async (api, position, constrained) => {
const win = api.window.getCurrentWindow()
const { Position } = api.positioner
const value = Position[position as keyof typeof Position]
await (constrained
? api.positioner.moveWindowConstrained(value)
: api.positioner.moveWindow(value))
await new Promise((resolve) => setTimeout(resolve, 500))
const monitor = await api.window.currentMonitor()
const size = await win.outerSize()
const outer = await win.outerPosition()
return {
monitor: {
x: monitor!.position.x,
y: monitor!.position.y,
width: monitor!.size.width,
height: monitor!.size.height
},
window: { width: size.width, height: size.height },
position: { x: outer.x, y: outer.y }
}
},
position,
constrained
)
}
function center({ monitor, window }: Geometry) {
return {
x: monitor.x + Math.trunc(monitor.width / 2) - Math.trunc(window.width / 2),
y:
monitor.y + Math.trunc(monitor.height / 2) - Math.trunc(window.height / 2)
}
}
describePlugin('positioner', { desktopOnly: true }, () => {
// must run before any tray rect is handed in
it('tray positions are rejected until the tray icon reported its rect', async () => {
const error = await tauriError((api) =>
api.positioner.moveWindow(api.positioner.Position.TrayCenter)
)
expect(error).toMatch(/Tray position not set/)
})
it('the Position enum matches the plugin', async () => {
const names = await tauri((api) =>
Object.keys(api.positioner.Position).filter((key) => isNaN(Number(key)))
)
expect(names).toEqual([
'TopLeft',
'TopRight',
'BottomLeft',
'BottomRight',
'TopCenter',
'BottomCenter',
'LeftCenter',
'RightCenter',
'Center',
'TrayLeft',
'TrayBottomLeft',
'TrayRight',
'TrayBottomRight',
'TrayCenter',
'TrayBottomCenter'
])
})
itWm('moveWindow centers the window on its monitor', async () => {
const result = await moveAndMeasure('Center')
expect(result.position).toEqual(center(result))
})
itWm(
'moveWindowConstrained behaves like moveWindow for screen positions',
async () => {
const result = await moveAndMeasure('Center', true)
expect(result.position).toEqual(center(result))
}
)
itWm('tray positions are relative to the reported tray rect', async () => {
// a tray "icon" in the middle of the window's monitor, far from any edge,
// so neither the OS-specific flips nor the constraint kick in
const tray = await tauri(async (api) => {
const monitor = await api.window.currentMonitor()
const rect = {
x: monitor!.position.x + Math.trunc(monitor!.size.width / 2),
y: monitor!.position.y + Math.trunc(monitor!.size.height / 2),
width: 20,
height: 20
}
await api.positioner.handleIconState({
type: 'Click',
id: 'e2e',
position: new api.dpi.PhysicalPosition(rect.x, rect.y),
rect: {
position: new api.dpi.PhysicalPosition(rect.x, rect.y),
size: new api.dpi.PhysicalSize(rect.width, rect.height)
},
button: 'Left',
buttonState: 'Down'
})
return rect
})
const bottomLeft = await moveAndMeasure('TrayBottomLeft')
expect(bottomLeft.position).toEqual({ x: tray.x, y: tray.y })
const bottomCenter = await moveAndMeasure('TrayBottomCenter', true)
expect(bottomCenter.position).toEqual({
x: tray.x + tray.width / 2 - Math.trunc(bottomCenter.window.width / 2),
y: tray.y
})
})
})
+1 -1
View File
@@ -233,6 +233,6 @@ describePlugin('shell', () => {
const message = await tauriError((api) => const message = await tauriError((api) =>
api.shell.open('ftp://example.com') api.shell.open('ftp://example.com')
) )
expect(message.length).toBeGreaterThan(0) expect(message).toMatch(/failed regex validation/)
}) })
}) })
+168
View File
@@ -0,0 +1,168 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import { expect } from '@wdio/globals'
import { tauri, tauriError, describePlugin } from '../helpers/index.js'
// SQLite databases live in the app config directory. The spec works on its
// own database, recreating its table before each test, and only reads the
// example's `sqlite:api.db` to check the migration the app registers for it.
const db = 'sqlite:e2e-sql.db'
interface Row {
id: number
name: string
score: number | null
ratio: number | null
}
describePlugin('sql', () => {
beforeEach(async () => {
await tauri(async (api, db) => {
const database = await api.sql.load(db)
await database.execute('DROP TABLE IF EXISTS e2e')
await database.execute(
'CREATE TABLE e2e (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, score INTEGER, ratio REAL)'
)
}, db)
})
it('load resolves with a database for the given path', async () => {
const path = await tauri(
async (api, db) => (await api.sql.load(db)).path,
db
)
expect(path).toBe(db)
})
it('migrations registered by the app run when their database is loaded', async () => {
const result = await tauri(async (api) => {
const database = await api.sql.load('sqlite:api.db')
return {
tables: await database.select<{ name: string }[]>(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'todos'"
),
migrations: await database.select<
{ version: number; description: string; success: number }[]
>('SELECT version, description, success FROM _sqlx_migrations')
}
})
expect(result.tables).toEqual([{ name: 'todos' }])
// `success` is declared BOOLEAN but stored, and read back, as an integer
expect(result.migrations).toEqual([
{ version: 1, description: 'create_todos_table', success: 1 }
])
})
it('execute reports affected rows and the last insert id', async () => {
const results = await tauri(async (api, db) => {
const database = api.sql.get(db)
const first = await database.execute(
'INSERT INTO e2e (name, score) VALUES ($1, $2)',
['first', 1]
)
const second = await database.execute(
'INSERT INTO e2e (name, score) VALUES ($1, $2)',
['second', 2]
)
const update = await database.execute('UPDATE e2e SET score = score + 10')
return { first, second, update }
}, db)
expect(results.first).toEqual({ rowsAffected: 1, lastInsertId: 1 })
expect(results.second).toEqual({ rowsAffected: 1, lastInsertId: 2 })
expect(results.update.rowsAffected).toBe(2)
})
it('select returns rows as objects, with bound values and column types', async () => {
const rows = await tauri(async (api, db) => {
const database = api.sql.get(db)
await database.execute(
'INSERT INTO e2e (name, score, ratio) VALUES ($1, $2, $3), ($4, $5, $6)',
['alpha', 42, 0.5, 'beta', null, null]
)
return {
all: await database.select<Row[]>('SELECT * FROM e2e ORDER BY id'),
filtered: await database.select<Row[]>(
'SELECT name FROM e2e WHERE score = $1',
[42]
),
none: await database.select<Row[]>(
'SELECT * FROM e2e WHERE name = $1',
['missing']
)
}
}, db)
expect(rows.all).toEqual([
{ id: 1, name: 'alpha', score: 42, ratio: 0.5 },
{ id: 2, name: 'beta', score: null, ratio: null }
])
expect(rows.filtered).toEqual([{ name: 'alpha' }])
expect(rows.none).toEqual([])
})
it('column order follows the query', async () => {
const columns = await tauri(async (api, db) => {
const database = api.sql.get(db)
await database.execute("INSERT INTO e2e (name, score) VALUES ('x', 1)")
const [row] = await database.select<Record<string, unknown>[]>(
'SELECT score, name, id FROM e2e'
)
return Object.keys(row)
}, db)
expect(columns).toEqual(['score', 'name', 'id'])
})
it('invalid statements reject', async () => {
const error = await tauriError(
(api, db) => api.sql.get(db).execute('INSERT INTO missing VALUES (1)'),
db
)
expect(error).toMatch(/no such table: missing/)
const constraint = await tauriError(
(api, db) =>
api.sql.get(db).execute('INSERT INTO e2e (name) VALUES ($1)', [null]),
db
)
expect(constraint).toMatch(/NOT NULL constraint failed/)
})
it('a database that was never loaded is rejected', async () => {
const error = await tauriError((api) =>
api.sql.get('sqlite:never-loaded.db').select('SELECT 1')
)
expect(error).toMatch(/database sqlite:never-loaded\.db not loaded/)
})
it('close shuts the pool down until the database is loaded again', async () => {
const result = await tauri(async (api, db) => {
const database = api.sql.get(db)
await database.execute("INSERT INTO e2e (name) VALUES ('persisted')")
const closed = await database.close(db)
let error = ''
try {
await database.select('SELECT * FROM e2e')
} catch (e) {
error = String(e)
}
const reloaded = await api.sql.load(db)
return {
closed,
error,
rows: await reloaded.select<{ name: string }[]>('SELECT name FROM e2e')
}
}, db)
expect(result.closed).toBe(true)
expect(result.error).toMatch(/closed/i)
// the data was written to disk, not just to the closed pool
expect(result.rows).toEqual([{ name: 'persisted' }])
})
it('closing a database that was never loaded is rejected', async () => {
const error = await tauriError((api) =>
api.sql.get('sqlite:never-loaded.db').close('sqlite:never-loaded.db')
)
expect(error).toMatch(/not loaded/)
})
})
@@ -0,0 +1,248 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import { expect } from '@wdio/globals'
import {
tauri,
tauriError,
describePlugin,
scratchDir
} from '../helpers/index.js'
// The example derives the snapshot key from the password with argon2. Each
// test works on its own snapshot file under the spec's scratch directory,
// which the fs plugin cleans up; the snapshot path is resolved inside the page.
const dir = scratchDir('stronghold')
const password = 'e2e-password'
const clientName = 'e2e-client'
/** An arbitrary, valid BIP39 mnemonic, so derived keys are deterministic. */
const mnemonic =
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
describePlugin('stronghold', () => {
before(async () => {
await tauri(async (api, dir) => {
const baseDir = api.fs.BaseDirectory.AppData
if (await api.fs.exists(dir, { baseDir })) {
await api.fs.remove(dir, { baseDir, recursive: true })
}
await api.fs.mkdir(dir, { baseDir, recursive: true })
}, dir)
})
after(async () => {
await tauri(async (api, dir) => {
const baseDir = api.fs.BaseDirectory.AppData
if (await api.fs.exists(dir, { baseDir })) {
await api.fs.remove(dir, { baseDir, recursive: true })
}
}, dir)
})
it('store records round-trip and persist in the snapshot', async () => {
const result = await tauri(
async (api, dir, password, clientName) => {
const path = await api.path.join(
await api.path.appDataDir(),
dir,
'store.stronghold'
)
const value = Array.from(new TextEncoder().encode('top secret'))
const stronghold = await api.stronghold.Stronghold.load(path, password)
const client = await stronghold.createClient(clientName)
await client.getStore().insert('key', value)
const sameSession = await client.getStore().get('key')
await stronghold.save()
await stronghold.unload()
const reopened = await api.stronghold.Stronghold.load(path, password)
const store = (await reopened.loadClient(clientName)).getStore()
const persisted = await store.get('key')
const missing = await store.get('missing')
await reopened.unload()
return {
sameSession: sameSession && Array.from(sameSession),
persisted: persisted && new TextDecoder().decode(persisted),
missing,
fileExists: await api.fs.exists(path)
}
},
dir,
password,
clientName
)
expect(result.sameSession).toEqual(
Array.from(new TextEncoder().encode('top secret'))
)
expect(result.persisted).toBe('top secret')
expect(result.missing).toBeNull()
expect(result.fileExists).toBe(true)
})
it('store.remove returns the removed value', async () => {
const result = await tauri(
async (api, dir, password, clientName) => {
const path = await api.path.join(
await api.path.appDataDir(),
dir,
'remove.stronghold'
)
const stronghold = await api.stronghold.Stronghold.load(path, password)
const store = (await stronghold.createClient(clientName)).getStore()
await store.insert('key', [1, 2, 3])
const removed = await store.remove('key')
const after = await store.get('key')
await stronghold.unload()
return { removed: removed && Array.from(removed), after }
},
dir,
password,
clientName
)
expect(result).toEqual({ removed: [1, 2, 3], after: null })
})
it('a snapshot cannot be opened with the wrong password', async () => {
const path = await tauri(
async (api, dir, password, clientName) => {
const path = await api.path.join(
await api.path.appDataDir(),
dir,
'password.stronghold'
)
const stronghold = await api.stronghold.Stronghold.load(path, password)
await stronghold.createClient(clientName)
await stronghold.unload()
return path
},
dir,
password,
clientName
)
const error = await tauriError(
(api, path) => api.stronghold.Stronghold.load(path, 'wrong-password'),
path
)
expect(error).toMatch(/failed to decode\/decrypt/)
})
it('loading a client that was never created is rejected', async () => {
const error = await tauriError(
async (api, dir, password) => {
const path = await api.path.join(
await api.path.appDataDir(),
dir,
'no-client.stronghold'
)
const stronghold = await api.stronghold.Stronghold.load(path, password)
try {
await stronghold.loadClient('never-created')
} finally {
await stronghold.unload()
}
},
dir,
password
)
expect(error).toMatch(/error loading client data/)
})
it('vault procedures derive keys and sign without exposing secrets', async () => {
const result = await tauri(
async (api, dir, password, clientName, mnemonic) => {
const { Location } = api.stronghold
const path = await api.path.join(
await api.path.appDataDir(),
dir,
'vault.stronghold'
)
const stronghold = await api.stronghold.Stronghold.load(path, password)
const vault = (await stronghold.createClient(clientName)).getVault(
'vault'
)
// the same mnemonic recovered twice derives the same key
const seedA = Location.generic('vault', 'seed-a')
const seedB = Location.generic('vault', 'seed-b')
await vault.recoverBIP39(mnemonic, seedA)
await vault.recoverBIP39(mnemonic, seedB)
// Ed25519 SLIP-10 only derives hardened indices (the high bit set).
// (No named helper: the transpiler would wrap it in a `__name` call
// that does not exist in the page.)
const chain = [44, 4218, 0, 0, 0].map((i) => (i | 0x80000000) >>> 0)
const otherChain = [44, 4218, 0, 0, 1].map(
(i) => (i | 0x80000000) >>> 0
)
const keyA = Location.generic('vault', 'key-a')
const keyB = Location.generic('vault', 'key-b')
const keyOther = Location.generic('vault', 'key-other')
await vault.deriveSLIP10(chain, 'Seed', seedA, keyA)
await vault.deriveSLIP10(chain, 'Seed', seedB, keyB)
await vault.deriveSLIP10(otherChain, 'Seed', seedA, keyOther)
const publicA = Array.from(await vault.getEd25519PublicKey(keyA))
const publicB = Array.from(await vault.getEd25519PublicKey(keyB))
const publicOther = Array.from(
await vault.getEd25519PublicKey(keyOther)
)
const signature = Array.from(await vault.signEd25519(keyA, 'message'))
const signatureAgain = Array.from(
await vault.signEd25519(keyA, 'message')
)
// a random seed and a raw secret can be stored and removed
const random = Location.generic('vault', 'random')
await vault.generateSLIP10Seed(random)
await vault.insert('raw', [9, 9, 9])
await vault.remove(Location.generic('vault', 'raw'))
// a generated mnemonic recovers the seed generateBIP39 stored
const generatedSeed = Location.generic('vault', 'generated')
const mnemonicBytes = await vault.generateBIP39(generatedSeed)
const recoveredSeed = Location.generic('vault', 'recovered')
await vault.recoverBIP39(
new TextDecoder().decode(mnemonicBytes),
recoveredSeed
)
const generatedKey = Location.generic('vault', 'generated-key')
const recoveredKey = Location.generic('vault', 'recovered-key')
await vault.deriveSLIP10(chain, 'Seed', generatedSeed, generatedKey)
await vault.deriveSLIP10(chain, 'Seed', recoveredSeed, recoveredKey)
const generatedPublic = Array.from(
await vault.getEd25519PublicKey(generatedKey)
)
const recoveredPublic = Array.from(
await vault.getEd25519PublicKey(recoveredKey)
)
await stronghold.unload()
return {
publicA,
publicB,
publicOther,
signature,
signatureAgain,
generatedPublic,
recoveredPublic
}
},
dir,
password,
clientName,
mnemonic
)
expect(result.publicA).toHaveLength(32)
expect(result.publicB).toEqual(result.publicA)
expect(result.publicOther).not.toEqual(result.publicA)
// Ed25519 signatures are 64 bytes and deterministic
expect(result.signature).toHaveLength(64)
expect(result.signatureAgain).toEqual(result.signature)
expect(result.recoveredPublic).toEqual(result.generatedPublic)
expect(result.generatedPublic).not.toEqual(result.publicA)
})
})
+1 -1
View File
@@ -156,6 +156,6 @@ describePlugin('upload', () => {
`${FIXTURE_SERVER_URL}/echo`, `${FIXTURE_SERVER_URL}/echo`,
`${dir}/does-not-exist.txt` `${dir}/does-not-exist.txt`
) )
expect(message.length).toBeGreaterThan(0) expect(message).toMatch(/os error 2/)
}) })
}) })
@@ -0,0 +1,185 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import { expect } from '@wdio/globals'
import { tauri, tauriError, describePlugin } from '../helpers/index.js'
import {
WEBSOCKET_FIXTURE_URL,
WEBSOCKET_CLOSE_REQUEST,
WEBSOCKET_CLOSE_CODE,
WEBSOCKET_CLOSE_REASON
} from '../helpers/server.js'
// Every test talks to the fixture server's `/ws` echo endpoint (through
// `adb reverse` on Android). Messages reach the page through the plugin's
// channel, so each page function collects them until it has what it expects,
// with its own timeout so a missing message fails with a readable error.
interface Message {
type: string
data: unknown
}
describePlugin('websocket', () => {
it('echoes text and binary messages', async () => {
const messages = await tauri(async (api, url) => {
const ws = await api.websocket.connect(url)
const received: Message[] = []
await new Promise<void>((resolve, reject) => {
setTimeout(() => reject(new Error('echo not received')), 5000)
ws.addListener((message) => {
received.push(message)
if (received.length === 3) resolve()
})
ws.send('hello')
.then(() => ws.send([1, 2, 3]))
.then(() => ws.send({ type: 'Text', data: 'explicit' }))
.catch(reject)
})
await ws.disconnect()
return received
}, WEBSOCKET_FIXTURE_URL)
expect(messages).toEqual([
{ type: 'Text', data: 'hello' },
{ type: 'Binary', data: [1, 2, 3] },
{ type: 'Text', data: 'explicit' }
])
})
it('a ping is answered with a pong carrying its payload', async () => {
const message = await tauri(async (api, url) => {
const ws = await api.websocket.connect(url)
const pong = await new Promise<Message>((resolve, reject) => {
setTimeout(() => reject(new Error('pong not received')), 5000)
ws.addListener((message) => {
if (message.type === 'Pong') resolve(message)
})
ws.send({ type: 'Ping', data: [7, 8, 9] }).catch(reject)
})
await ws.disconnect()
return pong
}, WEBSOCKET_FIXTURE_URL)
expect(message).toEqual({ type: 'Pong', data: [7, 8, 9] })
})
it('sends the configured headers with the upgrade request', async () => {
const headers = await tauri(async (api, url) => {
const ws = await api.websocket.connect(url, {
headers: { 'x-e2e-header': 'from the plugin' }
})
const message = await new Promise<Message>((resolve, reject) => {
setTimeout(() => reject(new Error('headers not received')), 5000)
ws.addListener(resolve)
})
await ws.disconnect()
return JSON.parse(message.data as string) as Record<string, string>
}, `${WEBSOCKET_FIXTURE_URL}/headers`)
expect(headers['x-e2e-header']).toBe('from the plugin')
})
it('a listener stops receiving once removed', async () => {
const counts = await tauri(async (api, url) => {
const ws = await api.websocket.connect(url)
let removed = 0
let kept = 0
const remove = ws.addListener(() => removed++)
await new Promise<void>((resolve, reject) => {
setTimeout(() => reject(new Error('echoes not received')), 5000)
ws.addListener(() => {
kept++
if (kept === 1) {
remove()
ws.send('second').catch(reject)
} else {
resolve()
}
})
ws.send('first').catch(reject)
})
await ws.disconnect()
return { removed, kept }
}, WEBSOCKET_FIXTURE_URL)
expect(counts).toEqual({ removed: 1, kept: 2 })
})
it('a close from the server is delivered and ends the connection', async () => {
const result = await tauri(
async (api, url, closeRequest) => {
const ws = await api.websocket.connect(url)
const close = await new Promise<Message>((resolve, reject) => {
setTimeout(() => reject(new Error('close not received')), 5000)
ws.addListener((message) => {
if (message.type === 'Close') resolve(message)
})
ws.send(closeRequest).catch(reject)
})
let sendError = ''
try {
await ws.send('after close')
} catch (error) {
sendError = String(error)
}
return { close, sendError }
},
WEBSOCKET_FIXTURE_URL,
WEBSOCKET_CLOSE_REQUEST
)
expect(result.close).toEqual({
type: 'Close',
data: { code: WEBSOCKET_CLOSE_CODE, reason: WEBSOCKET_CLOSE_REASON }
})
expect(result.sendError).toMatch(/connection not found/)
})
it('disconnect sends a normal close frame', async () => {
const close = await tauri(async (api, url) => {
const ws = await api.websocket.connect(url)
// the server acknowledges the close with the same frame
return await new Promise<Message>((resolve, reject) => {
setTimeout(() => reject(new Error('close not received')), 5000)
ws.addListener((message) => {
if (message.type === 'Close') resolve(message)
})
ws.disconnect().catch(reject)
})
}, WEBSOCKET_FIXTURE_URL)
expect(close).toEqual({
type: 'Close',
data: { code: 1000, reason: 'Disconnected by client' }
})
})
it('connecting to a closed port is rejected', async () => {
// port 1 (tcpmux) is never listening on the loopback interface
const error = await tauriError((api) =>
api.websocket.connect('ws://127.0.0.1:1/')
)
expect(error).toMatch(/refused|connect/i)
})
it('invalid URLs and header names are rejected', async () => {
const url = await tauriError((api) => api.websocket.connect('not a url'))
expect(url).toMatch(/invalid uri/i)
const header = await tauriError(
(api, url) =>
api.websocket.connect(url, { headers: [['bad header', 'value']] }),
WEBSOCKET_FIXTURE_URL
)
// Chromium (Android) already rejects the name in the `Headers` constructor;
// WebKit passes it through and the plugin rejects it
expect(header).toMatch(/invalid (header )?name/i)
})
it('sending an unsupported message type throws', async () => {
const error = await tauriError(async (api, url) => {
const ws = await api.websocket.connect(url)
try {
await ws.send(42 as unknown as string)
} finally {
await ws.disconnect()
}
}, WEBSOCKET_FIXTURE_URL)
expect(error).toMatch(/invalid `message` type/)
})
})
+42 -3
View File
@@ -164,7 +164,8 @@ export function mobileConfig(platform: MobilePlatform): WebdriverIO.Config {
'--apk', '--apk',
'--target', '--target',
androidTarget() androidTarget()
] ],
ios ? {} : androidBuildEnv()
) )
} }
@@ -282,11 +283,12 @@ for (const signal of [
} }
/** Runs `pnpm tauri <args>` in examples/api, failing loudly. */ /** Runs `pnpm tauri <args>` in examples/api, failing loudly. */
function tauriCli(args: string[]): void { function tauriCli(args: string[], env: NodeJS.ProcessEnv = {}): void {
const result = spawnSync('pnpm', ['tauri', ...args], { const result = spawnSync('pnpm', ['tauri', ...args], {
cwd: appDir, cwd: appDir,
stdio: 'inherit', stdio: 'inherit',
shell: true shell: true,
env: { ...process.env, ...env }
}) })
if (result.status !== 0) { if (result.status !== 0) {
throw new Error( throw new Error(
@@ -297,6 +299,39 @@ function tauriCli(args: string[]): void {
// --- Android ----------------------------------------------------------------- // --- Android -----------------------------------------------------------------
/**
* Environment for the Android build.
*
* - No debug info: the suite needs a debug build (webview debugging follows
* `debug_assertions`), but with the stronghold, sql and websocket plugins the
* debug info alone grows the APK past what a default emulator can install
* ("not enough space").
* - On a macOS host, `AR`/`RANLIB` point at the NDK's LLVM tools. Autotools-built
* C dependencies (libsodium, through the stronghold plugin) otherwise fall back
* to Apple's `ar`/`ranlib`, which silently produce an empty archive from the
* Android (ELF) objects, and the app then fails to load its library with an
* unresolved symbol. Linux hosts' GNU `ar` is fine.
*/
function androidBuildEnv(): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = { CARGO_PROFILE_DEV_DEBUG: '0' }
const ndk = process.env.NDK_HOME ?? process.env.ANDROID_NDK_HOME
if (process.platform !== 'darwin' || !ndk) return env
// the NDK only ships an x86_64 (Rosetta-compatible) macOS toolchain
const bin = path.join(
ndk,
'toolchains',
'llvm',
'prebuilt',
'darwin-x86_64',
'bin'
)
return {
...env,
AR: path.join(bin, 'llvm-ar'),
RANLIB: path.join(bin, 'llvm-ranlib')
}
}
/** `adb` from the Android SDK, else whatever is on `PATH`. */ /** `adb` from the Android SDK, else whatever is on `PATH`. */
function adb(args: string[]): SpawnSyncReturns<string> { function adb(args: string[]): SpawnSyncReturns<string> {
const sdk = process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT const sdk = process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT
@@ -329,6 +364,10 @@ function androidCapabilities(app: string): WebdriverIO.Capabilities {
'appium:app': app, 'appium:app': app,
'appium:appPackage': appId, 'appium:appPackage': appId,
'appium:appActivity': '.MainActivity', 'appium:appActivity': '.MainActivity',
// Every build has the same version code, and Appium otherwise skips
// installing an APK whose version is already on the device, so a rebuilt
// app would never replace the installed one.
'appium:enforceAppInstall': true,
// Which device/emulator to use; Appium picks the first connected one // Which device/emulator to use; Appium picks the first connected one
// otherwise. E2E_ANDROID_AVD instead boots that AVD. // otherwise. E2E_ANDROID_AVD instead boots that AVD.
...(process.env.E2E_ANDROID_DEVICE ...(process.env.E2E_ANDROID_DEVICE
@@ -39,7 +39,8 @@ class BatchArgs {
@InvokeArg @InvokeArg
class CancelArgs { class CancelArgs {
lateinit var notifications: List<Int> // `cancelAll()` sends no list: cancel every pending notification
var notifications: List<Int>? = null
} }
@InvokeArg @InvokeArg
@@ -152,7 +153,9 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
@Command @Command
fun cancel(invoke: Invoke) { fun cancel(invoke: Invoke) {
val args = invoke.parseArgs(CancelArgs::class.java) val args = invoke.parseArgs(CancelArgs::class.java)
manager.cancel(args.notifications) manager.cancel(
args.notifications ?: notificationStorage.getSavedNotificationIds().mapNotNull { it.toIntOrNull() }
)
invoke.resolve() invoke.resolve()
} }
@@ -192,32 +195,33 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
@SuppressLint("ObsoleteSdkInt") @SuppressLint("ObsoleteSdkInt")
@Command @Command
fun getActive(invoke: Invoke) { fun getActive(invoke: Invoke) {
val notifications = JSArray() // plain lists and maps: until tauri serializes org.json values natively,
// `resolveObject` turns a `JSArray` into `{ "values": [...] }`
val notifications = mutableListOf<Map<String, Any?>>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val activeNotifications = notificationManager.activeNotifications val activeNotifications = notificationManager.activeNotifications
for (activeNotification in activeNotifications) { for (activeNotification in activeNotifications) {
val jsNotification = JSObject() val jsNotification = mutableMapOf<String, Any?>(
jsNotification.put("id", activeNotification.id) "id" to activeNotification.id,
jsNotification.put("tag", activeNotification.tag) "tag" to activeNotification.tag
)
val notification = activeNotification.notification val notification = activeNotification.notification
if (notification != null) { if (notification != null) {
jsNotification.put("title", notification.extras.getCharSequence(android.app.Notification.EXTRA_TITLE)) jsNotification["title"] = notification.extras.getCharSequence(android.app.Notification.EXTRA_TITLE)?.toString()
jsNotification.put("body", notification.extras.getCharSequence(android.app.Notification.EXTRA_TEXT)) jsNotification["body"] = notification.extras.getCharSequence(android.app.Notification.EXTRA_TEXT)?.toString()
jsNotification.put("group", notification.group) jsNotification["group"] = notification.group
jsNotification.put( jsNotification["groupSummary"] =
"groupSummary",
0 != notification.flags and android.app.Notification.FLAG_GROUP_SUMMARY 0 != notification.flags and android.app.Notification.FLAG_GROUP_SUMMARY
) val extras = mutableMapOf<String, String?>()
val extras = JSObject()
for (key in notification.extras.keySet()) { for (key in notification.extras.keySet()) {
extras.put(key!!, notification.extras.getString(key)) extras[key] = notification.extras.getString(key)
} }
jsNotification.put("data", extras) jsNotification["data"] = extras
} }
notifications.put(jsNotification) notifications.add(jsNotification)
} }
} }
invoke.resolveObject(notifications) invoke.resolveObject(notifications)
} }
+1 -1
View File
@@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_NOTIFICATION__=function(i,n){"use strict";var e,t,o;i.ScheduleEvery=void 0,(e=i.ScheduleEvery||(i.ScheduleEvery={})).Year="year",e.Month="month",e.TwoWeeks="twoWeeks",e.Week="week",e.Day="day",e.Hour="hour",e.Minute="minute",e.Second="second";return i.Importance=void 0,(t=i.Importance||(i.Importance={}))[t.None=0]="None",t[t.Min=1]="Min",t[t.Low=2]="Low",t[t.Default=3]="Default",t[t.High=4]="High",i.Visibility=void 0,(o=i.Visibility||(i.Visibility={}))[o.Secret=-1]="Secret",o[o.Private=0]="Private",o[o.Public=1]="Public",i.Schedule=class{static at(i,n=!1,e=!1){return{at:{date:i,repeating:n,allowWhileIdle:e},interval:void 0,every:void 0}}static interval(i,n=!1){return{at:void 0,interval:{interval:i,allowWhileIdle:n},every:void 0}}static every(i,n,e=!1){return{at:void 0,interval:void 0,every:{interval:i,count:n,allowWhileIdle:e}}}},i.active=async function(){return await n.invoke("plugin:notification|get_active")},i.cancel=async function(i){await n.invoke("plugin:notification|cancel",{notifications:i})},i.cancelAll=async function(){await n.invoke("plugin:notification|cancel")},i.channels=async function(){return await n.invoke("plugin:notification|listChannels")},i.createChannel=async function(i){await n.invoke("plugin:notification|create_channel",{...i})},i.isPermissionGranted=async function(){return"default"!==window.Notification.permission?await Promise.resolve("granted"===window.Notification.permission):await n.invoke("plugin:notification|is_permission_granted")},i.onAction=async function(i){return await n.addPluginListener("notification","actionPerformed",i)},i.onNotificationReceived=async function(i){return await n.addPluginListener("notification","notification",i)},i.pending=async function(){return await n.invoke("plugin:notification|get_pending")},i.registerActionTypes=async function(i){await n.invoke("plugin:notification|register_action_types",{types:i})},i.removeActive=async function(i){await n.invoke("plugin:notification|remove_active",{notifications:i})},i.removeAllActive=async function(){await n.invoke("plugin:notification|remove_active")},i.removeChannel=async function(i){await n.invoke("plugin:notification|delete_channel",{id:i})},i.requestPermission=async function(){return await window.Notification.requestPermission()},i.sendNotification=function(i){"string"==typeof i?new window.Notification(i):new window.Notification(i.title,i)},i}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"notification",{value:__TAURI_PLUGIN_NOTIFICATION__})} if("__TAURI__"in window){var __TAURI_PLUGIN_NOTIFICATION__=function(i,n){"use strict";var e,t,o;i.ScheduleEvery=void 0,(e=i.ScheduleEvery||(i.ScheduleEvery={})).Year="year",e.Month="month",e.TwoWeeks="twoWeeks",e.Week="week",e.Day="day",e.Hour="hour",e.Minute="minute",e.Second="second";return i.Importance=void 0,(t=i.Importance||(i.Importance={}))[t.None=0]="None",t[t.Min=1]="Min",t[t.Low=2]="Low",t[t.Default=3]="Default",t[t.High=4]="High",i.Visibility=void 0,(o=i.Visibility||(i.Visibility={}))[o.Secret=-1]="Secret",o[o.Private=0]="Private",o[o.Public=1]="Public",i.Schedule=class{static at(i,n=!1,e=!1){return{at:{date:i,repeating:n,allowWhileIdle:e},interval:void 0,every:void 0}}static interval(i,n=!1){return{at:void 0,interval:{interval:i,allowWhileIdle:n},every:void 0}}static every(i,n,e=!1){return{at:void 0,interval:void 0,every:{interval:i,count:n,allowWhileIdle:e}}}},i.active=async function(){return await n.invoke("plugin:notification|get_active")},i.cancel=async function(i){await n.invoke("plugin:notification|cancel",{notifications:i})},i.cancelAll=async function(){await n.invoke("plugin:notification|cancel")},i.channels=async function(){return await n.invoke("plugin:notification|list_channels")},i.createChannel=async function(i){await n.invoke("plugin:notification|create_channel",{...i})},i.isPermissionGranted=async function(){return"default"!==window.Notification.permission?await Promise.resolve("granted"===window.Notification.permission):await n.invoke("plugin:notification|is_permission_granted")},i.onAction=async function(i){return await n.addPluginListener("notification","actionPerformed",i)},i.onNotificationReceived=async function(i){return await n.addPluginListener("notification","notification",i)},i.pending=async function(){return await n.invoke("plugin:notification|get_pending")},i.registerActionTypes=async function(i){await n.invoke("plugin:notification|register_action_types",{types:i})},i.removeActive=async function(i){await n.invoke("plugin:notification|remove_active",{notifications:i})},i.removeAllActive=async function(){await n.invoke("plugin:notification|remove_active")},i.removeChannel=async function(i){await n.invoke("plugin:notification|delete_channel",{id:i})},i.requestPermission=async function(){return await window.Notification.requestPermission()},i.sendNotification=function(i){"string"==typeof i?new window.Notification(i):new window.Notification(i.title,i)},i}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"notification",{value:__TAURI_PLUGIN_NOTIFICATION__})}
+1
View File
@@ -8,6 +8,7 @@ const COMMANDS: &[&str] = &[
"is_permission_granted", "is_permission_granted",
"register_action_types", "register_action_types",
"register_listener", "register_listener",
"remove_listener",
"cancel", "cancel",
"get_pending", "get_pending",
"remove_active", "remove_active",
+1 -1
View File
@@ -879,7 +879,7 @@ async function removeChannel(id: string): Promise<void> {
* @since 2.0.0 * @since 2.0.0
*/ */
async function channels(): Promise<Channel[]> { async function channels(): Promise<Channel[]> {
return await invoke('plugin:notification|listChannels') return await invoke('plugin:notification|list_channels')
} }
/** /**
@@ -120,7 +120,8 @@ func showNotification(invoke: Invoke, notification: Notification)
} }
struct CancelArgs: Decodable { struct CancelArgs: Decodable {
let notifications: [Int] // `cancelAll()` sends no list: cancel every pending notification
let notifications: [Int]?
} }
struct Action: Decodable { struct Action: Decodable {
@@ -216,9 +217,13 @@ class NotificationPlugin: Plugin {
@objc func cancel(_ invoke: Invoke) throws { @objc func cancel(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(CancelArgs.self) let args = try invoke.parseArgs(CancelArgs.self)
UNUserNotificationCenter.current().removePendingNotificationRequests( if let notifications = args.notifications {
withIdentifiers: args.notifications.map { String($0) } UNUserNotificationCenter.current().removePendingNotificationRequests(
) withIdentifiers: notifications.map { String($0) }
)
} else {
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
}
invoke.resolve() invoke.resolve()
} }
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-remove-listener"
description = "Enables the remove_listener command without any pre-configured scope."
commands.allow = ["remove_listener"]
[[permission]]
identifier = "deny-remove-listener"
description = "Denies the remove_listener command without any pre-configured scope."
commands.deny = ["remove_listener"]
@@ -14,6 +14,7 @@ It allows all notification related features.
- `allow-notify` - `allow-notify`
- `allow-register-action-types` - `allow-register-action-types`
- `allow-register-listener` - `allow-register-listener`
- `allow-remove-listener`
- `allow-cancel` - `allow-cancel`
- `allow-get-pending` - `allow-get-pending`
- `allow-remove-active` - `allow-remove-active`
@@ -402,6 +403,32 @@ Denies the remove_active command without any pre-configured scope.
<tr> <tr>
<td> <td>
`notification:allow-remove-listener`
</td>
<td>
Enables the remove_listener command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`notification:deny-remove-listener`
</td>
<td>
Denies the remove_listener command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`notification:allow-request-permission` `notification:allow-request-permission`
</td> </td>
@@ -16,6 +16,7 @@ permissions = [
"allow-notify", "allow-notify",
"allow-register-action-types", "allow-register-action-types",
"allow-register-listener", "allow-register-listener",
"allow-remove-listener",
"allow-cancel", "allow-cancel",
"allow-get-pending", "allow-get-pending",
"allow-remove-active", "allow-remove-active",
@@ -462,6 +462,18 @@
"const": "deny-remove-active", "const": "deny-remove-active",
"markdownDescription": "Denies the remove_active command without any pre-configured scope." "markdownDescription": "Denies the remove_active command without any pre-configured scope."
}, },
{
"description": "Enables the remove_listener command without any pre-configured scope.",
"type": "string",
"const": "allow-remove-listener",
"markdownDescription": "Enables the remove_listener command without any pre-configured scope."
},
{
"description": "Denies the remove_listener command without any pre-configured scope.",
"type": "string",
"const": "deny-remove-listener",
"markdownDescription": "Denies the remove_listener command without any pre-configured scope."
},
{ {
"description": "Enables the request_permission command without any pre-configured scope.", "description": "Enables the request_permission command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -487,10 +499,10 @@
"markdownDescription": "Denies the show command without any pre-configured scope." "markdownDescription": "Denies the show command without any pre-configured scope."
}, },
{ {
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`", "description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
"type": "string", "type": "string",
"const": "default", "const": "default",
"markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`" "markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`"
} }
] ]
} }
+2
View File
@@ -287,6 +287,8 @@ mod imp {
use std::path::PathBuf; use std::path::PathBuf;
pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> { pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> {
// these AppKit calls are only `unsafe` before objc2-app-kit 0.3.2
#[allow(unused_unsafe)]
unsafe { unsafe {
let mut urls = Vec::new(); let mut urls = Vec::new();
+63 -21
View File
@@ -156,6 +156,9 @@ importers:
'@tauri-apps/api': '@tauri-apps/api':
specifier: ^2.11.0 specifier: ^2.11.0
version: 2.11.0 version: 2.11.0
'@tauri-apps/plugin-autostart':
specifier: workspace:*
version: link:../../plugins/autostart
'@tauri-apps/plugin-barcode-scanner': '@tauri-apps/plugin-barcode-scanner':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/barcode-scanner version: link:../../plugins/barcode-scanner
@@ -168,6 +171,9 @@ importers:
'@tauri-apps/plugin-clipboard-manager': '@tauri-apps/plugin-clipboard-manager':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/clipboard-manager version: link:../../plugins/clipboard-manager
'@tauri-apps/plugin-deep-link':
specifier: workspace:*
version: link:../../plugins/deep-link
'@tauri-apps/plugin-dialog': '@tauri-apps/plugin-dialog':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/dialog version: link:../../plugins/dialog
@@ -198,21 +204,33 @@ importers:
'@tauri-apps/plugin-os': '@tauri-apps/plugin-os':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/os version: link:../../plugins/os
'@tauri-apps/plugin-positioner':
specifier: workspace:*
version: link:../../plugins/positioner
'@tauri-apps/plugin-process': '@tauri-apps/plugin-process':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/process version: link:../../plugins/process
'@tauri-apps/plugin-shell': '@tauri-apps/plugin-shell':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/shell version: link:../../plugins/shell
'@tauri-apps/plugin-sql':
specifier: workspace:*
version: link:../../plugins/sql
'@tauri-apps/plugin-store': '@tauri-apps/plugin-store':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/store version: link:../../plugins/store
'@tauri-apps/plugin-stronghold':
specifier: workspace:*
version: link:../../plugins/stronghold
'@tauri-apps/plugin-updater': '@tauri-apps/plugin-updater':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/updater version: link:../../plugins/updater
'@tauri-apps/plugin-upload': '@tauri-apps/plugin-upload':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/upload version: link:../../plugins/upload
'@tauri-apps/plugin-websocket':
specifier: workspace:*
version: link:../../plugins/websocket
'@zerodevx/svelte-json-view': '@zerodevx/svelte-json-view':
specifier: 2.0.0 specifier: 2.0.0
version: 2.0.0(svelte@5.55.7(@typescript-eslint/types@8.70.0)) version: 2.0.0(svelte@5.55.7(@typescript-eslint/types@8.70.0))
@@ -248,11 +266,14 @@ importers:
specifier: ^2.0.9 specifier: ^2.0.9
version: 2.0.9 version: 2.0.9
'@crabnebula/test-runner-backend': '@crabnebula/test-runner-backend':
specifier: ^0.2.9 specifier: ^0.3.0
version: 0.2.9 version: 0.3.0
'@tauri-apps/api': '@tauri-apps/api':
specifier: ^2.11.0 specifier: ^2.11.0
version: 2.11.0 version: 2.11.0
'@tauri-apps/plugin-autostart':
specifier: workspace:*
version: link:../../plugins/autostart
'@tauri-apps/plugin-barcode-scanner': '@tauri-apps/plugin-barcode-scanner':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/barcode-scanner version: link:../../plugins/barcode-scanner
@@ -265,6 +286,9 @@ importers:
'@tauri-apps/plugin-clipboard-manager': '@tauri-apps/plugin-clipboard-manager':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/clipboard-manager version: link:../../plugins/clipboard-manager
'@tauri-apps/plugin-deep-link':
specifier: workspace:*
version: link:../../plugins/deep-link
'@tauri-apps/plugin-dialog': '@tauri-apps/plugin-dialog':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/dialog version: link:../../plugins/dialog
@@ -298,27 +322,42 @@ importers:
'@tauri-apps/plugin-os': '@tauri-apps/plugin-os':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/os version: link:../../plugins/os
'@tauri-apps/plugin-positioner':
specifier: workspace:*
version: link:../../plugins/positioner
'@tauri-apps/plugin-process': '@tauri-apps/plugin-process':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/process version: link:../../plugins/process
'@tauri-apps/plugin-shell': '@tauri-apps/plugin-shell':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/shell version: link:../../plugins/shell
'@tauri-apps/plugin-sql':
specifier: workspace:*
version: link:../../plugins/sql
'@tauri-apps/plugin-store': '@tauri-apps/plugin-store':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/store version: link:../../plugins/store
'@tauri-apps/plugin-stronghold':
specifier: workspace:*
version: link:../../plugins/stronghold
'@tauri-apps/plugin-updater': '@tauri-apps/plugin-updater':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/updater version: link:../../plugins/updater
'@tauri-apps/plugin-upload': '@tauri-apps/plugin-upload':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/upload version: link:../../plugins/upload
'@tauri-apps/plugin-websocket':
specifier: workspace:*
version: link:../../plugins/websocket
'@tauri-apps/plugin-window-state': '@tauri-apps/plugin-window-state':
specifier: workspace:* specifier: workspace:*
version: link:../../plugins/window-state version: link:../../plugins/window-state
'@types/node': '@types/node':
specifier: ^22.10.0 specifier: ^22.10.0
version: 22.20.3 version: 22.20.3
'@types/ws':
specifier: ^8.18.1
version: 8.18.1
'@wdio/appium-service': '@wdio/appium-service':
specifier: ^9.31.9 specifier: ^9.31.9
version: 9.32.0(supports-color@8.1.1) version: 9.32.0(supports-color@8.1.1)
@@ -355,6 +394,9 @@ importers:
webdriverio: webdriverio:
specifier: ^9.31.9 specifier: ^9.31.9
version: 9.32.0(supports-color@8.1.1) version: 9.32.0(supports-color@8.1.1)
ws:
specifier: ^8.18.0
version: 8.21.3
plugins/autostart: plugins/autostart:
dependencies: dependencies:
@@ -657,33 +699,33 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
hasBin: true hasBin: true
'@crabnebula/test-runner-backend-darwin-arm64@0.2.9': '@crabnebula/test-runner-backend-darwin-arm64@0.3.0':
resolution: {integrity: sha512-UAlL8VA23y/fp+MigNYFwGvtE9vJ518f846c0k2vgT/bLIpt+d3UXndjZCMYbEVFTmaq00wHoRPSPbyzHEMWlQ==} resolution: {integrity: sha512-XnZdSeew2ew/Eix+z1BwKt5PcrJSjL+VQ7dlrV1fvRj4WUTQtluzE1VhiVV0j2EIFJsRH2IExNj5OdCWxKSPiQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@crabnebula/test-runner-backend-darwin-x64@0.2.9': '@crabnebula/test-runner-backend-darwin-x64@0.3.0':
resolution: {integrity: sha512-RUP9L4Lk8KsT4A64U4QFVy5rj99Y6iIichPLpZgTvpRybYKZBVXjUiU31aGUIlWnd4VHsWKLM2vNl90rKZm3lA==} resolution: {integrity: sha512-dpogk8TxKd2CZb8BZ2Ni1mpbogD8KSoB8J92aw+9R24S8xBeTu8I+t4w9PVNIBvFaNAhEQelYhFffxH0FDs5+Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@crabnebula/test-runner-backend-linux-x64-gnu@0.2.9': '@crabnebula/test-runner-backend-linux-x64-gnu@0.3.0':
resolution: {integrity: sha512-js/haLE9OUsa0oDHNuVps5DY7av3iWG7PpAKjr9nMnIEXiLACDmvyUL/IplHMmu1lr6S0fztoudRf4JfTR+mfA==} resolution: {integrity: sha512-ZzDVKdY1ydv+lqfRYs6PAdmIc8AlfOciH3ntuUxyRJ8aSq0v8y+pow6b3/CRqmumkj9riVWsUriJr7Xw2YZFTg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@crabnebula/test-runner-backend-win32-x64-msvc@0.2.9': '@crabnebula/test-runner-backend-win32-x64-msvc@0.3.0':
resolution: {integrity: sha512-p4pc/GQ0bj3vVLuUyTHDnhaXO52EQNkaCUPhWXwEkG00loW93pvCBRrOWoYpJH9me/n2zMFv61meW9mQyvbByw==} resolution: {integrity: sha512-xzE5bZx6QpqCkGFTpOYGxMfYidfsvYQgp3yQ1hXwuxeC5vsabOngRtE6X2DxQU6U8JzzNaooN0afG/khzoaihQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@crabnebula/test-runner-backend@0.2.9': '@crabnebula/test-runner-backend@0.3.0':
resolution: {integrity: sha512-3z6qZbJtVyA1JymQU5hFqcxG6nFD5INgqBc3OxTbYXVzMpihlRGOcHWyPK72ndHK88YmfwQWPcEFN1R/H/Lvhw==} resolution: {integrity: sha512-72aldFCPV0Sflv8KGLeaNoUY4Vz8eRl7AvUsJXFwqJ7KKNDAfPKwovXjmZoqIWn6Icv01sEsd1b2w0O66Y3WEQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
hasBin: true hasBin: true
@@ -4911,24 +4953,24 @@ snapshots:
'@crabnebula/tauri-driver-linux-x64-gnu': 2.0.9 '@crabnebula/tauri-driver-linux-x64-gnu': 2.0.9
'@crabnebula/tauri-driver-win32-x64-msvc': 2.0.9 '@crabnebula/tauri-driver-win32-x64-msvc': 2.0.9
'@crabnebula/test-runner-backend-darwin-arm64@0.2.9': '@crabnebula/test-runner-backend-darwin-arm64@0.3.0':
optional: true optional: true
'@crabnebula/test-runner-backend-darwin-x64@0.2.9': '@crabnebula/test-runner-backend-darwin-x64@0.3.0':
optional: true optional: true
'@crabnebula/test-runner-backend-linux-x64-gnu@0.2.9': '@crabnebula/test-runner-backend-linux-x64-gnu@0.3.0':
optional: true optional: true
'@crabnebula/test-runner-backend-win32-x64-msvc@0.2.9': '@crabnebula/test-runner-backend-win32-x64-msvc@0.3.0':
optional: true optional: true
'@crabnebula/test-runner-backend@0.2.9': '@crabnebula/test-runner-backend@0.3.0':
optionalDependencies: optionalDependencies:
'@crabnebula/test-runner-backend-darwin-arm64': 0.2.9 '@crabnebula/test-runner-backend-darwin-arm64': 0.3.0
'@crabnebula/test-runner-backend-darwin-x64': 0.2.9 '@crabnebula/test-runner-backend-darwin-x64': 0.3.0
'@crabnebula/test-runner-backend-linux-x64-gnu': 0.2.9 '@crabnebula/test-runner-backend-linux-x64-gnu': 0.3.0
'@crabnebula/test-runner-backend-win32-x64-msvc': 0.2.9 '@crabnebula/test-runner-backend-win32-x64-msvc': 0.3.0
'@dabh/diagnostics@2.0.9': '@dabh/diagnostics@2.0.9':
dependencies: dependencies: