Skip to main content

tauri_plugin_playwright/
lib.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3use tokio::sync::Mutex;
4
5use tauri::{
6    plugin::{Builder, TauriPlugin},
7    Runtime,
8};
9
10mod commands;
11mod native_capture;
12mod server;
13
14use server::PendingResults;
15
16fn js_init_script() -> String {
17    // The init script injects a <script> tag into the DOM.
18    // The injected script uses RELATIVE URLs (/pw-poll, /pw) which go
19    // through the Vite dev server proxy (same origin, no CORS/mixed-content).
20    let mut js = String::new();
21    js.push_str("(function() {\n");
22    js.push_str("  function inject() {\n");
23    js.push_str("    if (document.getElementById('__pw_script__')) return;\n");
24    js.push_str("    var s = document.createElement('script');\n");
25    js.push_str("    s.id = '__pw_script__';\n");
26    js.push_str("    s.textContent = '");
27    // Poll script using relative URLs — no port needed
28    let poll_script = concat!(
29        "(function() {",
30        "  if (window.__PW_ACTIVE__) return;",
31        "  window.__PW_ACTIVE__ = true;",
32        "  async function poll() {",
33        "    while (window.__PW_ACTIVE__) {",
34        "      try {",
35        "        var resp = await fetch(\"/pw-poll\");",
36        "        if (resp.status === 200) {",
37        "          var cmd = await resp.json();",
38        "          if (cmd && cmd.id && cmd.script) {",
39        "            try {",
40        "              var fn = new Function(\"return (async function() { return (\" + cmd.script + \"); })()\");",
41        "              var result = await fn();",
42        "              var body = JSON.stringify({ id: cmd.id, result: JSON.stringify({ ok: true, v: result }) });",
43        "              await fetch(\"/pw\", { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: body });",
44        "            } catch(e) {",
45        "              var body = JSON.stringify({ id: cmd.id, result: JSON.stringify({ ok: false, e: (e && e.message) || String(e) }) });",
46        "              await fetch(\"/pw\", { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: body }).catch(function(){});",
47        "            }",
48        "          }",
49        "        }",
50        "      } catch(e) {}",
51        "      await new Promise(function(r) { setTimeout(r, 16); });",
52        "    }",
53        "  }",
54        "  poll();",
55        "  console.log(\"[tauri-plugin-playwright] bridge active\");",
56        "})();"
57    );
58    // Escape for JS string literal (single quotes wrapping)
59    let escaped = poll_script
60        .replace('\\', "\\\\")
61        .replace('\'', "\\'");
62    js.push_str(&escaped);
63    js.push_str("';\n");
64    js.push_str("    document.head.appendChild(s);\n");
65    js.push_str("  }\n");
66    js.push_str("  if (document.head) { inject(); }\n");
67    js.push_str("  else { document.addEventListener('DOMContentLoaded', inject); }\n");
68    js.push_str("  new MutationObserver(function() { if (document.head && !document.getElementById('__pw_script__')) inject(); }).observe(document, { childList: true, subtree: true });\n");
69    js.push_str("})();\n");
70    js
71}
72
73pub fn init<R: Runtime>() -> TauriPlugin<R> {
74    init_with_config(PluginConfig::default())
75}
76
77pub fn init_with_config<R: Runtime>(config: PluginConfig) -> TauriPlugin<R> {
78    let pending: PendingResults = Arc::new(Mutex::new(HashMap::new()));
79    let pending_for_setup = Arc::clone(&pending);
80
81    Builder::new("playwright")
82        .js_init_script(js_init_script())
83        .setup(move |app, _api| {
84            server::start(
85                app.clone(),
86                Arc::clone(&pending_for_setup),
87                config.socket_path.clone(),
88                config.tcp_port,
89            );
90            Ok(())
91        })
92        .build()
93}
94
95#[derive(Debug, Clone)]
96pub struct PluginConfig {
97    pub socket_path: Option<String>,
98    pub tcp_port: Option<u16>,
99}
100
101impl Default for PluginConfig {
102    fn default() -> Self {
103        Self {
104            socket_path: Some("/tmp/tauri-playwright.sock".to_string()),
105            tcp_port: None,
106        }
107    }
108}
109
110impl PluginConfig {
111    pub fn new() -> Self { Self::default() }
112    pub fn socket_path(mut self, path: impl Into<String>) -> Self { self.socket_path = Some(path.into()); self }
113    pub fn tcp_port(mut self, port: u16) -> Self { self.tcp_port = Some(port); self }
114}