1#[cfg(not(target_arch = "wasm32"))]
2use fission_test_driver::TestCommand;
3#[cfg(not(target_arch = "wasm32"))]
4use fission_test_driver::TestEvent;
5use fission_test_driver::TestResponse;
6#[cfg(not(target_arch = "wasm32"))]
7use std::collections::VecDeque;
8#[cfg(not(target_arch = "wasm32"))]
9use std::io::{Read, Write};
10#[cfg(not(target_arch = "wasm32"))]
11use std::net::{TcpListener, TcpStream};
12use std::sync::mpsc;
13#[cfg(not(target_arch = "wasm32"))]
14use std::sync::{Arc, Mutex};
15#[cfg(not(target_arch = "wasm32"))]
16use winit::event_loop::EventLoopProxy;
17
18pub type ResponseSender = fission_test_driver::TestResponseSender;
20pub type ResponseReceiver = mpsc::Receiver<TestResponse>;
22#[cfg(not(target_arch = "wasm32"))]
24pub type PendingEventQueue = Arc<Mutex<VecDeque<TestEvent>>>;
25
26#[cfg(not(target_arch = "wasm32"))]
27#[derive(Clone)]
28pub enum EventInjector {
29 Proxy(EventLoopProxy<TestEvent>),
30 Queue {
31 queue: PendingEventQueue,
32 wake_proxy: Option<EventLoopProxy<TestEvent>>,
33 },
34}
35
36#[cfg(not(target_arch = "wasm32"))]
37pub fn create_pending_event_queue() -> PendingEventQueue {
38 Arc::new(Mutex::new(VecDeque::new()))
39}
40
41#[cfg(not(target_arch = "wasm32"))]
43pub fn spawn_server(port: u16, injector: EventInjector) -> std::thread::JoinHandle<()> {
44 std::thread::spawn(move || {
45 let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
46 .unwrap_or_else(|e| panic!("failed to bind test control port {}: {}", port, e));
47 eprintln!("[fission-test-control] listening on port {}", port);
48
49 for stream in listener.incoming() {
50 match stream {
51 Ok(stream) => handle_connection(stream, &injector),
52 Err(e) => eprintln!("[fission-test-control] accept error: {}", e),
53 }
54 }
55 })
56}
57
58#[cfg(not(target_arch = "wasm32"))]
59fn handle_connection(mut stream: TcpStream, injector: &EventInjector) {
60 let mut buf = Vec::new();
61 let mut tmp = [0u8; 4096];
62
63 loop {
64 match stream.read(&mut tmp) {
65 Ok(0) => return,
66 Ok(n) => {
67 buf.extend_from_slice(&tmp[..n]);
68 if buf.windows(4).any(|w| w == b"\r\n\r\n") {
69 break;
70 }
71 }
72 Err(_) => return,
73 }
74 }
75
76 let request = String::from_utf8_lossy(&buf);
77 let first_line = request.lines().next().unwrap_or("");
78 let parts: Vec<&str> = first_line.split_whitespace().collect();
79 let method = parts.first().copied().unwrap_or("");
80 let path = parts.get(1).copied().unwrap_or("");
81
82 if path == "/health" {
83 send_http_response(&mut stream, 200, r#"{"status":"ok"}"#);
84 return;
85 }
86
87 if method != "POST" || path != "/cmd" {
88 send_http_response(
89 &mut stream,
90 404,
91 r#"{"status":"Error","message":"not found"}"#,
92 );
93 return;
94 }
95
96 let content_length = request
97 .lines()
98 .find(|line| line.to_lowercase().starts_with("content-length:"))
99 .and_then(|line| line.split(':').nth(1))
100 .and_then(|value| value.trim().parse::<usize>().ok())
101 .unwrap_or(0);
102
103 let header_end = buf
104 .windows(4)
105 .position(|w| w == b"\r\n\r\n")
106 .map(|pos| pos + 4)
107 .unwrap_or(buf.len());
108
109 let mut body = buf[header_end..].to_vec();
110 while body.len() < content_length {
111 match stream.read(&mut tmp) {
112 Ok(0) => break,
113 Ok(n) => body.extend_from_slice(&tmp[..n]),
114 Err(_) => break,
115 }
116 }
117
118 let body_str = String::from_utf8_lossy(&body);
119 let cmd: TestCommand = match serde_json::from_str(&body_str) {
120 Ok(cmd) => cmd,
121 Err(error) => {
122 let resp = TestResponse::Error {
123 message: format!("parse error: {}", error),
124 };
125 send_http_response(&mut stream, 400, &serde_json::to_string(&resp).unwrap());
126 return;
127 }
128 };
129
130 let response = dispatch_command(cmd, injector);
131 send_http_response(&mut stream, 200, &serde_json::to_string(&response).unwrap());
132}
133
134#[cfg(not(target_arch = "wasm32"))]
135fn dispatch_command(cmd: TestCommand, injector: &EventInjector) -> TestResponse {
136 match cmd {
137 TestCommand::Tap { x, y } => {
138 inject_event(injector, TestEvent::MouseMove { x, y });
139 inject_event(injector, TestEvent::MouseDown { x, y, button: 0 });
140 inject_event(injector, TestEvent::MouseUp { x, y, button: 0 });
141 TestResponse::Ok {}
142 }
143 TestCommand::Drag {
144 start_x,
145 start_y,
146 end_x,
147 end_y,
148 steps,
149 } => {
150 let steps = steps.max(1);
151 inject_event(
152 injector,
153 TestEvent::MouseMove {
154 x: start_x,
155 y: start_y,
156 },
157 );
158 inject_event(
159 injector,
160 TestEvent::MouseDown {
161 x: start_x,
162 y: start_y,
163 button: 0,
164 },
165 );
166 for step in 1..=steps {
167 let t = step as f32 / steps as f32;
168 let x = start_x + (end_x - start_x) * t;
169 let y = start_y + (end_y - start_y) * t;
170 inject_event(injector, TestEvent::MouseMove { x, y });
171 }
172 inject_event(
173 injector,
174 TestEvent::MouseUp {
175 x: end_x,
176 y: end_y,
177 button: 0,
178 },
179 );
180 TestResponse::Ok {}
181 }
182 TestCommand::TapText { text } => query_event(injector, |response_tx| TestEvent::TapText {
183 text,
184 response_tx,
185 }),
186 TestCommand::ResolveSelector { query } => query_event(injector, |response_tx| {
187 TestEvent::ResolveSelector { query, response_tx }
188 }),
189 TestCommand::ScrollIntoView { query } => query_event(injector, |response_tx| {
190 TestEvent::ScrollIntoView { query, response_tx }
191 }),
192 TestCommand::TapSelector { query } => {
193 auto_scroll_then_query(injector, query, |query, response_tx| {
194 TestEvent::TapSelector { query, response_tx }
195 })
196 }
197 TestCommand::ActivateSelector { query } => {
198 auto_scroll_then_query(injector, query, |query, response_tx| {
199 TestEvent::ActivateSelector { query, response_tx }
200 })
201 }
202 TestCommand::FocusSelector { query } => {
203 auto_scroll_then_query(injector, query, |query, response_tx| {
204 TestEvent::FocusSelector { query, response_tx }
205 })
206 }
207 TestCommand::HoverSelector { query } => {
208 auto_scroll_then_query(injector, query, |query, response_tx| {
209 TestEvent::HoverSelector { query, response_tx }
210 })
211 }
212 TestCommand::RightClickSelector { query } => {
213 auto_scroll_then_query(injector, query, |query, response_tx| {
214 TestEvent::RightClickSelector { query, response_tx }
215 })
216 }
217 TestCommand::FillText { query, text } => {
218 auto_scroll_then_query(injector, query, |query, response_tx| TestEvent::FillText {
219 query,
220 text,
221 response_tx,
222 })
223 }
224 TestCommand::ClearText { query } => {
225 auto_scroll_then_query(injector, query, |query, response_tx| TestEvent::ClearText {
226 query,
227 response_tx,
228 })
229 }
230 TestCommand::Toggle { query } => {
231 auto_scroll_then_query(injector, query, |query, response_tx| TestEvent::Toggle {
232 query,
233 response_tx,
234 })
235 }
236 TestCommand::SelectOption { query } => {
237 auto_scroll_then_query(injector, query, |query, response_tx| {
238 TestEvent::SelectOption { query, response_tx }
239 })
240 }
241 TestCommand::Scroll { x, y, dx, dy } => {
242 inject_event(injector, TestEvent::Scroll { x, y, dx, dy });
243 TestResponse::Ok {}
244 }
245 TestCommand::ExternalFileHover { x, y, paths } => {
246 inject_event(injector, TestEvent::ExternalFileHover { x, y, paths });
247 TestResponse::Ok {}
248 }
249 TestCommand::ExternalFileDrop { x, y, paths } => {
250 inject_event(injector, TestEvent::ExternalFileDrop { x, y, paths });
251 TestResponse::Ok {}
252 }
253 TestCommand::ExternalFileCancel {} => {
254 inject_event(injector, TestEvent::ExternalFileCancel);
255 TestResponse::Ok {}
256 }
257 TestCommand::TypeText { text } => {
258 inject_event(injector, TestEvent::TextInput { text });
259 TestResponse::Ok {}
260 }
261 TestCommand::ImePreedit {
262 text,
263 cursor_start,
264 cursor_end,
265 } => {
266 let cursor = match (cursor_start, cursor_end) {
267 (Some(start), Some(end)) => Some((start, end)),
268 _ => None,
269 };
270 inject_event(injector, TestEvent::ImePreedit { text, cursor });
271 TestResponse::Ok {}
272 }
273 TestCommand::ImeCommit { text } => {
274 inject_event(injector, TestEvent::ImeCommit { text });
275 TestResponse::Ok {}
276 }
277 TestCommand::ImeCancel {} => {
278 inject_event(injector, TestEvent::ImeCancel);
279 TestResponse::Ok {}
280 }
281 TestCommand::PressKey { key, modifiers } => {
282 inject_event(
283 injector,
284 TestEvent::KeyDown {
285 key_code: key.clone(),
286 modifiers,
287 },
288 );
289 inject_event(
290 injector,
291 TestEvent::KeyUp {
292 key_code: key,
293 modifiers,
294 },
295 );
296 TestResponse::Ok {}
297 }
298 TestCommand::Screenshot { path } => query_event(injector, |response_tx| {
299 TestEvent::Screenshot { path, response_tx }
300 }),
301 TestCommand::CaptureScreenshot {} => query_event(injector, |response_tx| {
302 TestEvent::CaptureScreenshot { response_tx }
303 }),
304 TestCommand::GetText {} => {
305 query_event(injector, |response_tx| TestEvent::GetText { response_tx })
306 }
307 TestCommand::GetTree {} => {
308 query_event(injector, |response_tx| TestEvent::GetTree { response_tx })
309 }
310 TestCommand::Wait { ms } => {
311 std::thread::sleep(std::time::Duration::from_millis(ms));
312 TestResponse::Ok {}
313 }
314 TestCommand::WaitForSelector { query, timeout_ms } => {
315 wait_for_selector_state(injector, query, timeout_ms, SelectorWaitCondition::Present)
316 }
317 TestCommand::WaitForVisible { query, timeout_ms } => {
318 wait_for_selector_state(injector, query, timeout_ms, SelectorWaitCondition::Visible)
319 }
320 TestCommand::WaitForEnabled { query, timeout_ms } => {
321 wait_for_selector_state(injector, query, timeout_ms, SelectorWaitCondition::Enabled)
322 }
323 TestCommand::WaitForDisabled { query, timeout_ms } => {
324 wait_for_selector_state(injector, query, timeout_ms, SelectorWaitCondition::Disabled)
325 }
326 TestCommand::WaitForValue {
327 query,
328 value,
329 timeout_ms,
330 } => wait_for_selector_state(
331 injector,
332 query,
333 timeout_ms,
334 SelectorWaitCondition::Value(value),
335 ),
336 TestCommand::WaitForText { text, timeout_ms } => wait_for_text(injector, text, timeout_ms),
337 TestCommand::WaitForGone { query, timeout_ms } => {
338 wait_for_selector_state(injector, query, timeout_ms, SelectorWaitCondition::Gone)
339 }
340 TestCommand::Pump {} => {
341 query_event(injector, |response_tx| TestEvent::Pump { response_tx })
342 }
343 TestCommand::Quit {} => {
344 inject_event(injector, TestEvent::Quit);
345 TestResponse::Ok {}
346 }
347 TestCommand::SimulateMouseMove { x, y } => {
348 inject_event(injector, TestEvent::MouseMove { x, y });
349 TestResponse::Ok {}
350 }
351 TestCommand::SimulateRightClick { x, y } => {
352 inject_event(injector, TestEvent::MouseMove { x, y });
353 inject_event(injector, TestEvent::MouseDown { x, y, button: 1 });
354 inject_event(injector, TestEvent::MouseUp { x, y, button: 1 });
355 TestResponse::Ok {}
356 }
357 TestCommand::SimulateResize { width, height } => {
358 inject_event(injector, TestEvent::Resize { width, height });
359 TestResponse::Ok {}
360 }
361 }
362}
363
364#[cfg(not(target_arch = "wasm32"))]
365fn auto_scroll_then_query<F>(
366 injector: &EventInjector,
367 query: fission_test_driver::SelectorQuery,
368 make_event: F,
369) -> TestResponse
370where
371 F: FnOnce(fission_test_driver::SelectorQuery, ResponseSender) -> TestEvent,
372{
373 let scroll = query_event(injector, |response_tx| TestEvent::ScrollIntoView {
374 query: query.clone().include_hidden(),
375 response_tx,
376 });
377 if matches!(
378 scroll,
379 TestResponse::Error { .. } | TestResponse::SelectorError { .. }
380 ) {
381 return scroll;
382 }
383 let pump = query_event(injector, |response_tx| TestEvent::Pump { response_tx });
384 if matches!(
385 pump,
386 TestResponse::Error { .. } | TestResponse::SelectorError { .. }
387 ) {
388 return pump;
389 }
390 query_event(injector, |response_tx| make_event(query, response_tx))
391}
392
393#[cfg(not(target_arch = "wasm32"))]
394enum SelectorWaitCondition {
395 Present,
396 Visible,
397 Enabled,
398 Disabled,
399 Value(String),
400 Gone,
401}
402
403#[cfg(not(target_arch = "wasm32"))]
404fn wait_for_selector_state(
405 injector: &EventInjector,
406 query: fission_test_driver::SelectorQuery,
407 timeout_ms: u64,
408 condition: SelectorWaitCondition,
409) -> TestResponse {
410 let start = std::time::Instant::now();
411 let timeout = std::time::Duration::from_millis(timeout_ms);
412 loop {
413 let resp = query_event(injector, |response_tx| TestEvent::ResolveSelector {
414 query: query.clone(),
415 response_tx,
416 });
417 let matched = match (&condition, &resp) {
418 (SelectorWaitCondition::Gone, TestResponse::SelectorError { failure })
419 if failure.kind == fission_test_driver::SelectorFailureKind::NoMatch =>
420 {
421 true
422 }
423 (SelectorWaitCondition::Present, TestResponse::SelectorResolved { .. }) => true,
424 (SelectorWaitCondition::Visible, TestResponse::SelectorResolved { node }) => {
425 node.visibility != fission_test_driver::VisibilityState::Hidden
426 }
427 (SelectorWaitCondition::Enabled, TestResponse::SelectorResolved { node }) => {
428 !node.disabled
429 }
430 (SelectorWaitCondition::Disabled, TestResponse::SelectorResolved { node }) => {
431 node.disabled
432 }
433 (SelectorWaitCondition::Value(expected), TestResponse::SelectorResolved { node }) => {
434 node.value.as_deref() == Some(expected.as_str())
435 }
436 _ => false,
437 };
438 if matched {
439 return TestResponse::Ok {};
440 }
441 if start.elapsed() >= timeout {
442 let candidates = match resp {
443 TestResponse::SelectorResolved { node } => {
444 vec![fission_test_driver::SelectorCandidate {
445 node,
446 rejected_reason: Some("wait condition did not pass".into()),
447 }]
448 }
449 TestResponse::SelectorError { failure } => failure.candidates,
450 _ => Vec::new(),
451 };
452 return TestResponse::SelectorError {
453 failure: fission_test_driver::SelectorFailure {
454 kind: fission_test_driver::SelectorFailureKind::Timeout,
455 selector: query,
456 candidates,
457 message: format!("timed out after {timeout_ms}ms waiting for selector"),
458 },
459 };
460 }
461 let _ = query_event(injector, |response_tx| TestEvent::Pump { response_tx });
462 std::thread::sleep(std::time::Duration::from_millis(25));
463 }
464}
465
466#[cfg(not(target_arch = "wasm32"))]
467fn wait_for_text(injector: &EventInjector, text: String, timeout_ms: u64) -> TestResponse {
468 let start = std::time::Instant::now();
469 let timeout = std::time::Duration::from_millis(timeout_ms);
470 loop {
471 match query_event(injector, |response_tx| TestEvent::GetText { response_tx }) {
472 TestResponse::Text { items } if items.iter().any(|item| item.text.contains(&text)) => {
473 return TestResponse::Ok {};
474 }
475 TestResponse::Error { message } => return TestResponse::Error { message },
476 _ => {}
477 }
478 if start.elapsed() >= timeout {
479 return TestResponse::Error {
480 message: format!("timed out after {timeout_ms}ms waiting for text `{text}`"),
481 };
482 }
483 let _ = query_event(injector, |response_tx| TestEvent::Pump { response_tx });
484 std::thread::sleep(std::time::Duration::from_millis(25));
485 }
486}
487
488#[cfg(not(target_arch = "wasm32"))]
489fn query_event<F>(injector: &EventInjector, make_event: F) -> TestResponse
490where
491 F: FnOnce(ResponseSender) -> TestEvent,
492{
493 let (response_tx, response_rx) = mpsc::channel();
494 inject_event(injector, make_event(response_tx));
495 wait_for_response(&response_rx)
496}
497
498#[cfg(not(target_arch = "wasm32"))]
499fn inject_event(injector: &EventInjector, event: TestEvent) {
500 match injector {
501 EventInjector::Proxy(proxy) => {
502 let _ = proxy.send_event(event);
503 }
504 EventInjector::Queue { queue, wake_proxy } => {
505 #[cfg(target_os = "android")]
506 let debug_android_events = std::env::var_os("FISSION_DEBUG_ANDROID_EVENTS").is_some();
507 #[cfg(target_os = "android")]
508 if debug_android_events {
509 eprintln!("[android-debug] queue_inject={event:?}");
510 }
511 if let Ok(mut pending) = queue.lock() {
512 pending.push_back(event);
513 #[cfg(target_os = "android")]
514 if debug_android_events {
515 eprintln!("[android-debug] queue_len={}", pending.len());
516 }
517 }
518 if let Some(proxy) = wake_proxy {
519 #[cfg(target_os = "android")]
520 if debug_android_events {
521 eprintln!("[android-debug] wake_send");
522 }
523 let _ = proxy.send_event(TestEvent::Wake);
524 }
525 }
526 }
527}
528
529#[cfg(not(target_arch = "wasm32"))]
531fn wait_for_response(rx: &ResponseReceiver) -> TestResponse {
532 match rx.recv_timeout(std::time::Duration::from_secs(30)) {
533 Ok(resp) => resp,
534 Err(_) => TestResponse::Error {
535 message: "timeout waiting for response from event loop".into(),
536 },
537 }
538}
539
540#[cfg(not(target_arch = "wasm32"))]
541fn send_http_response(stream: &mut TcpStream, status: u16, body: &str) {
542 let status_text = match status {
543 200 => "OK",
544 400 => "Bad Request",
545 404 => "Not Found",
546 500 => "Internal Server Error",
547 504 => "Gateway Timeout",
548 _ => "Unknown",
549 };
550 let response = format!(
551 "HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
552 status, status_text, body.len(), body
553 );
554 let _ = stream.write_all(response.as_bytes());
555}