Skip to main content

fission_shell_winit/
test_control.rs

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
18/// Sender for query responses from the main event loop back to the TCP server.
19pub type ResponseSender = fission_test_driver::TestResponseSender;
20/// Receiver for query responses.
21pub type ResponseReceiver = mpsc::Receiver<TestResponse>;
22/// Shared queue used on platforms where winit user events are unreliable.
23#[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/// Spawn the TCP test-control server.
42#[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/// Block until the main event loop sends a response, with a 30-second timeout.
518#[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}