mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-09-24 21:40:48 +02:00
tests(e2e): improve coverage
This commit is contained in:
@@ -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
|
||||
env:
|
||||
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
|
||||
|
||||
- name: save gradle cache
|
||||
|
||||
@@ -79,7 +79,9 @@ jobs:
|
||||
librsvg2-dev \
|
||||
dbus \
|
||||
xvfb \
|
||||
fluxbox
|
||||
fluxbox \
|
||||
desktop-file-utils \
|
||||
xdg-utils
|
||||
|
||||
# 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
|
||||
|
||||
Generated
+88
-52
@@ -215,10 +215,12 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-automation",
|
||||
"tauri-plugin-autostart",
|
||||
"tauri-plugin-barcode-scanner",
|
||||
"tauri-plugin-biometric",
|
||||
"tauri-plugin-cli",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-deep-link",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-geolocation",
|
||||
@@ -230,11 +232,15 @@ dependencies = [
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-positioner",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-sql",
|
||||
"tauri-plugin-store",
|
||||
"tauri-plugin-stronghold",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-upload",
|
||||
"tauri-plugin-websocket",
|
||||
"tauri-plugin-window-state",
|
||||
"time",
|
||||
"tiny_http",
|
||||
@@ -298,7 +304,7 @@ dependencies = [
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.59.0",
|
||||
@@ -1554,7 +1560,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.0",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1597,7 +1603,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||
dependencies = [
|
||||
"libloading 0.7.4",
|
||||
"libloading 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3509,7 +3515,7 @@ checksum = "0b95dfb34071d1592b45622bf93e315e3a72d414b6782aca9a015c12bec367ef"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"objc2 0.6.4",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
"time",
|
||||
]
|
||||
|
||||
@@ -3645,12 +3651,12 @@ dependencies = [
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
"once_cell",
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3900,9 +3906,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "objc2-app-kit"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb"
|
||||
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"block2 0.6.2",
|
||||
@@ -3913,49 +3919,53 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-core-image",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-quartz-core 0.3.0",
|
||||
"objc2-core-text",
|
||||
"objc2-core-video",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-quartz-core 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-cloud-kit"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c1948a9be5f469deadbd6bcb86ad7ff9e47b4f632380139722f7d9840c0d42c"
|
||||
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"objc2 0.6.4",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-data"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4"
|
||||
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"objc2 0.6.4",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"dispatch2",
|
||||
"objc2 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-graphics"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02"
|
||||
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"dispatch2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-surface",
|
||||
@@ -3963,12 +3973,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-image"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ffa6bea72bf42c78b0b34e89c0bafac877d5f80bf91e159a5d96ea7f693ca56"
|
||||
checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
|
||||
dependencies = [
|
||||
"objc2 0.6.4",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3978,7 +3988,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d31f4c5b5192304996badc466aeadffe1411d73a9bbd3b18b6b2ee9d048b07bd"
|
||||
dependencies = [
|
||||
"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]]
|
||||
@@ -4010,9 +4045,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"block2 0.6.2",
|
||||
@@ -4023,9 +4058,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "objc2-io-surface"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19"
|
||||
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"objc2 0.6.4",
|
||||
@@ -4053,7 +4088,7 @@ dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4071,14 +4106,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fb3794501bb1bee12f08dcad8c61f2a5875791ad1c6f47faa71a0f033f20071"
|
||||
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4096,8 +4131,8 @@ dependencies = [
|
||||
"objc2-core-graphics",
|
||||
"objc2-core-image",
|
||||
"objc2-core-location",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-quartz-core 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-quartz-core 0.3.2",
|
||||
"objc2-user-notifications",
|
||||
]
|
||||
|
||||
@@ -4108,7 +4143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "670fe793adbf3b5e93686d48a05a7ed7ee53dfa65d106ced4805fae8969059b2"
|
||||
dependencies = [
|
||||
"objc2 0.6.4",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4122,7 +4157,7 @@ dependencies = [
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4266,7 +4301,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
|
||||
dependencies = [
|
||||
"objc2 0.6.4",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-osa-kit",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -5044,7 +5079,7 @@ dependencies = [
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
"pollster",
|
||||
"raw-window-handle",
|
||||
"urlencoding",
|
||||
@@ -5218,7 +5253,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.11.0",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5275,7 +5310,7 @@ dependencies = [
|
||||
"security-framework 3.5.1",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6297,7 +6332,7 @@ dependencies = [
|
||||
"ndk-sys",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-ui-kit",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
@@ -6372,7 +6407,7 @@ dependencies = [
|
||||
"muda",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
"percent-encoding",
|
||||
@@ -6483,12 +6518,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-automation"
|
||||
version = "0.1.4"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "314d4ba88ce4a5e17c5f1e2dad990edb5956643b6e04a6d127d39aa64d0f2a7d"
|
||||
checksum = "40015ce24bdb1204983f83ac37c5469d55028de1e7fc56cdd0a6a7e15a1b5375"
|
||||
dependencies = [
|
||||
"libloading 0.8.9",
|
||||
"objc2-app-kit",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
@@ -6604,7 +6640,7 @@ dependencies = [
|
||||
"log",
|
||||
"notify",
|
||||
"notify-debouncer-full",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
"percent-encoding",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -6701,7 +6737,7 @@ dependencies = [
|
||||
"fern",
|
||||
"log",
|
||||
"objc2 0.6.4",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@@ -6752,7 +6788,7 @@ dependencies = [
|
||||
"dunce",
|
||||
"glob",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
"open",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -7108,7 +7144,7 @@ dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"once_cell",
|
||||
"rustix 1.1.3",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7535,12 +7571,12 @@ dependencies = [
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
"once_cell",
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8261,7 +8297,7 @@ dependencies = [
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
"raw-window-handle",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-version",
|
||||
@@ -8840,7 +8876,7 @@ dependencies = [
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.0",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
"once_cell",
|
||||
|
||||
+17
@@ -32,6 +32,23 @@ license = "Apache-2.0 OR MIT"
|
||||
rust-version = "1.77.2"
|
||||
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
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.11.0",
|
||||
"@tauri-apps/plugin-autostart": "workspace:*",
|
||||
"@tauri-apps/plugin-barcode-scanner": "workspace:*",
|
||||
"@tauri-apps/plugin-biometric": "workspace:*",
|
||||
"@tauri-apps/plugin-cli": "workspace:*",
|
||||
"@tauri-apps/plugin-clipboard-manager": "workspace:*",
|
||||
"@tauri-apps/plugin-deep-link": "workspace:*",
|
||||
"@tauri-apps/plugin-dialog": "workspace:*",
|
||||
"@tauri-apps/plugin-fs": "workspace:*",
|
||||
"@tauri-apps/plugin-geolocation": "workspace:*",
|
||||
@@ -25,11 +27,15 @@
|
||||
"@tauri-apps/plugin-notification": "workspace:*",
|
||||
"@tauri-apps/plugin-opener": "workspace:*",
|
||||
"@tauri-apps/plugin-os": "workspace:*",
|
||||
"@tauri-apps/plugin-positioner": "workspace:*",
|
||||
"@tauri-apps/plugin-process": "workspace:*",
|
||||
"@tauri-apps/plugin-shell": "workspace:*",
|
||||
"@tauri-apps/plugin-sql": "workspace:*",
|
||||
"@tauri-apps/plugin-store": "workspace:*",
|
||||
"@tauri-apps/plugin-stronghold": "workspace:*",
|
||||
"@tauri-apps/plugin-updater": "workspace:*",
|
||||
"@tauri-apps/plugin-upload": "workspace:*",
|
||||
"@tauri-apps/plugin-websocket": "workspace:*",
|
||||
"@zerodevx/svelte-json-view": "2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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-store = { path = "../../../plugins/store", version = "2.4.5" }
|
||||
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).
|
||||
# Desktop-only and behind the off-by-default `automation` feature so it never ships in a
|
||||
# regular build.
|
||||
[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]
|
||||
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]
|
||||
tauri-plugin-autostart = { path = "../../../plugins/autostart", version = "2.5.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-positioner = { path = "../../../plugins/positioner", version = "2.3.4", features = [
|
||||
"tray-icon",
|
||||
] }
|
||||
tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.12.0" }
|
||||
tauri-plugin-window-state = { path = "../../../plugins/window-state", version = "2.2.0" }
|
||||
|
||||
|
||||
@@ -132,6 +132,17 @@
|
||||
"identifier": "opener:allow-open-path",
|
||||
"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"],
|
||||
"platforms": ["linux", "macOS", "windows"],
|
||||
"permissions": [
|
||||
"autostart:default",
|
||||
"cli:default",
|
||||
"positioner:default",
|
||||
"updater:default",
|
||||
"global-shortcut:allow-unregister",
|
||||
"global-shortcut:allow-register",
|
||||
|
||||
@@ -9,7 +9,7 @@ mod tray;
|
||||
use serde::Serialize;
|
||||
use tauri::{
|
||||
webview::{PageLoadEvent, WebviewWindowBuilder},
|
||||
App, AppHandle, Emitter, Listener, RunEvent, WebviewUrl,
|
||||
App, AppHandle, Emitter, Listener, Manager, RunEvent, WebviewUrl,
|
||||
};
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
@@ -55,11 +55,40 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
.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| {
|
||||
// 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)]
|
||||
{
|
||||
// registered before the tray, whose events it tracks
|
||||
app.handle().plugin(tauri_plugin_positioner::init())?;
|
||||
tray::create_tray(app.handle())?;
|
||||
app.handle().plugin(tauri_plugin_cli::init())?;
|
||||
app.handle().plugin(tauri_plugin_autostart::init(
|
||||
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
|
||||
None,
|
||||
))?;
|
||||
app.handle()
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())?;
|
||||
app.handle()
|
||||
|
||||
@@ -107,6 +107,8 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
||||
_ => {}
|
||||
})
|
||||
.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 {
|
||||
button_state: MouseButtonState::Down,
|
||||
button: MouseButton::Left,
|
||||
|
||||
@@ -33,6 +33,12 @@
|
||||
import Geolocation from './views/Geolocation.svelte'
|
||||
import Haptics from './views/Haptics.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'
|
||||
|
||||
@@ -131,6 +137,36 @@
|
||||
component: WebRTC,
|
||||
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 && {
|
||||
label: 'Scanner',
|
||||
component: Scanner,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
skipped on desktop, and the rest run everywhere with the odd test gated.
|
||||
|
||||
| Plugin | Coverage | On mobile |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
||||
| `cli` | `getMatches` shape for an argument-less launch. | Skipped — desktop-only plugin. |
|
||||
| `clipboard-manager` | text, HTML and image round-trips, `clear`, error paths. | Text only; HTML and images are unsupported there. |
|
||||
| `dialog` | Only that the API is registered — every dialog blocks on native UI the driver cannot operate. | 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). |
|
||||
| `global-shortcut` | register/unregister/isRegistered/unregisterAll and error paths. Shortcuts cannot be triggered. | Skipped — desktop-only plugin. |
|
||||
| `http` | `fetch` methods, headers, JSON/bytes/multipart bodies, the cookie jar, abort, scope, failures. | Same. |
|
||||
| `log` | `attachLogger`/`attachConsole` for webview and Rust records, level filtering. | Same. |
|
||||
| `notification` | Permission model, `window.Notification` override, `sendNotification`. Display is not observable. | Sending only — see the note below. |
|
||||
| `opener` | Scope enforcement only — a successful open launches an external app the suite cannot close. | Same. |
|
||||
| `os` | Every function, compared against what the app was built for. | Same, minus the hostname/host comparison. |
|
||||
| `process` | Only that the API is registered — `exit`/`relaunch` terminate the app under test. | Same. |
|
||||
| `shell` | `execute`, `spawn` with stdout/stderr/close events, stdin, `kill`, scope enforcement. | Scope only on iOS, which cannot spawn a process at all. |
|
||||
| `store` | CRUD, persistence, auto-save, defaults/reset, reload, `getStore`, `LazyStore`, change events. | 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). |
|
||||
| `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. |
|
||||
| Plugin | Coverage | On mobile |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `autostart` | enable/disable/isEnabled, restoring the host's original state. | Skipped — desktop-only plugin. |
|
||||
| `cli` | `getMatches` shape for an argument-less launch. | Skipped — desktop-only plugin. |
|
||||
| `clipboard-manager` | text, HTML and image round-trips, `clear`, error paths. | Text only; HTML and images are unsupported there. |
|
||||
| `deep-link` | `getCurrent` on a plain launch, `onOpenUrl` delivery, runtime `register`/`isRegistered`/`unregister` on Linux and Windows. | Only that runtime registration is unsupported. |
|
||||
| `dialog` | Only that the API is registered — every dialog blocks on native UI the driver cannot operate. | 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). |
|
||||
| `global-shortcut` | register/unregister/isRegistered/unregisterAll and error paths. Shortcuts cannot be triggered. | Skipped — desktop-only plugin. |
|
||||
| `http` | `fetch` methods, headers, JSON/bytes/multipart bodies, the cookie jar, abort, scope, failures. | Same. |
|
||||
| `log` | `attachLogger`/`attachConsole` for webview and Rust records, level filtering. | Same. |
|
||||
| `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. |
|
||||
| `opener` | Scope enforcement only — a successful open launches an external app the suite cannot close. | Same. |
|
||||
| `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`. |
|
||||
| `positioner` | Screen and tray positions (with a tray rect handed in through `handleIconState`), `moveWindowConstrained`, the missing-tray error (WM dependent). | Skipped — desktop-only plugin. |
|
||||
| `process` | Only that the API is registered — `exit`/`relaunch` terminate the app under test. | Same. |
|
||||
| `shell` | `execute`, `spawn` with stdout/stderr/close events, stdin, `kill`, scope enforcement. | Scope only on iOS, which cannot spawn a process at all. |
|
||||
| `sql` | SQLite `load`/`execute`/`select`/`close`, bound values, column types, app-registered migrations, error paths. | Same. |
|
||||
| `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
|
||||
`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
|
||||
plugin the platform registers injects its global API with its documented members, and that
|
||||
|
||||
@@ -14,12 +14,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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/plugin-autostart": "workspace:*",
|
||||
"@tauri-apps/plugin-barcode-scanner": "workspace:*",
|
||||
"@tauri-apps/plugin-biometric": "workspace:*",
|
||||
"@tauri-apps/plugin-cli": "workspace:*",
|
||||
"@tauri-apps/plugin-clipboard-manager": "workspace:*",
|
||||
"@tauri-apps/plugin-deep-link": "workspace:*",
|
||||
"@tauri-apps/plugin-dialog": "workspace:*",
|
||||
"@tauri-apps/plugin-fs": "workspace:*",
|
||||
"@tauri-apps/plugin-geolocation": "workspace:*",
|
||||
@@ -31,13 +33,18 @@
|
||||
"@tauri-apps/plugin-notification": "workspace:*",
|
||||
"@tauri-apps/plugin-opener": "workspace:*",
|
||||
"@tauri-apps/plugin-os": "workspace:*",
|
||||
"@tauri-apps/plugin-positioner": "workspace:*",
|
||||
"@tauri-apps/plugin-process": "workspace:*",
|
||||
"@tauri-apps/plugin-shell": "workspace:*",
|
||||
"@tauri-apps/plugin-sql": "workspace:*",
|
||||
"@tauri-apps/plugin-store": "workspace:*",
|
||||
"@tauri-apps/plugin-stronghold": "workspace:*",
|
||||
"@tauri-apps/plugin-updater": "workspace:*",
|
||||
"@tauri-apps/plugin-upload": "workspace:*",
|
||||
"@tauri-apps/plugin-websocket": "workspace:*",
|
||||
"@tauri-apps/plugin-window-state": "workspace:*",
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@wdio/appium-service": "^9.31.9",
|
||||
"@wdio/cli": "^9.31.9",
|
||||
"@wdio/globals": "^9.31.3",
|
||||
@@ -49,6 +56,7 @@
|
||||
"appium-xcuitest-driver": "^12.12.4",
|
||||
"cross-env": "^10.1.0",
|
||||
"typescript": "6.0.3",
|
||||
"webdriverio": "^9.31.9"
|
||||
"webdriverio": "^9.31.9",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
|
||||
import { browser } from '@wdio/globals'
|
||||
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 Biometric from '@tauri-apps/plugin-biometric'
|
||||
import type * as Cli from '@tauri-apps/plugin-cli'
|
||||
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 Fs from '@tauri-apps/plugin-fs'
|
||||
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 Opener from '@tauri-apps/plugin-opener'
|
||||
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 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 Stronghold from '@tauri-apps/plugin-stronghold'
|
||||
import type * as Updater from '@tauri-apps/plugin-updater'
|
||||
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'
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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 {
|
||||
clipboardManager: typeof ClipboardManager
|
||||
deepLink: typeof DeepLink
|
||||
dialog: typeof Dialog
|
||||
fs: typeof Fs
|
||||
http: typeof Http
|
||||
@@ -42,14 +50,19 @@ export interface CommonPluginApi {
|
||||
os: typeof Os
|
||||
process: typeof Process
|
||||
shell: typeof Shell
|
||||
sql: typeof Sql.default
|
||||
store: typeof Store
|
||||
stronghold: typeof Stronghold
|
||||
upload: typeof Upload
|
||||
websocket: typeof WebSocket.default
|
||||
}
|
||||
|
||||
/** The plugin APIs the example only registers on desktop (`#[cfg(desktop)]`). */
|
||||
export interface DesktopPluginApi {
|
||||
autostart: typeof Autostart
|
||||
cli: typeof Cli
|
||||
globalShortcut: typeof GlobalShortcut
|
||||
positioner: typeof Positioner
|
||||
updater: typeof Updater
|
||||
windowState: typeof WindowState
|
||||
}
|
||||
@@ -231,8 +244,9 @@ const skippedModules = (process.env.E2E_SKIP ?? '')
|
||||
|
||||
export interface DescribePluginOptions {
|
||||
/**
|
||||
* The example only registers the plugin on desktop (`cli`, `global-shortcut`,
|
||||
* `updater`, `window-state`), so the whole suite is skipped on mobile.
|
||||
* The example only registers the plugin on desktop (`autostart`, `cli`,
|
||||
* `global-shortcut`, `positioner`, `updater`, `window-state`), so the whole
|
||||
* suite is skipped on mobile.
|
||||
*/
|
||||
desktopOnly?: boolean
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import http from 'node:http'
|
||||
import { WebSocketServer } from 'ws'
|
||||
|
||||
/**
|
||||
* 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 =
|
||||
'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 {
|
||||
close(): void
|
||||
}
|
||||
@@ -38,6 +46,10 @@ export interface FixtureServer {
|
||||
* - `GET /download` — {@link DOWNLOAD_FIXTURE_BODY} with a `Content-Length`.
|
||||
* - `* /echo` — a JSON description of the request (`method`, `url`, `headers`
|
||||
* 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> {
|
||||
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) => {
|
||||
server.once('error', reject)
|
||||
server.listen(FIXTURE_SERVER_PORT, '127.0.0.1', () => {
|
||||
server.off('error', reject)
|
||||
resolve({
|
||||
close: () => {
|
||||
for (const client of wss.clients) client.terminate()
|
||||
wss.close()
|
||||
server.closeAllConnections()
|
||||
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,
|
||||
tauriError,
|
||||
describePlugin,
|
||||
itDesktop
|
||||
itDesktop,
|
||||
isMobile
|
||||
} from '../helpers/index.js'
|
||||
|
||||
// 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.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 () => {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -276,7 +276,7 @@ describePlugin('fs', () => {
|
||||
await file.close()
|
||||
await file.stat()
|
||||
}, `${dir}/closed.txt`)
|
||||
expect(message.length).toBeGreaterThan(0)
|
||||
expect(message).toMatch(/resource id \d+ is invalid/)
|
||||
})
|
||||
|
||||
it('rejects paths outside the configured scope', async () => {
|
||||
@@ -339,8 +339,9 @@ describePlugin('fs', () => {
|
||||
unwatch()
|
||||
return events
|
||||
}, `${dir}/watched`)
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result.every((e) => typeof e.kind === 'string')).toBe(true)
|
||||
expect(
|
||||
result.some((e) => e.paths.some((p) => p.endsWith('touched.txt')))
|
||||
).toBe(true)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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
|
||||
// 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 () => {
|
||||
const message = await tauriError(async (api) => {
|
||||
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 () => {
|
||||
|
||||
@@ -144,6 +144,6 @@ describePlugin('http', () => {
|
||||
api.http.fetch(url, { proxy: { all: 'http://127.0.0.1:1' } }),
|
||||
echoServer
|
||||
)
|
||||
expect(message.length).toBeGreaterThan(0)
|
||||
expect(message).toMatch(/error sending request/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,15 +3,25 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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
|
||||
// (a notification daemon on Linux, the app's registration on Windows and
|
||||
// macOS), which the suite cannot observe. The specs cover the permission
|
||||
// model and that sending does not error out synchronously.
|
||||
// macOS), which the suite cannot observe, and sending is fire-and-forget, so
|
||||
// only the permission model is covered on desktop.
|
||||
//
|
||||
// The permission specs are desktop-only: mobile starts out ungranted and
|
||||
// `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', () => {
|
||||
itDesktop('permission is granted on desktop', async () => {
|
||||
@@ -30,29 +40,102 @@ describePlugin('notification', () => {
|
||||
expect(result).toEqual({ permission: 'granted', requested: 'granted' })
|
||||
})
|
||||
|
||||
it('sendNotification accepts a title string and an options object', async () => {
|
||||
const result = await tauri((api) => {
|
||||
api.notification.sendNotification('notification from e2e')
|
||||
api.notification.sendNotification({
|
||||
title: 'notification from e2e',
|
||||
body: 'with a body',
|
||||
sound: 'default'
|
||||
itOn(
|
||||
['android', 'ios'],
|
||||
'action types register, and cancel/remove leave nothing pending or active',
|
||||
async () => {
|
||||
const result = await tauri(async (api) => {
|
||||
await api.notification.registerActionTypes([
|
||||
{
|
||||
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 () => {
|
||||
const result = await tauri(() => {
|
||||
const notification = new window.Notification(
|
||||
'window.Notification from e2e',
|
||||
{
|
||||
body: 'created through the DOM API'
|
||||
}
|
||||
itOn('ios', 'channels are not implemented', async () => {
|
||||
for (const call of ['create', 'remove', 'list'] as const) {
|
||||
const error = await tauriError(
|
||||
(api, call) =>
|
||||
call === 'create'
|
||||
? api.notification.createChannel({ id: 'e2e', name: 'e2e' })
|
||||
: call === 'remove'
|
||||
? api.notification.removeChannel('e2e')
|
||||
: api.notification.channels(),
|
||||
call
|
||||
)
|
||||
return typeof notification === 'object'
|
||||
})
|
||||
expect(result).toBe(true)
|
||||
expect(error).toMatch(/not implemented/)
|
||||
}
|
||||
})
|
||||
|
||||
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'
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -41,6 +41,6 @@ describePlugin('opener', () => {
|
||||
await api.path.join(await api.path.appDataDir(), 'does-not-exist-e2e')
|
||||
)
|
||||
)
|
||||
expect(message.length).toBeGreaterThan(0)
|
||||
expect(message).toMatch(/os error 2/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -86,11 +86,6 @@ describePlugin('os', () => {
|
||||
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 () => {
|
||||
const locale = await tauri((api) => api.os.locale())
|
||||
// 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 () => {
|
||||
const hostname = await tauri((api) => api.os.hostname())
|
||||
// Windows can report the name in a different case than Node does.
|
||||
|
||||
@@ -31,6 +31,13 @@ const commonSurface: Surface<CommonPluginApi> = {
|
||||
'readImage',
|
||||
'writeImage'
|
||||
],
|
||||
deepLink: [
|
||||
'getCurrent',
|
||||
'register',
|
||||
'unregister',
|
||||
'isRegistered',
|
||||
'onOpenUrl'
|
||||
],
|
||||
dialog: ['open', 'save', 'message', 'ask', 'confirm'],
|
||||
fs: [
|
||||
'BaseDirectory',
|
||||
@@ -98,14 +105,26 @@ const commonSurface: Surface<CommonPluginApi> = {
|
||||
],
|
||||
process: ['exit', 'relaunch'],
|
||||
shell: ['Command', 'Child', 'EventEmitter', 'open'],
|
||||
// the `Database` class itself
|
||||
sql: ['load', 'get'],
|
||||
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. */
|
||||
const desktopSurface: Surface<DesktopPluginApi> = {
|
||||
autostart: ['enable', 'disable', 'isEnabled'],
|
||||
cli: ['getMatches'],
|
||||
globalShortcut: ['register', 'unregister', 'unregisterAll', 'isRegistered'],
|
||||
positioner: [
|
||||
'Position',
|
||||
'moveWindow',
|
||||
'moveWindowConstrained',
|
||||
'handleIconState'
|
||||
],
|
||||
updater: ['check', 'Update'],
|
||||
windowState: [
|
||||
'StateFlags',
|
||||
@@ -168,12 +187,12 @@ describe('plugin globals', () => {
|
||||
it('every plugin this platform registers exposes its API on window.__TAURI__', async () => {
|
||||
const missing = await tauri(
|
||||
(api, plugins) =>
|
||||
plugins.filter(
|
||||
(plugin) =>
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
typeof (api as unknown as Record<string, unknown>)[plugin]
|
||||
!== 'object'
|
||||
),
|
||||
plugins.filter((plugin) => {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
const global = (api as unknown as Record<string, unknown>)[plugin]
|
||||
// a module namespace, or the class a default-export-only module (sql, websocket) defines
|
||||
return typeof global !== 'object' && typeof global !== 'function'
|
||||
}),
|
||||
Object.keys(surface)
|
||||
)
|
||||
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
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -233,6 +233,6 @@ describePlugin('shell', () => {
|
||||
const message = await tauriError((api) =>
|
||||
api.shell.open('ftp://example.com')
|
||||
)
|
||||
expect(message.length).toBeGreaterThan(0)
|
||||
expect(message).toMatch(/failed regex validation/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -156,6 +156,6 @@ describePlugin('upload', () => {
|
||||
`${FIXTURE_SERVER_URL}/echo`,
|
||||
`${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/)
|
||||
})
|
||||
})
|
||||
@@ -164,7 +164,8 @@ export function mobileConfig(platform: MobilePlatform): WebdriverIO.Config {
|
||||
'--apk',
|
||||
'--target',
|
||||
androidTarget()
|
||||
]
|
||||
],
|
||||
ios ? {} : androidBuildEnv()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -282,11 +283,12 @@ for (const signal of [
|
||||
}
|
||||
|
||||
/** 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], {
|
||||
cwd: appDir,
|
||||
stdio: 'inherit',
|
||||
shell: true
|
||||
shell: true,
|
||||
env: { ...process.env, ...env }
|
||||
})
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
@@ -297,6 +299,39 @@ function tauriCli(args: string[]): void {
|
||||
|
||||
// --- 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`. */
|
||||
function adb(args: string[]): SpawnSyncReturns<string> {
|
||||
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:appPackage': appId,
|
||||
'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
|
||||
// otherwise. E2E_ANDROID_AVD instead boots that AVD.
|
||||
...(process.env.E2E_ANDROID_DEVICE
|
||||
|
||||
@@ -39,7 +39,8 @@ class BatchArgs {
|
||||
|
||||
@InvokeArg
|
||||
class CancelArgs {
|
||||
lateinit var notifications: List<Int>
|
||||
// `cancelAll()` sends no list: cancel every pending notification
|
||||
var notifications: List<Int>? = null
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
@@ -152,7 +153,9 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
|
||||
@Command
|
||||
fun cancel(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(CancelArgs::class.java)
|
||||
manager.cancel(args.notifications)
|
||||
manager.cancel(
|
||||
args.notifications ?: notificationStorage.getSavedNotificationIds().mapNotNull { it.toIntOrNull() }
|
||||
)
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
@@ -192,32 +195,33 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
@Command
|
||||
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) {
|
||||
val activeNotifications = notificationManager.activeNotifications
|
||||
for (activeNotification in activeNotifications) {
|
||||
val jsNotification = JSObject()
|
||||
jsNotification.put("id", activeNotification.id)
|
||||
jsNotification.put("tag", activeNotification.tag)
|
||||
val jsNotification = mutableMapOf<String, Any?>(
|
||||
"id" to activeNotification.id,
|
||||
"tag" to activeNotification.tag
|
||||
)
|
||||
val notification = activeNotification.notification
|
||||
if (notification != null) {
|
||||
jsNotification.put("title", notification.extras.getCharSequence(android.app.Notification.EXTRA_TITLE))
|
||||
jsNotification.put("body", notification.extras.getCharSequence(android.app.Notification.EXTRA_TEXT))
|
||||
jsNotification.put("group", notification.group)
|
||||
jsNotification.put(
|
||||
"groupSummary",
|
||||
jsNotification["title"] = notification.extras.getCharSequence(android.app.Notification.EXTRA_TITLE)?.toString()
|
||||
jsNotification["body"] = notification.extras.getCharSequence(android.app.Notification.EXTRA_TEXT)?.toString()
|
||||
jsNotification["group"] = notification.group
|
||||
jsNotification["groupSummary"] =
|
||||
0 != notification.flags and android.app.Notification.FLAG_GROUP_SUMMARY
|
||||
)
|
||||
val extras = JSObject()
|
||||
val extras = mutableMapOf<String, String?>()
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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__})}
|
||||
|
||||
@@ -8,6 +8,7 @@ const COMMANDS: &[&str] = &[
|
||||
"is_permission_granted",
|
||||
"register_action_types",
|
||||
"register_listener",
|
||||
"remove_listener",
|
||||
"cancel",
|
||||
"get_pending",
|
||||
"remove_active",
|
||||
|
||||
@@ -879,7 +879,7 @@ async function removeChannel(id: string): Promise<void> {
|
||||
* @since 2.0.0
|
||||
*/
|
||||
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 {
|
||||
let notifications: [Int]
|
||||
// `cancelAll()` sends no list: cancel every pending notification
|
||||
let notifications: [Int]?
|
||||
}
|
||||
|
||||
struct Action: Decodable {
|
||||
@@ -216,9 +217,13 @@ class NotificationPlugin: Plugin {
|
||||
@objc func cancel(_ invoke: Invoke) throws {
|
||||
let args = try invoke.parseArgs(CancelArgs.self)
|
||||
|
||||
UNUserNotificationCenter.current().removePendingNotificationRequests(
|
||||
withIdentifiers: args.notifications.map { String($0) }
|
||||
)
|
||||
if let notifications = args.notifications {
|
||||
UNUserNotificationCenter.current().removePendingNotificationRequests(
|
||||
withIdentifiers: notifications.map { String($0) }
|
||||
)
|
||||
} else {
|
||||
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
|
||||
}
|
||||
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-register-action-types`
|
||||
- `allow-register-listener`
|
||||
- `allow-remove-listener`
|
||||
- `allow-cancel`
|
||||
- `allow-get-pending`
|
||||
- `allow-remove-active`
|
||||
@@ -402,6 +403,32 @@ Denies the remove_active command without any pre-configured scope.
|
||||
<tr>
|
||||
<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`
|
||||
|
||||
</td>
|
||||
|
||||
@@ -16,6 +16,7 @@ permissions = [
|
||||
"allow-notify",
|
||||
"allow-register-action-types",
|
||||
"allow-register-listener",
|
||||
"allow-remove-listener",
|
||||
"allow-cancel",
|
||||
"allow-get-pending",
|
||||
"allow-remove-active",
|
||||
|
||||
@@ -462,6 +462,18 @@
|
||||
"const": "deny-remove-active",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@@ -487,10 +499,10 @@
|
||||
"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",
|
||||
"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`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -287,6 +287,8 @@ mod imp {
|
||||
use std::path::PathBuf;
|
||||
|
||||
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 {
|
||||
let mut urls = Vec::new();
|
||||
|
||||
|
||||
Generated
+63
-21
@@ -156,6 +156,9 @@ importers:
|
||||
'@tauri-apps/api':
|
||||
specifier: ^2.11.0
|
||||
version: 2.11.0
|
||||
'@tauri-apps/plugin-autostart':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/autostart
|
||||
'@tauri-apps/plugin-barcode-scanner':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/barcode-scanner
|
||||
@@ -168,6 +171,9 @@ importers:
|
||||
'@tauri-apps/plugin-clipboard-manager':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/clipboard-manager
|
||||
'@tauri-apps/plugin-deep-link':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/deep-link
|
||||
'@tauri-apps/plugin-dialog':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/dialog
|
||||
@@ -198,21 +204,33 @@ importers:
|
||||
'@tauri-apps/plugin-os':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/os
|
||||
'@tauri-apps/plugin-positioner':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/positioner
|
||||
'@tauri-apps/plugin-process':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/process
|
||||
'@tauri-apps/plugin-shell':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/shell
|
||||
'@tauri-apps/plugin-sql':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/sql
|
||||
'@tauri-apps/plugin-store':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/store
|
||||
'@tauri-apps/plugin-stronghold':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/stronghold
|
||||
'@tauri-apps/plugin-updater':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/updater
|
||||
'@tauri-apps/plugin-upload':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/upload
|
||||
'@tauri-apps/plugin-websocket':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/websocket
|
||||
'@zerodevx/svelte-json-view':
|
||||
specifier: 2.0.0
|
||||
version: 2.0.0(svelte@5.55.7(@typescript-eslint/types@8.70.0))
|
||||
@@ -248,11 +266,14 @@ importers:
|
||||
specifier: ^2.0.9
|
||||
version: 2.0.9
|
||||
'@crabnebula/test-runner-backend':
|
||||
specifier: ^0.2.9
|
||||
version: 0.2.9
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0
|
||||
'@tauri-apps/api':
|
||||
specifier: ^2.11.0
|
||||
version: 2.11.0
|
||||
'@tauri-apps/plugin-autostart':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/autostart
|
||||
'@tauri-apps/plugin-barcode-scanner':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/barcode-scanner
|
||||
@@ -265,6 +286,9 @@ importers:
|
||||
'@tauri-apps/plugin-clipboard-manager':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/clipboard-manager
|
||||
'@tauri-apps/plugin-deep-link':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/deep-link
|
||||
'@tauri-apps/plugin-dialog':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/dialog
|
||||
@@ -298,27 +322,42 @@ importers:
|
||||
'@tauri-apps/plugin-os':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/os
|
||||
'@tauri-apps/plugin-positioner':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/positioner
|
||||
'@tauri-apps/plugin-process':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/process
|
||||
'@tauri-apps/plugin-shell':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/shell
|
||||
'@tauri-apps/plugin-sql':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/sql
|
||||
'@tauri-apps/plugin-store':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/store
|
||||
'@tauri-apps/plugin-stronghold':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/stronghold
|
||||
'@tauri-apps/plugin-updater':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/updater
|
||||
'@tauri-apps/plugin-upload':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/upload
|
||||
'@tauri-apps/plugin-websocket':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/websocket
|
||||
'@tauri-apps/plugin-window-state':
|
||||
specifier: workspace:*
|
||||
version: link:../../plugins/window-state
|
||||
'@types/node':
|
||||
specifier: ^22.10.0
|
||||
version: 22.20.3
|
||||
'@types/ws':
|
||||
specifier: ^8.18.1
|
||||
version: 8.18.1
|
||||
'@wdio/appium-service':
|
||||
specifier: ^9.31.9
|
||||
version: 9.32.0(supports-color@8.1.1)
|
||||
@@ -355,6 +394,9 @@ importers:
|
||||
webdriverio:
|
||||
specifier: ^9.31.9
|
||||
version: 9.32.0(supports-color@8.1.1)
|
||||
ws:
|
||||
specifier: ^8.18.0
|
||||
version: 8.21.3
|
||||
|
||||
plugins/autostart:
|
||||
dependencies:
|
||||
@@ -657,33 +699,33 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
hasBin: true
|
||||
|
||||
'@crabnebula/test-runner-backend-darwin-arm64@0.2.9':
|
||||
resolution: {integrity: sha512-UAlL8VA23y/fp+MigNYFwGvtE9vJ518f846c0k2vgT/bLIpt+d3UXndjZCMYbEVFTmaq00wHoRPSPbyzHEMWlQ==}
|
||||
'@crabnebula/test-runner-backend-darwin-arm64@0.3.0':
|
||||
resolution: {integrity: sha512-XnZdSeew2ew/Eix+z1BwKt5PcrJSjL+VQ7dlrV1fvRj4WUTQtluzE1VhiVV0j2EIFJsRH2IExNj5OdCWxKSPiQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@crabnebula/test-runner-backend-darwin-x64@0.2.9':
|
||||
resolution: {integrity: sha512-RUP9L4Lk8KsT4A64U4QFVy5rj99Y6iIichPLpZgTvpRybYKZBVXjUiU31aGUIlWnd4VHsWKLM2vNl90rKZm3lA==}
|
||||
'@crabnebula/test-runner-backend-darwin-x64@0.3.0':
|
||||
resolution: {integrity: sha512-dpogk8TxKd2CZb8BZ2Ni1mpbogD8KSoB8J92aw+9R24S8xBeTu8I+t4w9PVNIBvFaNAhEQelYhFffxH0FDs5+Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@crabnebula/test-runner-backend-linux-x64-gnu@0.2.9':
|
||||
resolution: {integrity: sha512-js/haLE9OUsa0oDHNuVps5DY7av3iWG7PpAKjr9nMnIEXiLACDmvyUL/IplHMmu1lr6S0fztoudRf4JfTR+mfA==}
|
||||
'@crabnebula/test-runner-backend-linux-x64-gnu@0.3.0':
|
||||
resolution: {integrity: sha512-ZzDVKdY1ydv+lqfRYs6PAdmIc8AlfOciH3ntuUxyRJ8aSq0v8y+pow6b3/CRqmumkj9riVWsUriJr7Xw2YZFTg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@crabnebula/test-runner-backend-win32-x64-msvc@0.2.9':
|
||||
resolution: {integrity: sha512-p4pc/GQ0bj3vVLuUyTHDnhaXO52EQNkaCUPhWXwEkG00loW93pvCBRrOWoYpJH9me/n2zMFv61meW9mQyvbByw==}
|
||||
'@crabnebula/test-runner-backend-win32-x64-msvc@0.3.0':
|
||||
resolution: {integrity: sha512-xzE5bZx6QpqCkGFTpOYGxMfYidfsvYQgp3yQ1hXwuxeC5vsabOngRtE6X2DxQU6U8JzzNaooN0afG/khzoaihQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@crabnebula/test-runner-backend@0.2.9':
|
||||
resolution: {integrity: sha512-3z6qZbJtVyA1JymQU5hFqcxG6nFD5INgqBc3OxTbYXVzMpihlRGOcHWyPK72ndHK88YmfwQWPcEFN1R/H/Lvhw==}
|
||||
'@crabnebula/test-runner-backend@0.3.0':
|
||||
resolution: {integrity: sha512-72aldFCPV0Sflv8KGLeaNoUY4Vz8eRl7AvUsJXFwqJ7KKNDAfPKwovXjmZoqIWn6Icv01sEsd1b2w0O66Y3WEQ==}
|
||||
engines: {node: '>= 10'}
|
||||
hasBin: true
|
||||
|
||||
@@ -4911,24 +4953,24 @@ snapshots:
|
||||
'@crabnebula/tauri-driver-linux-x64-gnu': 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
|
||||
|
||||
'@crabnebula/test-runner-backend-darwin-x64@0.2.9':
|
||||
'@crabnebula/test-runner-backend-darwin-x64@0.3.0':
|
||||
optional: true
|
||||
|
||||
'@crabnebula/test-runner-backend-linux-x64-gnu@0.2.9':
|
||||
'@crabnebula/test-runner-backend-linux-x64-gnu@0.3.0':
|
||||
optional: true
|
||||
|
||||
'@crabnebula/test-runner-backend-win32-x64-msvc@0.2.9':
|
||||
'@crabnebula/test-runner-backend-win32-x64-msvc@0.3.0':
|
||||
optional: true
|
||||
|
||||
'@crabnebula/test-runner-backend@0.2.9':
|
||||
'@crabnebula/test-runner-backend@0.3.0':
|
||||
optionalDependencies:
|
||||
'@crabnebula/test-runner-backend-darwin-arm64': 0.2.9
|
||||
'@crabnebula/test-runner-backend-darwin-x64': 0.2.9
|
||||
'@crabnebula/test-runner-backend-linux-x64-gnu': 0.2.9
|
||||
'@crabnebula/test-runner-backend-win32-x64-msvc': 0.2.9
|
||||
'@crabnebula/test-runner-backend-darwin-arm64': 0.3.0
|
||||
'@crabnebula/test-runner-backend-darwin-x64': 0.3.0
|
||||
'@crabnebula/test-runner-backend-linux-x64-gnu': 0.3.0
|
||||
'@crabnebula/test-runner-backend-win32-x64-msvc': 0.3.0
|
||||
|
||||
'@dabh/diagnostics@2.0.9':
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user