Skip to main content

tauri_plugin_webdriver_automation/
lib.rs

1// tauri-plugin-webdriver-automation: Tauri plugin enabling WebDriver-based e2e testing.
2//
3// This plugin runs an HTTP server inside the Tauri app (debug builds only) that
4// allows an external WebDriver server to interact with the webview: find elements,
5// click buttons, read text, manage windows, and execute JavaScript.
6
7use std::collections::HashMap;
8use std::sync::Mutex;
9
10use tauri::{Manager, Runtime, State};
11
12mod server;
13
14// --- Tauri IPC command: receives script results from the JS bridge ---
15
16#[tauri::command]
17async fn resolve<R: Runtime>(
18    _app: tauri::AppHandle<R>,
19    webdriver: State<'_, WebDriverState>,
20    id: String,
21    result: Option<serde_json::Value>,
22) -> Result<(), ()> {
23    webdriver
24        .pending_scripts
25        .lock()
26        .expect("failed to lock pending scripts")
27        .remove(&id)
28        .expect("no pending script with that id")
29        .send(result.unwrap_or_default())
30        .expect("failed to send script result");
31    Ok(())
32}
33
34// --- Internal types ---
35
36pub(crate) struct WebDriverState {
37    pub pending_scripts: Mutex<HashMap<String, tokio::sync::oneshot::Sender<serde_json::Value>>>,
38}
39
40// --- Plugin entry point ---
41
42pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
43    let (webview_created_tx, webview_created_rx) = tokio::sync::broadcast::channel(16);
44
45    tauri::plugin::Builder::new("webdriver-automation")
46        .invoke_handler(tauri::generate_handler![resolve])
47        .js_init_script(include_str!("init.js").to_string())
48        .on_webview_ready(move |webview| {
49            webview_created_tx
50                .send(
51                    webview
52                        .get_webview_window(webview.label())
53                        .unwrap_or_else(|| {
54                            panic!("failed to get webview window for label {}", webview.label())
55                        }),
56                )
57                .unwrap_or_default();
58        })
59        .setup(move |app, _api| {
60            app.manage(WebDriverState {
61                pending_scripts: Mutex::new(HashMap::new()),
62            });
63
64            app.add_capability(
65                tauri::ipc::CapabilityBuilder::new("webdriver-automation")
66                    .local(true)
67                    .window("*")
68                    .remote("http://*".into())
69                    .remote("https://*".into())
70                    .permission("webdriver-automation:default"),
71            )?;
72
73            // Start the HTTP server that the external WebDriver CLI connects to.
74            let app_handle = app.clone();
75            let rx = webview_created_rx.resubscribe();
76            tauri::async_runtime::spawn(async move {
77                server::start(app_handle, rx).await;
78            });
79
80            Ok(())
81        })
82        .build()
83}
84
85// --- Helper: resolve a window by label ---
86
87pub(crate) fn window_by_label<R: Runtime>(
88    app: &tauri::AppHandle<R>,
89    label: Option<&str>,
90) -> Option<tauri::WebviewWindow<R>> {
91    if let Some(label) = label {
92        app.get_webview_window(label)
93    } else {
94        app.get_webview_window("main")
95            .or_else(|| app.webview_windows().into_values().next())
96    }
97}
98