-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcommands.rs
More file actions
267 lines (241 loc) · 10.6 KB
/
commands.rs
File metadata and controls
267 lines (241 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
use tauri::{command, Manager, Runtime, WebviewWindow, Listener};
use serde_json::Value as JsonValue;
use uuid::Uuid;
use crate::models::ExecuteRequest;
use crate::Result;
use crate::Error;
/// Window state information for generic window management
/// Mirrors Electron's window tracking - discover active window without app-specific knowledge
#[derive(serde::Serialize, Debug, Clone)]
pub struct WindowState {
pub label: String,
pub title: String,
pub is_visible: bool,
pub is_focused: bool,
}
/// Debug command to verify plugin is working
#[command]
pub(crate) async fn debug_plugin<R: Runtime>(_window: WebviewWindow<R>) -> String {
eprintln!("[WDIO-Rust] DEBUG PLUGIN CALLED!");
"Plugin alive".to_string()
}
/// Log a frontend message to stderr (for standalone mode capture)
/// This bypasses the event system and writes directly to stderr
#[command]
pub(crate) async fn log_frontend<R: Runtime>(
_window: WebviewWindow<R>,
message: String,
level: String,
) -> Result<String> {
// Output with a special marker that the log parser recognizes as frontend
// Format: [WDIO-FRONTEND][LEVEL] message
eprintln!("[WDIO-FRONTEND][{}] {}", level.to_uppercase(), message);
// Return success indicator
Ok(format!("logged: {} @ {}", level, message))
}
/// Execute JavaScript code in the frontend context
/// This command is called via invoke from the frontend plugin
/// It extracts the script from the request, evaluates it, and returns the result
#[command]
pub(crate) async fn execute<R: Runtime>(
app: tauri::AppHandle<R>,
window: WebviewWindow<R>,
request: ExecuteRequest,
) -> Result<JsonValue> {
log::debug!("Execute command called");
log::trace!("Script length: {} chars", request.script.len());
use std::sync::{Arc, Mutex};
use std::time::Duration;
// Use tokio's async oneshot channel for async waiting
// Wrap sender in Arc<Mutex<Option>> so the Fn closure can take it once
let (tx, rx) = tokio::sync::oneshot::channel();
let tx = Arc::new(Mutex::new(Some(tx)));
// Build the script with args if offered
let script = if !request.args.is_empty() {
let args_json = serde_json::to_string(&request.args)
.map_err(|e| crate::Error::SerializationError(format!("Failed to serialize args: {}", e)))?;
let script_json = serde_json::to_string(&request.script)
.map_err(|e| crate::Error::SerializationError(format!("Failed to serialize script: {}", e)))?;
format!("(function() {{ const __wdio_args = {}; return ({}); }})()", args_json, script_json)
} else {
request.script.clone()
};
// Generate unique event ID for this execution
let event_id = format!("wdio-result-{}", Uuid::new_v4());
log::trace!("Generated event_id for result: {}", event_id);
// Listen for the result event using the app's event listener
// The JavaScript uses window.__TAURI__.event.emit() which emits to the APP target
// So we need to listen on the app target, not the window target
let tx_clone = Arc::clone(&tx);
let listener_id = app.listen(&event_id, move |event| {
log::trace!("Received result event payload: {}", event.payload());
// Take the sender from the Option (only the first call will succeed)
let tx = match tx_clone.lock().ok().and_then(|mut guard| guard.take()) {
Some(tx) => tx,
None => {
log::warn!("Event received but sender already taken, ignoring");
return;
}
};
if let Ok(payload) = serde_json::from_str::<serde_json::Value>(event.payload()) {
if let Some(success) = payload.get("success").and_then(|s| s.as_bool()) {
if success {
let value: JsonValue = payload.get("value").unwrap_or(&JsonValue::Null).clone();
let _ = tx.send(Ok(value));
} else {
let error_msg = payload.get("error")
.and_then(|e| e.as_str())
.unwrap_or("Unknown error")
.to_string();
let _ = tx.send(Err(crate::Error::ExecuteError(error_msg)));
}
}
}
});
// Wrap the script to:
// 1. Wait for window.__TAURI__.core.invoke to be available (handles race condition)
// 2. Execute the user's script
// 3. Emit the result via a Tauri event using the current window's emit
let script_with_result = format!(
r#"
(async () => {{
try {{
// Wait for window.__TAURI__.core.invoke to be available
const maxWait = 5000;
const startTime = Date.now();
while (!window.__TAURI__?.core?.invoke && (Date.now() - startTime) < maxWait) {{
await new Promise(r => setTimeout(r, 10));
}}
if (!window.__TAURI__?.core?.invoke) {{
throw new Error('window.__TAURI__.core.invoke not available after timeout');
}}
// Execute the user's script
const result = await ({});
// Emit the result using the current window's event emitter
// This ensures the event goes to the same window where we're listening
if (window.__TAURI__?.event?.emit) {{
await window.__TAURI__.event.emit('{}', {{ success: true, value: result }});
}} else {{
// Fallback: try dynamic import
try {{
const {{ emit }} = await import('@tauri-apps/api/event');
await emit('{}', {{ success: true, value: result }});
}} catch (importError) {{
console.error('[WDIO Execute] Failed to import emit:', importError);
// Last resort: try to use the globalTauri emit
if (typeof window.__TAURI__ !== 'undefined') {{
const {{ emit }} = await import('@tauri-apps/api/event');
await emit('{}', {{ success: true, value: result }});
}}
}}
}}
}} catch (error) {{
// Emit error via event
try {{
if (window.__TAURI__?.event?.emit) {{
await window.__TAURI__.event.emit('{}', {{ success: false, error: error.message || String(error) }});
}} else {{
const {{ emit }} = await import('@tauri-apps/api/event');
await emit('{}', {{ success: false, error: error.message || String(error) }});
}}
}} catch (emitError) {{
console.error('[WDIO Execute] Failed to emit error:', emitError);
}}
}}
}})();
"#,
script, event_id, event_id, event_id, event_id, event_id
);
log::trace!("Executing script via window.eval()");
// Evaluate the script
if let Err(e) = window.eval(&script_with_result) {
log::error!("Failed to eval script: {}", e);
app.unlisten(listener_id);
return Err(crate::Error::ExecuteError(format!("Failed to eval script: {}", e)));
}
log::trace!("Waiting for execute result (30s timeout)");
// Wait for the result event with 30s timeout using async
// This allows the async runtime to process other tasks (like IPC) while waiting
// This matches the WebDriver default script timeout
let window_label = window.label().to_owned();
let timeout_duration = Duration::from_secs(30);
match tokio::time::timeout(timeout_duration, rx).await {
Ok(Ok(Ok(result))) => {
log::debug!("Execute completed successfully");
log::trace!("Result: {:?}", result);
app.unlisten(listener_id);
Ok(result)
}
Ok(Ok(Err(e))) => {
log::error!("JS error during execution: {}", e);
app.unlisten(listener_id);
Err(e)
}
Ok(Err(_)) => {
// Channel closed without sending (shouldn't happen)
log::error!("Channel closed unexpectedly. Event ID: {}. Window: {}", event_id, window_label);
app.unlisten(listener_id);
Err(crate::Error::ExecuteError(format!(
"Channel closed unexpectedly. Event ID: {}. Window: {}",
event_id, window_label
)))
}
Err(_) => {
log::error!("Timeout waiting for execute result after 30s. Event ID: {}. Window: {}",
event_id, window_label);
app.unlisten(listener_id);
Err(crate::Error::ExecuteError(format!(
"Script execution timed out after 30s. Event ID: {}. Window: {}",
event_id, window_label
)))
}
}
}
/// Get the label of the currently focused/active window
/// Note: Tauri 2.x doesn't expose window focus state, so this returns
/// the "main" window if it exists, or the first window in lexicographic order
#[command]
pub(crate) async fn get_active_window_label<R: Runtime>(
app: tauri::AppHandle<R>,
) -> Result<String> {
let windows = app.webview_windows();
if windows.is_empty() {
return Err(Error::WindowError("No windows available".to_string()));
}
// Return the "main" window if it exists for predictable behavior
if let Some(main) = windows.get("main") {
return Ok(main.label().to_string());
}
// Otherwise, return the first window in lexicographic order for consistency
let mut labels: Vec<_> = windows.keys().collect();
labels.sort();
let first_label = labels.first()
.ok_or_else(|| Error::WindowError("No windows available".to_string()))?;
Ok(first_label.to_string())
}
/// List all window labels in the application
#[command]
pub(crate) async fn list_windows<R: Runtime>(
app: tauri::AppHandle<R>,
) -> Result<Vec<String>> {
Ok(app.webview_windows().keys().cloned().collect())
}
/// Get detailed state of all windows (for generic window management like Electron)
#[command]
pub(crate) async fn get_window_states<R: Runtime>(
app: tauri::AppHandle<R>,
) -> Result<Vec<WindowState>> {
let mut states = Vec::new();
for (label, window) in app.webview_windows() {
let state = WindowState {
label: label.clone(),
title: window.title().unwrap_or_default(),
is_visible: window.is_visible().unwrap_or(false),
is_focused: window.is_focused().unwrap_or(false),
};
log::debug!("[get_window_states] {}: title='{}', visible={}, focused={}",
label, state.title, state.is_visible, state.is_focused);
states.push(state);
}
Ok(states)
}