chore(testing): add Tauri integration tests and E2E test infrastructure (#3483)
Set up WebDriver-based testing for the Tauri app with two tiers: - Vitest browser-mode tests (*.tauri.test.ts) running inside the Tauri WebView for plugin IPC testing (libsql, smoke tests) - WDIO E2E tests (*.e2e.ts) for UI-level interaction testing Key changes: - Add webdriver Cargo feature gating tauri-plugin-webdriver - Add runtime capability for remote URLs (webdriver builds only) - Add vitest.tauri.config.mts and wdio.conf.ts connecting to embedded WebDriver server on port 4445 - Add shared tauri-invoke helper for IPC from Vitest iframe context - Add testing documentation in docs/testing.md
This commit is contained in:
Generated
+284
-69
@@ -52,6 +52,7 @@ dependencies = [
|
||||
"tauri-plugin-sign-in-with-apple",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-webdriver",
|
||||
"tauri-plugin-websocket",
|
||||
"tauri-plugin-window-state",
|
||||
"thiserror 2.0.18",
|
||||
@@ -411,7 +412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b"
|
||||
dependencies = [
|
||||
"atk-sys",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -421,10 +422,10 @@ version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -446,7 +447,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"axum-core 0.3.4",
|
||||
"bitflags 1.3.2",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
@@ -454,7 +455,7 @@ dependencies = [
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.32",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"matchit 0.7.3",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
@@ -467,6 +468,39 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
|
||||
dependencies = [
|
||||
"axum-core 0.5.6",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.8.1",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit 0.8.4",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper 1.0.2",
|
||||
"tokio",
|
||||
"tower 0.5.3",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.3.4"
|
||||
@@ -484,6 +518,25 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper 1.0.2",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
@@ -752,7 +805,7 @@ checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"cairo-sys-rs",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"thiserror 1.0.69",
|
||||
@@ -764,9 +817,9 @@ version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -863,7 +916,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
"target-lexicon 0.12.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-expr"
|
||||
version = "0.20.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78cef5b5a1a6827c7322ae2a636368a573006b27cfa76c7ebd53e834daeaab6a"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
"target-lexicon 0.13.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2059,7 +2122,7 @@ dependencies = [
|
||||
"gdk-pixbuf",
|
||||
"gdk-sys",
|
||||
"gio",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"libc",
|
||||
"pango",
|
||||
]
|
||||
@@ -2072,7 +2135,7 @@ checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
|
||||
dependencies = [
|
||||
"gdk-pixbuf-sys",
|
||||
"gio",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"libc",
|
||||
"once_cell",
|
||||
]
|
||||
@@ -2083,11 +2146,11 @@ version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
|
||||
dependencies = [
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gio-sys 0.18.1",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2098,13 +2161,13 @@ checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7"
|
||||
dependencies = [
|
||||
"cairo-sys-rs",
|
||||
"gdk-pixbuf-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gio-sys 0.18.1",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"pkg-config",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2114,11 +2177,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69"
|
||||
dependencies = [
|
||||
"gdk-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2130,7 +2193,7 @@ dependencies = [
|
||||
"gdk",
|
||||
"gdkx11-sys",
|
||||
"gio",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"libc",
|
||||
"x11",
|
||||
]
|
||||
@@ -2142,9 +2205,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d"
|
||||
dependencies = [
|
||||
"gdk-sys",
|
||||
"glib-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
"x11",
|
||||
]
|
||||
|
||||
@@ -2252,8 +2315,8 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"gio-sys",
|
||||
"glib",
|
||||
"gio-sys 0.18.1",
|
||||
"glib 0.18.5",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
@@ -2267,13 +2330,26 @@ version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio-sys"
|
||||
version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22"
|
||||
dependencies = [
|
||||
"glib-sys 0.21.5",
|
||||
"gobject-sys 0.21.5",
|
||||
"libc",
|
||||
"system-deps 7.0.7",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib"
|
||||
version = "0.18.5"
|
||||
@@ -2286,10 +2362,10 @@ dependencies = [
|
||||
"futures-executor",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
"gio-sys",
|
||||
"glib-macros",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gio-sys 0.18.1",
|
||||
"glib-macros 0.18.5",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
@@ -2297,6 +2373,27 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib"
|
||||
version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
"gio-sys 0.21.5",
|
||||
"glib-macros 0.21.5",
|
||||
"glib-sys 0.21.5",
|
||||
"gobject-sys 0.21.5",
|
||||
"libc",
|
||||
"memchr",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-macros"
|
||||
version = "0.18.5"
|
||||
@@ -2311,6 +2408,19 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-macros"
|
||||
version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-sys"
|
||||
version = "0.18.1"
|
||||
@@ -2318,7 +2428,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-sys"
|
||||
version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"system-deps 7.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2333,9 +2453,20 @@ version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gobject-sys"
|
||||
version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294"
|
||||
dependencies = [
|
||||
"glib-sys 0.21.5",
|
||||
"libc",
|
||||
"system-deps 7.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2351,7 +2482,7 @@ dependencies = [
|
||||
"gdk",
|
||||
"gdk-pixbuf",
|
||||
"gio",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"gtk-sys",
|
||||
"gtk3-macros",
|
||||
"libc",
|
||||
@@ -2369,12 +2500,12 @@ dependencies = [
|
||||
"cairo-sys-rs",
|
||||
"gdk-pixbuf-sys",
|
||||
"gdk-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gio-sys 0.18.1",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2633,6 +2764,7 @@ dependencies = [
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
@@ -2995,7 +3127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"javascriptcore-rs-sys",
|
||||
]
|
||||
|
||||
@@ -3005,10 +3137,10 @@ version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3123,7 +3255,7 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a"
|
||||
dependencies = [
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"gtk",
|
||||
"gtk-sys",
|
||||
"libappindicator-sys",
|
||||
@@ -3454,6 +3586,12 @@ version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "memalloc"
|
||||
version = "0.1.0"
|
||||
@@ -3969,6 +4107,16 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-javascript-core"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-osa-kit"
|
||||
version = "0.3.2"
|
||||
@@ -3999,6 +4147,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
@@ -4046,6 +4195,8 @@ dependencies = [
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"objc2-javascript-core",
|
||||
"objc2-security",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4220,7 +4371,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
|
||||
dependencies = [
|
||||
"gio",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"pango-sys",
|
||||
@@ -4232,10 +4383,10 @@ version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5257,8 +5408,8 @@ checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"gtk-sys",
|
||||
"js-sys",
|
||||
"log",
|
||||
@@ -5760,6 +5911,17 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.20"
|
||||
@@ -6046,7 +6208,7 @@ checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"gio",
|
||||
"glib",
|
||||
"glib 0.18.5",
|
||||
"libc",
|
||||
"soup3-sys",
|
||||
]
|
||||
@@ -6057,11 +6219,11 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27"
|
||||
dependencies = [
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gio-sys 0.18.1",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6259,13 +6421,26 @@ version = "6.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
|
||||
dependencies = [
|
||||
"cfg-expr",
|
||||
"cfg-expr 0.15.8",
|
||||
"heck 0.5.0",
|
||||
"pkg-config",
|
||||
"toml 0.8.2",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "7.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f"
|
||||
dependencies = [
|
||||
"cfg-expr 0.20.6",
|
||||
"heck 0.5.0",
|
||||
"pkg-config",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.34.6"
|
||||
@@ -6338,6 +6513,12 @@ version = "0.12.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.10.3"
|
||||
@@ -6852,6 +7033,39 @@ dependencies = [
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-webdriver"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a130e5cc896b52a87d618b53e1ba025954af7c004b8781a4ddee7298f7b3749a"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum 0.8.8",
|
||||
"base64 0.22.1",
|
||||
"block2",
|
||||
"cairo-rs",
|
||||
"glib 0.21.5",
|
||||
"gtk",
|
||||
"javascriptcore-rs",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"objc2-web-kit",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid 1.21.0",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-websocket"
|
||||
version = "2.4.2"
|
||||
@@ -7341,7 +7555,7 @@ checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum 0.6.20",
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"h2 0.3.27",
|
||||
@@ -7413,6 +7627,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7995,10 +8210,10 @@ dependencies = [
|
||||
"gdk",
|
||||
"gdk-sys",
|
||||
"gio",
|
||||
"gio-sys",
|
||||
"glib",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gio-sys 0.18.1",
|
||||
"glib 0.18.5",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"gtk",
|
||||
"gtk-sys",
|
||||
"javascriptcore-rs",
|
||||
@@ -8017,15 +8232,15 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cairo-sys-rs",
|
||||
"gdk-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gio-sys 0.18.1",
|
||||
"glib-sys 0.18.1",
|
||||
"gobject-sys 0.18.0",
|
||||
"gtk-sys",
|
||||
"javascriptcore-rs-sys",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"soup3-sys",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -13,10 +13,13 @@ pnpm tauri dev # Desktop dev with Tauri (compiles Rust backend)
|
||||
pnpm build # Build Next.js for Tauri
|
||||
pnpm build-web # Build Next.js for web deployment
|
||||
|
||||
# Testing
|
||||
pnpm test # Run all tests (vitest + jsdom)
|
||||
# Testing (see [docs/testing.md](docs/testing.md) for full details)
|
||||
pnpm test # Unit tests (vitest + jsdom)
|
||||
pnpm test -- src/__tests__/utils/misc.test.ts # Run a single test file
|
||||
pnpm test -- --watch # Watch mode
|
||||
pnpm test:browser # Browser tests (Chromium via Playwright)
|
||||
pnpm tauri:dev:test # Start Tauri app with webdriver
|
||||
pnpm test:tauri # Run Tauri integration tests
|
||||
|
||||
# Linting & Formatting
|
||||
pnpm lint # ESLint
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# Testing
|
||||
|
||||
Readest uses three test tiers, all powered by [Vitest](https://vitest.dev/).
|
||||
|
||||
## Unit Tests (`pnpm test`)
|
||||
|
||||
Runs tests in a **jsdom** environment. No browser or Tauri runtime required.
|
||||
|
||||
```bash
|
||||
pnpm test # Run all unit tests
|
||||
pnpm test -- src/__tests__/utils/misc.test.ts # Run a single file
|
||||
pnpm test -- --watch # Watch mode
|
||||
```
|
||||
|
||||
- **Config:** `vitest.config.mts`
|
||||
- **Pattern:** `src/**/*.test.ts` (excludes `*.browser.test.ts` and `*.tauri.test.ts`)
|
||||
- **Environment:** jsdom
|
||||
- **Use for:** Pure logic, utilities, services that don't need real browser APIs or Tauri IPC.
|
||||
|
||||
## Browser Tests (`pnpm test:browser`)
|
||||
|
||||
Runs tests in a **real Chromium** browser via Playwright. Required for code that depends on Web Workers, SharedArrayBuffer, OPFS, or other browser-only APIs.
|
||||
|
||||
```bash
|
||||
pnpm test:browser
|
||||
```
|
||||
|
||||
- **Config:** `vitest.browser.config.mts`
|
||||
- **Pattern:** `src/**/*.browser.test.ts`
|
||||
- **Browser:** Chromium (headless, via `@vitest/browser-playwright`)
|
||||
- **Use for:** WASM modules (e.g. `@tursodatabase/database-wasm`), Web Worker integration, browser-specific storage APIs.
|
||||
|
||||
## Tauri Integration Tests (`pnpm test:tauri`)
|
||||
|
||||
Runs Vitest tests **inside the Tauri WebView**, with access to Tauri IPC and native plugin commands. Tests execute in the actual app environment.
|
||||
|
||||
### Step 1: Start the Tauri App
|
||||
|
||||
In one terminal, start the app with the `webdriver` feature enabled:
|
||||
|
||||
```bash
|
||||
pnpm tauri:dev:test # Dev mode (uses tauri dev server, faster iteration)
|
||||
pnpm tauri:build:test # Debug release build (closer to production)
|
||||
```
|
||||
|
||||
These commands compile the Rust backend with `--features webdriver`, which:
|
||||
|
||||
- Includes `tauri-plugin-webdriver` (embeds a W3C WebDriver server on port 4445)
|
||||
- Adds a runtime capability granting plugin permissions to remote URLs (`http://127.0.0.1:*`), so Vitest's browser-mode iframe can call Tauri IPC
|
||||
|
||||
Keep this running while you run tests.
|
||||
|
||||
### Step 2: Run Tests
|
||||
|
||||
In another terminal:
|
||||
|
||||
```bash
|
||||
pnpm test:tauri
|
||||
```
|
||||
|
||||
Vitest connects directly to the embedded WebDriver server (port 4445) in the running Tauri app and executes tests inside its WebView.
|
||||
|
||||
- **Config:** `vitest.tauri.config.mts`
|
||||
- **Pattern:** `src/**/*.tauri.test.ts`
|
||||
- **Browser provider:** `@vitest/browser-webdriverio` (connects to port 4445)
|
||||
- **Use for:** Tauri plugin commands (libsql, native-tts, etc.), native filesystem, Tauri IPC.
|
||||
|
||||
### Writing Tauri Tests
|
||||
|
||||
Tests access Tauri IPC via a shared helper:
|
||||
|
||||
```typescript
|
||||
import { invoke } from '../tauri/tauri-invoke';
|
||||
|
||||
it('calls a plugin command', async () => {
|
||||
const result = await invoke('plugin:libsql|load', { options: { path: 'sqlite::memory:' } });
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
```
|
||||
|
||||
The `invoke()` helper accesses `window.top.__TAURI_INTERNALS__` (Vitest runs in an iframe, Tauri injects IPC into the main frame).
|
||||
|
||||
**Limitations:** Only custom invoke commands and plugin commands listed in the webdriver capability work. Standard Tauri JS APIs (e.g. `@tauri-apps/api`) that rely on `URL: local` may not work from the Vitest iframe.
|
||||
|
||||
## E2E Tests (WDIO)
|
||||
|
||||
Full end-to-end tests using WebDriverIO, for UI-level testing against the running Tauri app. Same two-step workflow as Tauri integration tests.
|
||||
|
||||
```bash
|
||||
# Terminal 1: start the app (same as for Tauri integration tests)
|
||||
pnpm tauri:dev:test
|
||||
|
||||
# Terminal 2: run E2E tests
|
||||
pnpm test:e2e
|
||||
```
|
||||
|
||||
- **Config:** `wdio.conf.ts`
|
||||
- **Pattern:** `e2e/**/*.e2e.ts`
|
||||
- **Framework:** Mocha (via `@wdio/mocha-framework`)
|
||||
- **Connects to:** port 4445 (embedded WebDriver server)
|
||||
- **Use for:** UI interaction tests, window management, navigation flows.
|
||||
|
||||
## Test File Naming
|
||||
|
||||
| Suffix | Runner | Environment |
|
||||
| ------------------- | ------------------- | --------------------- |
|
||||
| `*.test.ts` | `pnpm test` | jsdom |
|
||||
| `*.browser.test.ts` | `pnpm test:browser` | Chromium (Playwright) |
|
||||
| `*.tauri.test.ts` | `pnpm test:tauri` | Tauri WebView |
|
||||
| `*.e2e.ts` | `pnpm test:e2e` | Tauri app (WDIO) |
|
||||
@@ -0,0 +1,123 @@
|
||||
describe('Readest App Launch', () => {
|
||||
it('should have a visible body element', async () => {
|
||||
const body = await $('body');
|
||||
await body.waitForDisplayed({ timeout: 10000 });
|
||||
expect(await body.isDisplayed()).toBe(true);
|
||||
});
|
||||
|
||||
it('should have the correct window handle', async () => {
|
||||
const handle = await browser.getWindowHandle();
|
||||
expect(handle).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return the page source', async () => {
|
||||
const source = await browser.getPageSource();
|
||||
expect(source).toContain('html');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Library Page', () => {
|
||||
it('should navigate to the library page', async () => {
|
||||
const url = await browser.getUrl();
|
||||
expect(url).toMatch(/library|localhost/);
|
||||
});
|
||||
|
||||
it('should display the library container', async () => {
|
||||
const library = await $('[aria-label="Your Library"]');
|
||||
await library.waitForExist({ timeout: 15000 });
|
||||
expect(await library.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should display the library header', async () => {
|
||||
const header = await $('[aria-label="Library Header"]');
|
||||
await header.waitForExist({ timeout: 10000 });
|
||||
expect(await header.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should display the bookshelf area', async () => {
|
||||
const bookshelf = await $('[aria-label="Bookshelf"]');
|
||||
await bookshelf.waitForExist({ timeout: 10000 });
|
||||
expect(await bookshelf.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should have a search input', async () => {
|
||||
const searchInput = await $('.search-input');
|
||||
await searchInput.waitForExist({ timeout: 10000 });
|
||||
expect(await searchInput.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow typing in the search input', async () => {
|
||||
const searchInput = await $('.search-input');
|
||||
await searchInput.waitForDisplayed({ timeout: 10000 });
|
||||
await searchInput.setValue('test search');
|
||||
const value = await searchInput.getValue();
|
||||
expect(value).toBe('test search');
|
||||
});
|
||||
|
||||
it('should show the clear search button after typing', async () => {
|
||||
const clearBtn = await $('[aria-label="Clear Search"]');
|
||||
await clearBtn.waitForExist({ timeout: 5000 });
|
||||
expect(await clearBtn.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should clear the search input when clear button is clicked', async () => {
|
||||
const clearBtn = await $('[aria-label="Clear Search"]');
|
||||
await clearBtn.click();
|
||||
const searchInput = await $('.search-input');
|
||||
const value = await searchInput.getValue();
|
||||
expect(value).toBe('');
|
||||
});
|
||||
|
||||
it('should have a select books button', async () => {
|
||||
const selectBtn = await $('[aria-label="Select Books"]');
|
||||
await selectBtn.waitForExist({ timeout: 10000 });
|
||||
expect(await selectBtn.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should have an import books button', async () => {
|
||||
const importBtn = await $('[aria-label="Import Books"]');
|
||||
await importBtn.waitForExist({ timeout: 10000 });
|
||||
expect(await importBtn.isExisting()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Window Management', () => {
|
||||
it('should return the window size', async () => {
|
||||
const size = await browser.getWindowSize();
|
||||
expect(size.width).toBeGreaterThan(0);
|
||||
expect(size.height).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('JavaScript Execution', () => {
|
||||
it('should execute JavaScript in the app context', async () => {
|
||||
const result = await browser.execute(() => {
|
||||
return document.readyState;
|
||||
});
|
||||
expect(result).toBe('complete');
|
||||
});
|
||||
|
||||
it('should access the document title via JS', async () => {
|
||||
const title = await browser.execute(() => {
|
||||
return document.title;
|
||||
});
|
||||
expect(title).toContain('Readest');
|
||||
});
|
||||
|
||||
it('should detect the app platform globals', async () => {
|
||||
const hasCLIAccess = await browser.execute(() => {
|
||||
return (window as unknown as Record<string, unknown>).__READEST_CLI_ACCESS === true;
|
||||
});
|
||||
expect(hasCLIAccess).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should navigate back to library after visiting another route', async () => {
|
||||
const currentUrl = await browser.getUrl();
|
||||
await browser.url(currentUrl.replace(/\/[^/]*$/, '/library'));
|
||||
const library = await $('[aria-label="Your Library"]');
|
||||
await library.waitForExist({ timeout: 15000 });
|
||||
expect(await library.isExisting()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["mocha", "@wdio/globals/types"]
|
||||
},
|
||||
"include": ["./**/*.ts"]
|
||||
}
|
||||
@@ -14,6 +14,10 @@
|
||||
"lint": "eslint .",
|
||||
"test": "dotenv -e .env -e .env.test.local vitest",
|
||||
"test:browser": "vitest --config vitest.browser.config.mts --watch=false",
|
||||
"test:tauri": "vitest --config vitest.tauri.config.mts --watch=false",
|
||||
"test:e2e": "wdio run wdio.conf.ts",
|
||||
"tauri:dev:test": "dotenv -e .env.tauri -- tauri dev --features webdriver",
|
||||
"tauri:build:test": "dotenv -e .env.tauri -- tauri build --debug --features webdriver",
|
||||
"tauri": "tauri",
|
||||
"clippy": "cargo clippy -p Readest --no-deps -- -D warnings",
|
||||
"format": "pnpm -w format",
|
||||
@@ -170,6 +174,7 @@
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/cssbeautify": "^0.3.5",
|
||||
"@types/lunr": "^2.3.7",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/nunjucks": "^3.2.6",
|
||||
"@types/react": "^19.0.0",
|
||||
@@ -184,6 +189,13 @@
|
||||
"@typescript-eslint/parser": "^8.48.0",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"@vitest/browser-webdriverio": "^4.0.18",
|
||||
"@wdio/cli": "^9.24.0",
|
||||
"@wdio/globals": "^9.23.0",
|
||||
"@wdio/local-runner": "^9.24.0",
|
||||
"@wdio/mocha-framework": "^9.24.0",
|
||||
"@wdio/spec-reporter": "^9.24.0",
|
||||
"@wdio/types": "^9.24.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"caniuse-lite": "^1.0.30001746",
|
||||
"cpx2": "^8.0.0",
|
||||
|
||||
@@ -17,6 +17,8 @@ crate-type = ["staticlib", "cdylib", "lib"]
|
||||
[features]
|
||||
# Internal feature to suppress warnings from old objc crate
|
||||
cargo-clippy = []
|
||||
# Enable WebDriver plugin for E2E testing (use with `tauri build --debug --features webdriver`)
|
||||
webdriver = ["tauri-plugin-webdriver"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
@@ -57,6 +59,7 @@ tauri-plugin-websocket = "2"
|
||||
tauri-plugin-sharekit = "0.3"
|
||||
tauri-plugin-device-info = "1.0.1"
|
||||
tauri-plugin-libsql = { path = "./plugins/tauri-plugin-libsql" }
|
||||
tauri-plugin-webdriver = { version = "0.2", optional = true }
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
rand = "0.8"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"identifier": "webdriver-testing",
|
||||
"description": "Grants plugin permissions to remote URLs for Vitest browser-mode tests. Only loaded at runtime when the webdriver feature is enabled.",
|
||||
"remote": {
|
||||
"urls": ["http://127.0.0.1:*", "http://localhost:*"]
|
||||
},
|
||||
"local": false,
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"fs:default",
|
||||
"os:default",
|
||||
"dialog:default",
|
||||
"core:window:default",
|
||||
"core:path:allow-resolve-directory",
|
||||
"log:default",
|
||||
"shell:default",
|
||||
"process:default",
|
||||
"libsql:default",
|
||||
"native-tts:default",
|
||||
"native-bridge:default"
|
||||
]
|
||||
}
|
||||
@@ -228,8 +228,19 @@ pub fn run() {
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
let builder = builder.plugin(tauri_plugin_haptics::init());
|
||||
|
||||
#[cfg(feature = "webdriver")]
|
||||
let builder = builder.plugin(tauri_plugin_webdriver::init());
|
||||
|
||||
builder
|
||||
.setup(|#[allow(unused_variables)] app| {
|
||||
// When running with the webdriver feature (E2E/integration tests),
|
||||
// grant all default permissions to remote URLs (http://127.0.0.1:*)
|
||||
// so that Vitest browser-mode tests can call plugin commands.
|
||||
#[cfg(feature = "webdriver")]
|
||||
{
|
||||
use tauri::Manager;
|
||||
app.add_capability(include_str!("../capabilities-extra/webdriver.json"))?;
|
||||
}
|
||||
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
|
||||
{
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { invoke } from '../tauri/tauri-invoke';
|
||||
|
||||
/**
|
||||
* Integration tests for tauri-plugin-libsql running inside the Tauri WebView.
|
||||
* These call plugin IPC commands directly via __TAURI_INTERNALS__.invoke().
|
||||
*/
|
||||
describe('libsql plugin (native Tauri)', () => {
|
||||
const DB_PATH = 'sqlite::memory:';
|
||||
let dbPath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
dbPath = (await invoke('plugin:libsql|load', {
|
||||
options: { path: DB_PATH },
|
||||
})) as string;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await invoke('plugin:libsql|close', { db: dbPath });
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connection lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('loads an in-memory database and returns a path', () => {
|
||||
expect(typeof dbPath).toBe('string');
|
||||
expect(dbPath.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schema & basic operations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('creates a table and inserts a row', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)',
|
||||
values: [],
|
||||
});
|
||||
|
||||
const result = (await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['apple'],
|
||||
})) as { rowsAffected: number; lastInsertId: number };
|
||||
|
||||
expect(result.rowsAffected).toBe(1);
|
||||
expect(result.lastInsertId).toBe(1);
|
||||
});
|
||||
|
||||
it('inserts multiple rows with auto-incrementing ids', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)',
|
||||
values: [],
|
||||
});
|
||||
|
||||
const r1 = (await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['a'],
|
||||
})) as { lastInsertId: number };
|
||||
|
||||
const r2 = (await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['b'],
|
||||
})) as { lastInsertId: number };
|
||||
|
||||
const r3 = (await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['c'],
|
||||
})) as { lastInsertId: number };
|
||||
|
||||
expect(r1.lastInsertId).toBe(1);
|
||||
expect(r2.lastInsertId).toBe(2);
|
||||
expect(r3.lastInsertId).toBe(3);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SELECT queries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('select returns typed rows', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, qty INTEGER)',
|
||||
values: [],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name, qty) VALUES (?, ?)',
|
||||
values: ['apple', 10],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name, qty) VALUES (?, ?)',
|
||||
values: ['banana', 20],
|
||||
});
|
||||
|
||||
const rows = (await invoke('plugin:libsql|select', {
|
||||
db: dbPath,
|
||||
query: 'SELECT * FROM items ORDER BY id',
|
||||
values: [],
|
||||
})) as Array<{ id: number; name: string; qty: number }>;
|
||||
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows[0]!.name).toBe('apple');
|
||||
expect(rows[0]!.qty).toBe(10);
|
||||
expect(rows[1]!.name).toBe('banana');
|
||||
expect(rows[1]!.qty).toBe(20);
|
||||
});
|
||||
|
||||
it('select with WHERE and params', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)',
|
||||
values: [],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['apple'],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['banana'],
|
||||
});
|
||||
|
||||
const rows = (await invoke('plugin:libsql|select', {
|
||||
db: dbPath,
|
||||
query: 'SELECT * FROM items WHERE name = ?',
|
||||
values: ['banana'],
|
||||
})) as Array<{ id: number; name: string }>;
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]!.name).toBe('banana');
|
||||
});
|
||||
|
||||
it('select returns empty array for no matching rows', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)',
|
||||
values: [],
|
||||
});
|
||||
|
||||
const rows = (await invoke('plugin:libsql|select', {
|
||||
db: dbPath,
|
||||
query: 'SELECT * FROM items',
|
||||
values: [],
|
||||
})) as unknown[];
|
||||
|
||||
expect(rows).toEqual([]);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UPDATE & DELETE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('UPDATE returns rowsAffected', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)',
|
||||
values: [],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['old'],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['old'],
|
||||
});
|
||||
|
||||
const result = (await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'UPDATE items SET name = ? WHERE name = ?',
|
||||
values: ['new', 'old'],
|
||||
})) as { rowsAffected: number };
|
||||
|
||||
expect(result.rowsAffected).toBe(2);
|
||||
});
|
||||
|
||||
it('DELETE returns rowsAffected', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)',
|
||||
values: [],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['a'],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['b'],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO items (name) VALUES (?)',
|
||||
values: ['c'],
|
||||
});
|
||||
|
||||
const result = (await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'DELETE FROM items WHERE name IN (?, ?)',
|
||||
values: ['a', 'c'],
|
||||
})) as { rowsAffected: number };
|
||||
|
||||
expect(result.rowsAffected).toBe(2);
|
||||
|
||||
const remaining = (await invoke('plugin:libsql|select', {
|
||||
db: dbPath,
|
||||
query: 'SELECT name FROM items',
|
||||
values: [],
|
||||
})) as Array<{ name: string }>;
|
||||
|
||||
expect(remaining).toHaveLength(1);
|
||||
expect(remaining[0]!.name).toBe('b');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Batch execution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('batch executes multiple statements atomically', async () => {
|
||||
await invoke('plugin:libsql|batch', {
|
||||
db: dbPath,
|
||||
queries: [
|
||||
'CREATE TABLE t1 (id INTEGER PRIMARY KEY, val TEXT)',
|
||||
"INSERT INTO t1 (val) VALUES ('one')",
|
||||
"INSERT INTO t1 (val) VALUES ('two')",
|
||||
"INSERT INTO t1 (val) VALUES ('three')",
|
||||
],
|
||||
});
|
||||
|
||||
const rows = (await invoke('plugin:libsql|select', {
|
||||
db: dbPath,
|
||||
query: 'SELECT val FROM t1 ORDER BY id',
|
||||
values: [],
|
||||
})) as Array<{ val: string }>;
|
||||
|
||||
expect(rows).toHaveLength(3);
|
||||
expect(rows.map((r) => r.val)).toEqual(['one', 'two', 'three']);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('handles NULL values correctly', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)',
|
||||
values: [],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO t (val) VALUES (?)',
|
||||
values: [null],
|
||||
});
|
||||
|
||||
const rows = (await invoke('plugin:libsql|select', {
|
||||
db: dbPath,
|
||||
query: 'SELECT * FROM t',
|
||||
values: [],
|
||||
})) as Array<{ id: number; val: string | null }>;
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]!.val).toBeNull();
|
||||
});
|
||||
|
||||
it('handles integer and real types', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE nums (i INTEGER, r REAL)',
|
||||
values: [],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO nums (i, r) VALUES (?, ?)',
|
||||
values: [42, 3.14],
|
||||
});
|
||||
|
||||
const rows = (await invoke('plugin:libsql|select', {
|
||||
db: dbPath,
|
||||
query: 'SELECT * FROM nums',
|
||||
values: [],
|
||||
})) as Array<{ i: number; r: number }>;
|
||||
|
||||
expect(rows[0]!.i).toBe(42);
|
||||
expect(rows[0]!.r).toBeCloseTo(3.14);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('throws on invalid SQL', async () => {
|
||||
await expect(
|
||||
invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INVALID SQL STATEMENT',
|
||||
values: [],
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('throws on constraint violation', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT UNIQUE)',
|
||||
values: [],
|
||||
});
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO t (val) VALUES (?)',
|
||||
values: ['unique_val'],
|
||||
});
|
||||
|
||||
await expect(
|
||||
invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO t (val) VALUES (?)',
|
||||
values: ['unique_val'],
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Result contract
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('execute result always has rowsAffected and lastInsertId as numbers', async () => {
|
||||
await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'CREATE TABLE t (id INTEGER PRIMARY KEY)',
|
||||
values: [],
|
||||
});
|
||||
|
||||
const result = (await invoke('plugin:libsql|execute', {
|
||||
db: dbPath,
|
||||
query: 'INSERT INTO t DEFAULT VALUES',
|
||||
values: [],
|
||||
})) as { rowsAffected: number; lastInsertId: number };
|
||||
|
||||
expect(typeof result.rowsAffected).toBe('number');
|
||||
expect(typeof result.lastInsertId).toBe('number');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('get_config returns encryption status', async () => {
|
||||
const config = (await invoke('plugin:libsql|get_config')) as { encrypted: boolean };
|
||||
expect(typeof config.encrypted).toBe('boolean');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getTauri, invoke } from './tauri-invoke';
|
||||
|
||||
describe('Tauri Smoke Tests', () => {
|
||||
it('should have __TAURI_INTERNALS__ available via window.top', () => {
|
||||
const tauri = getTauri();
|
||||
expect(tauri).toBeDefined();
|
||||
expect(typeof tauri.invoke).toBe('function');
|
||||
});
|
||||
|
||||
it('should invoke get_executable_dir', async () => {
|
||||
const execDir = (await invoke('get_executable_dir')) as string;
|
||||
expect(typeof execDir).toBe('string');
|
||||
expect(execDir.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should invoke get_environment_variable for HOME', async () => {
|
||||
const home = (await invoke('get_environment_variable', { name: 'HOME' })) as string;
|
||||
expect(typeof home).toBe('string');
|
||||
expect(home.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should return empty string for non-existent env var', async () => {
|
||||
const result = await invoke('get_environment_variable', {
|
||||
name: '__TAURI_SMOKE_TEST_NONEXISTENT__',
|
||||
});
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should invoke get_environment_variable for PATH', async () => {
|
||||
const pathVar = (await invoke('get_environment_variable', { name: 'PATH' })) as string;
|
||||
expect(typeof pathVar).toBe('string');
|
||||
expect(pathVar).toContain('/');
|
||||
});
|
||||
|
||||
it('should get executable dir that contains the app name', async () => {
|
||||
const execDir = (await invoke('get_executable_dir')) as string;
|
||||
expect(execDir.toLowerCase()).toMatch(/readest|target/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
interface TauriInternals {
|
||||
invoke(cmd: string, args?: Record<string, unknown>): Promise<unknown>;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
__TAURI_INTERNALS__: TauriInternals;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Vitest runs tests inside an iframe. Tauri injects __TAURI_INTERNALS__
|
||||
// into the main frame only, so we access it via window.top.
|
||||
export function getTauri(): TauriInternals {
|
||||
const top = window.top ?? window;
|
||||
const tauri = (top as unknown as Record<string, unknown>)['__TAURI_INTERNALS__'] as
|
||||
| TauriInternals
|
||||
| undefined;
|
||||
if (!tauri) {
|
||||
throw new Error(
|
||||
'__TAURI_INTERNALS__ not found. Are tests running inside a Tauri WebView? ' +
|
||||
'(checked both window and window.top)',
|
||||
);
|
||||
}
|
||||
return tauri;
|
||||
}
|
||||
|
||||
export function invoke(cmd: string, args?: Record<string, unknown>): Promise<unknown> {
|
||||
return getTauri().invoke(cmd, args);
|
||||
}
|
||||
@@ -7,6 +7,6 @@ export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./vitest.setup.ts'],
|
||||
exclude: ['**/node_modules/**', '**/dist/**', '**/*.browser.test.ts'],
|
||||
exclude: ['**/node_modules/**', '**/dist/**', '**/*.browser.test.ts', '**/*.tauri.test.ts'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { webdriverio } from '@vitest/browser-webdriverio';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tsconfigPaths()],
|
||||
resolve: {
|
||||
conditions: ['development'],
|
||||
},
|
||||
test: {
|
||||
include: ['src/**/*.tauri.test.ts'],
|
||||
testTimeout: 30000,
|
||||
browser: {
|
||||
enabled: true,
|
||||
provider: webdriverio({
|
||||
hostname: '127.0.0.1',
|
||||
port: 4445,
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
} as WebdriverIO.Capabilities,
|
||||
}),
|
||||
instances: [{ browser: 'chrome' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Options } from '@wdio/types';
|
||||
|
||||
export const config: Options.Testrunner & { capabilities: unknown } = {
|
||||
runner: 'local',
|
||||
specs: ['./e2e/**/*.e2e.ts'],
|
||||
maxInstances: 1,
|
||||
capabilities: [
|
||||
{
|
||||
browserName: 'chrome',
|
||||
} as WebdriverIO.Capabilities,
|
||||
],
|
||||
logLevel: 'error',
|
||||
bail: 0,
|
||||
waitforTimeout: 10000,
|
||||
connectionRetryTimeout: 120000,
|
||||
connectionRetryCount: 3,
|
||||
hostname: '127.0.0.1',
|
||||
port: 4445,
|
||||
framework: 'mocha',
|
||||
reporters: ['spec'],
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
timeout: 60000,
|
||||
},
|
||||
};
|
||||
Generated
+2541
-59
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user