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::TypeText { text } => {
246 inject_event(injector, TestEvent::TextInput { text });
247 TestResponse::Ok {}
248 }
249 TestCommand::ImePreedit {
250 text,
251 cursor_start,
252 cursor_end,
253 } => {
254 let cursor = match (cursor_start, cursor_end) {
255 (Some(start), Some(end)) => Some((start, end)),
256 _ => None,
257 };
258 inject_event(injector, TestEvent::ImePreedit { text, cursor });
259 TestResponse::Ok {}
260 }
261 TestCommand::ImeCommit { text } => {
262 inject_event(injector, TestEvent::ImeCommit { text });
263 TestResponse::Ok {}
264 }
265 TestCommand::ImeCancel {} => {
266 inject_event(injector, TestEvent::ImeCancel);
267 TestResponse::Ok {}
268 }
269 TestCommand::PressKey { key, modifiers } => {
270 inject_event(
271 injector,
272 TestEvent::KeyDown {
273 key_code: key.clone(),
274 modifiers,
275 },
276 );
277 inject_event(
278 injector,
279 TestEvent::KeyUp {
280 key_code: key,
281 modifiers,
282 },
283 );
284 TestResponse::Ok {}
285 }
286 TestCommand::Screenshot { path } => query_event(injector, |response_tx| {
287 TestEvent::Screenshot { path, response_tx }
288 }),
289 TestCommand::CaptureScreenshot {} => query_event(injector, |response_tx| {
290 TestEvent::CaptureScreenshot { response_tx }
291 }),
292 TestCommand::GetText {} => {
293 query_event(injector, |response_tx| TestEvent::GetText { response_tx })
294 }
295 TestCommand::GetTree {} => {
296 query_event(injector, |response_tx| TestEvent::GetTree { response_tx })
297 }
298 TestCommand::Wait { ms } => {
299 std::thread::sleep(std::time::Duration::from_millis(ms));
300 TestResponse::Ok {}
301 }
302 TestCommand::WaitForSelector { query, timeout_ms } => {
303 wait_for_selector_state(injector, query, timeout_ms, SelectorWaitCondition::Present)
304 }
305 TestCommand::WaitForVisible { query, timeout_ms } => {
306 wait_for_selector_state(injector, query, timeout_ms, SelectorWaitCondition::Visible)
307 }
308 TestCommand::WaitForEnabled { query, timeout_ms } => {
309 wait_for_selector_state(injector, query, timeout_ms, SelectorWaitCondition::Enabled)
310 }
311 TestCommand::WaitForDisabled { query, timeout_ms } => {
312 wait_for_selector_state(injector, query, timeout_ms, SelectorWaitCondition::Disabled)
313 }
314 TestCommand::WaitForValue {
315 query,
316 value,
317 timeout_ms,
318 } => wait_for_selector_state(
319 injector,
320 query,
321 timeout_ms,
322 SelectorWaitCondition::Value(value),
323 ),
324 TestCommand::WaitForText { text, timeout_ms } => wait_for_text(injector, text, timeout_ms),
325 TestCommand::WaitForGone { query, timeout_ms } => {
326 wait_for_selector_state(injector, query, timeout_ms, SelectorWaitCondition::Gone)
327 }
328 TestCommand::Pump {} => {
329 query_event(injector, |response_tx| TestEvent::Pump { response_tx })
330 }
331 TestCommand::Quit {} => {
332 inject_event(injector, TestEvent::Quit);
333 TestResponse::Ok {}
334 }
335 TestCommand::SimulateMouseMove { x, y } => {
336 inject_event(injector, TestEvent::MouseMove { x, y });
337 TestResponse::Ok {}
338 }
339 TestCommand::SimulateRightClick { x, y } => {
340 inject_event(injector, TestEvent::MouseMove { x, y });
341 inject_event(injector, TestEvent::MouseDown { x, y, button: 1 });
342 inject_event(injector, TestEvent::MouseUp { x, y, button: 1 });
343 TestResponse::Ok {}
344 }
345 TestCommand::SimulateResize { width, height } => {
346 inject_event(injector, TestEvent::Resize { width, height });
347 TestResponse::Ok {}
348 }
349 }
350}
351
352#[cfg(not(target_arch = "wasm32"))]
353fn auto_scroll_then_query<F>(
354 injector: &EventInjector,
355 query: fission_test_driver::SelectorQuery,
356 make_event: F,
357) -> TestResponse
358where
359 F: FnOnce(fission_test_driver::SelectorQuery, ResponseSender) -> TestEvent,
360{
361 let scroll = query_event(injector, |response_tx| TestEvent::ScrollIntoView {
362 query: query.clone().include_hidden(),
363 response_tx,
364 });
365 if matches!(
366 scroll,
367 TestResponse::Error { .. } | TestResponse::SelectorError { .. }
368 ) {
369 return scroll;
370 }
371 let pump = query_event(injector, |response_tx| TestEvent::Pump { response_tx });
372 if matches!(
373 pump,
374 TestResponse::Error { .. } | TestResponse::SelectorError { .. }
375 ) {
376 return pump;
377 }
378 query_event(injector, |response_tx| make_event(query, response_tx))
379}
380
381#[cfg(not(target_arch = "wasm32"))]
382enum SelectorWaitCondition {
383 Present,
384 Visible,
385 Enabled,
386 Disabled,
387 Value(String),
388 Gone,
389}
390
391#[cfg(not(target_arch = "wasm32"))]
392fn wait_for_selector_state(
393 injector: &EventInjector,
394 query: fission_test_driver::SelectorQuery,
395 timeout_ms: u64,
396 condition: SelectorWaitCondition,
397) -> TestResponse {
398 let start = std::time::Instant::now();
399 let timeout = std::time::Duration::from_millis(timeout_ms);
400 loop {
401 let resp = query_event(injector, |response_tx| TestEvent::ResolveSelector {
402 query: query.clone(),
403 response_tx,
404 });
405 let matched = match (&condition, &resp) {
406 (SelectorWaitCondition::Gone, TestResponse::SelectorError { failure })
407 if failure.kind == fission_test_driver::SelectorFailureKind::NoMatch =>
408 {
409 true
410 }
411 (SelectorWaitCondition::Present, TestResponse::SelectorResolved { .. }) => true,
412 (SelectorWaitCondition::Visible, TestResponse::SelectorResolved { node }) => {
413 node.visibility != fission_test_driver::VisibilityState::Hidden
414 }
415 (SelectorWaitCondition::Enabled, TestResponse::SelectorResolved { node }) => {
416 !node.disabled
417 }
418 (SelectorWaitCondition::Disabled, TestResponse::SelectorResolved { node }) => {
419 node.disabled
420 }
421 (SelectorWaitCondition::Value(expected), TestResponse::SelectorResolved { node }) => {
422 node.value.as_deref() == Some(expected.as_str())
423 }
424 _ => false,
425 };
426 if matched {
427 return TestResponse::Ok {};
428 }
429 if start.elapsed() >= timeout {
430 let candidates = match resp {
431 TestResponse::SelectorResolved { node } => {
432 vec![fission_test_driver::SelectorCandidate {
433 node,
434 rejected_reason: Some("wait condition did not pass".into()),
435 }]
436 }
437 TestResponse::SelectorError { failure } => failure.candidates,
438 _ => Vec::new(),
439 };
440 return TestResponse::SelectorError {
441 failure: fission_test_driver::SelectorFailure {
442 kind: fission_test_driver::SelectorFailureKind::Timeout,
443 selector: query,
444 candidates,
445 message: format!("timed out after {timeout_ms}ms waiting for selector"),
446 },
447 };
448 }
449 let _ = query_event(injector, |response_tx| TestEvent::Pump { response_tx });
450 std::thread::sleep(std::time::Duration::from_millis(25));
451 }
452}
453
454#[cfg(not(target_arch = "wasm32"))]
455fn wait_for_text(injector: &EventInjector, text: String, timeout_ms: u64) -> TestResponse {
456 let start = std::time::Instant::now();
457 let timeout = std::time::Duration::from_millis(timeout_ms);
458 loop {
459 match query_event(injector, |response_tx| TestEvent::GetText { response_tx }) {
460 TestResponse::Text { items } if items.iter().any(|item| item.text.contains(&text)) => {
461 return TestResponse::Ok {};
462 }
463 TestResponse::Error { message } => return TestResponse::Error { message },
464 _ => {}
465 }
466 if start.elapsed() >= timeout {
467 return TestResponse::Error {
468 message: format!("timed out after {timeout_ms}ms waiting for text `{text}`"),
469 };
470 }
471 let _ = query_event(injector, |response_tx| TestEvent::Pump { response_tx });
472 std::thread::sleep(std::time::Duration::from_millis(25));
473 }
474}
475
476#[cfg(not(target_arch = "wasm32"))]
477fn query_event<F>(injector: &EventInjector, make_event: F) -> TestResponse
478where
479 F: FnOnce(ResponseSender) -> TestEvent,
480{
481 let (response_tx, response_rx) = mpsc::channel();
482 inject_event(injector, make_event(response_tx));
483 wait_for_response(&response_rx)
484}
485
486#[cfg(not(target_arch = "wasm32"))]
487fn inject_event(injector: &EventInjector, event: TestEvent) {
488 match injector {
489 EventInjector::Proxy(proxy) => {
490 let _ = proxy.send_event(event);
491 }
492 EventInjector::Queue { queue, wake_proxy } => {
493 #[cfg(target_os = "android")]
494 let debug_android_events = std::env::var_os("FISSION_DEBUG_ANDROID_EVENTS").is_some();
495 #[cfg(target_os = "android")]
496 if debug_android_events {
497 eprintln!("[android-debug] queue_inject={event:?}");
498 }
499 if let Ok(mut pending) = queue.lock() {
500 pending.push_back(event);
501 #[cfg(target_os = "android")]
502 if debug_android_events {
503 eprintln!("[android-debug] queue_len={}", pending.len());
504 }
505 }
506 if let Some(proxy) = wake_proxy {
507 #[cfg(target_os = "android")]
508 if debug_android_events {
509 eprintln!("[android-debug] wake_send");
510 }
511 let _ = proxy.send_event(TestEvent::Wake);
512 }
513 }
514 }
515}
516
517#[cfg(not(target_arch = "wasm32"))]
519fn wait_for_response(rx: &ResponseReceiver) -> TestResponse {
520 match rx.recv_timeout(std::time::Duration::from_secs(30)) {
521 Ok(resp) => resp,
522 Err(_) => TestResponse::Error {
523 message: "timeout waiting for response from event loop".into(),
524 },
525 }
526}
527
528#[cfg(not(target_arch = "wasm32"))]
529fn send_http_response(stream: &mut TcpStream, status: u16, body: &str) {
530 let status_text = match status {
531 200 => "OK",
532 400 => "Bad Request",
533 404 => "Not Found",
534 500 => "Internal Server Error",
535 504 => "Gateway Timeout",
536 _ => "Unknown",
537 };
538 let response = format!(
539 "HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
540 status, status_text, body.len(), body
541 );
542 let _ = stream.write_all(response.as_bytes());
543}