use smix_driver::{HttpRunnerClient, SimctlDriver};
use smix_error::FailureCode;
use smix_input::{KeyName, SwipeDirection};
use smix_screen::Role;
use smix_screen::{A11yNode, Rect};
use smix_selector::{Modifiers, Pattern, Selector};
use std::time::Duration;
use wiremock::matchers::{body_json, body_partial_json, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn mk_node(label: Option<&str>, bounds: Rect, children: Vec<A11yNode>) -> A11yNode {
A11yNode {
raw_type: "other".into(),
role: None,
identifier: None,
label: label.map(String::from),
title: None,
placeholder_value: None,
value: None,
text: None,
bounds,
enabled: true,
selected: false,
has_focus: false,
visible: true,
children,
}
}
fn rect(x: f64, y: f64, w: f64, h: f64) -> Rect {
Rect { x, y, w, h }
}
fn tree_with_login() -> A11yNode {
let mut root = mk_node(
None,
rect(0.0, 0.0, 390.0, 844.0),
vec![mk_node(
Some("Login"),
rect(50.0, 100.0, 200.0, 40.0),
vec![],
)],
);
root.raw_type = "application".into();
root
}
fn text_sel(t: &str) -> Selector {
Selector::Text {
text: Pattern::text(t),
modifiers: Modifiers::default(),
}
}
fn driver_for(server: &MockServer) -> SimctlDriver {
SimctlDriver::new(HttpRunnerClient::with_base(server.uri()))
}
#[tokio::test]
async fn tree_passthrough() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(tree_with_login()))
.mount(&server)
.await;
let d = driver_for(&server);
let t = d.tree(None).await.expect("tree");
assert_eq!(t.children.len(), 1);
assert_eq!(t.children[0].label.as_deref(), Some("Login"));
}
#[tokio::test]
async fn describe_returns_visible_summaries() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(tree_with_login()))
.mount(&server)
.await;
let d = driver_for(&server);
let desc = d.describe().await.expect("describe");
assert_eq!(desc.elements.len(), 2);
assert_eq!(desc.elements[1].name.as_deref(), Some("Login"));
}
#[tokio::test]
async fn find_one_resolves_via_tree() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(tree_with_login()))
.mount(&server)
.await;
let d = driver_for(&server);
let node = d.find_one(&text_sel("Login"), None).await.expect("ok");
assert!(node.is_some());
}
#[tokio::test]
async fn find_one_returns_none_when_missing() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(tree_with_login()))
.mount(&server)
.await;
let d = driver_for(&server);
let node = d
.find_one(&text_sel("DoesNotExist"), None)
.await
.expect("ok");
assert!(node.is_none());
}
#[tokio::test]
async fn find_quick_probe_passthrough() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/find"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"exists": true
})))
.mount(&server)
.await;
let d = driver_for(&server);
assert!(d.find(&text_sel("X"), None).await.unwrap());
}
#[tokio::test]
async fn tap_resolves_then_tap_at_norm_coord() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(tree_with_login()))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/tap-at-norm-coord"))
.and(body_partial_json(serde_json::json!({})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"ok": true
})))
.mount(&server)
.await;
let d = driver_for(&server);
d.tap(&text_sel("Login"), None).await.expect("tap ok");
}
#[tokio::test]
async fn tap_miss_returns_element_not_found_after_implicit_wait() {
let server = MockServer::start().await;
let empty_tree = mk_node(None, rect(0.0, 0.0, 390.0, 844.0), vec![]);
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(empty_tree))
.mount(&server)
.await;
let d = driver_for(&server);
let err = d.tap(&text_sel("DoesNotExist"), None).await.unwrap_err();
assert_eq!(err.code, FailureCode::ElementNotFound);
assert!(err.message.contains("element not found"));
}
#[tokio::test]
async fn fill_chunks_one_char_per_post() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/fill"))
.and(body_partial_json(serde_json::json!({
"selector": {"text": "Email"}
})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"tree": null, "stages": null
})))
.expect(7) .mount(&server)
.await;
let d = driver_for(&server);
d.fill(&text_sel("Email"), "u@x.com", None).await.unwrap();
}
#[tokio::test]
async fn fill_with_id_selector_focus_taps_then_fills_focused() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(tree_with_id_and_role()))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/tap-at-norm-coord"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"tree": null
})))
.expect(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/fill"))
.and(body_partial_json(serde_json::json!({
"selector": {"focused": true}
})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"tree": null, "stages": null
})))
.expect(5) .mount(&server)
.await;
let d = driver_for(&server);
d.fill(&id_sel("login-btn"), "Hello", None).await.unwrap();
}
#[tokio::test]
async fn fill_single_char_takes_fast_path() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/fill"))
.and(body_json(serde_json::json!({
"selector": {"text": "Email"},
"text": "x"
})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"tree": null, "stages": null
})))
.expect(1)
.mount(&server)
.await;
let d = driver_for(&server);
d.fill(&text_sel("Email"), "x", None).await.unwrap();
}
#[tokio::test]
async fn clear_with_text_selector_takes_fast_path() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/clear"))
.and(body_partial_json(serde_json::json!({
"selector": {"text": "Email"}
})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"tree": null, "stages": null
})))
.expect(1)
.mount(&server)
.await;
let d = driver_for(&server);
d.clear(&text_sel("Email"), None).await.unwrap();
}
#[tokio::test]
async fn clear_with_id_selector_focus_taps_then_clears_focused() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(tree_with_id_and_role()))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/tap-at-norm-coord"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"tree": null
})))
.expect(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/clear"))
.and(body_partial_json(serde_json::json!({
"selector": {"focused": true}
})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"tree": null, "stages": null
})))
.expect(1)
.mount(&server)
.await;
let d = driver_for(&server);
d.clear(&id_sel("login-btn"), None).await.unwrap();
}
#[tokio::test]
async fn press_key_passthrough() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/press-key"))
.and(body_json(serde_json::json!({"key": "return"})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"tree": null, "stages": null
})))
.mount(&server)
.await;
let d = driver_for(&server);
d.press_key(KeyName::Return).await.unwrap();
}
#[tokio::test]
async fn swipe_once_passthrough() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/swipe-once"))
.and(body_json(serde_json::json!({"direction": "up"})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"ok": true
})))
.mount(&server)
.await;
let d = driver_for(&server);
d.swipe_once(SwipeDirection::Up).await.unwrap();
}
#[tokio::test]
async fn back_and_hide_keyboard_passthroughs() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/back"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"ok": true
})))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/hide-keyboard"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"ok": true
})))
.mount(&server)
.await;
let d = driver_for(&server);
d.back().await.unwrap();
d.hide_keyboard().await.unwrap();
}
#[tokio::test]
async fn wait_for_resolves_immediately_on_hit() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(tree_with_login()))
.mount(&server)
.await;
let d = driver_for(&server);
let node = d
.wait_for(&text_sel("Login"), Duration::from_millis(500), None)
.await
.expect("wait_for");
assert_eq!(node.label.as_deref(), Some("Login"));
}
#[tokio::test]
async fn wait_for_times_out_when_never_appears() {
let server = MockServer::start().await;
let empty_tree = mk_node(None, rect(0.0, 0.0, 390.0, 844.0), vec![]);
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(empty_tree))
.mount(&server)
.await;
let d = driver_for(&server);
let err = d
.wait_for(&text_sel("Never"), Duration::from_millis(300), None)
.await
.unwrap_err();
assert_eq!(err.code, FailureCode::Timeout);
}
#[tokio::test]
async fn dispose_idempotent_noop() {
let server = MockServer::start().await;
let d = driver_for(&server);
d.dispose().await.unwrap();
d.dispose().await.unwrap();
}
fn id_sel(s: &str) -> Selector {
Selector::Id {
id: s.to_string(),
modifiers: Modifiers::default(),
}
}
fn label_sel(s: &str) -> Selector {
Selector::Label {
label: s.to_string(),
modifiers: Modifiers::default(),
}
}
fn role_sel(name: &str) -> Selector {
Selector::Role {
role: Role::Button,
name: Some(Pattern::text(name)),
modifiers: Modifiers::default(),
}
}
fn tree_with_id_and_role() -> A11yNode {
let mut button = mk_node(Some("Login"), rect(50.0, 100.0, 200.0, 40.0), vec![]);
button.raw_type = "button".into();
button.identifier = Some("login-btn".into());
let mut root = mk_node(None, rect(0.0, 0.0, 390.0, 844.0), vec![button]);
root.raw_type = "application".into();
root
}
#[tokio::test]
async fn find_with_id_selector_falls_back_to_host_resolve() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(tree_with_id_and_role()))
.mount(&server)
.await;
let d = driver_for(&server);
assert!(d.find(&id_sel("login-btn"), None).await.unwrap());
assert!(!d.find(&id_sel("missing-btn"), None).await.unwrap());
}
#[tokio::test]
async fn find_with_label_selector_falls_back_to_host_resolve() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(tree_with_login()))
.mount(&server)
.await;
let d = driver_for(&server);
assert!(d.find(&label_sel("Login"), None).await.unwrap());
assert!(!d.find(&label_sel("Logout"), None).await.unwrap());
}
#[tokio::test]
async fn find_with_role_selector_falls_back_to_host_resolve() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(tree_with_id_and_role()))
.mount(&server)
.await;
let d = driver_for(&server);
assert!(d.find(&role_sel("Login"), None).await.unwrap());
}
#[tokio::test]
async fn find_with_text_plus_index_modifier_falls_back_to_host_resolve() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(tree_with_login()))
.mount(&server)
.await;
let d = driver_for(&server);
let sel = Selector::Text {
text: Pattern::text("Login"),
modifiers: Modifiers {
first: Some(true),
..Default::default()
},
};
assert!(d.find(&sel, None).await.unwrap());
}
#[tokio::test]
async fn find_with_text_plus_ancestor_modifier_falls_back_to_host_resolve() {
let server = MockServer::start().await;
let mut tab_bar = mk_node(
None,
rect(0.0, 760.0, 390.0, 80.0),
vec![mk_node(Some("Same"), rect(50.0, 780.0, 80.0, 40.0), vec![])],
);
tab_bar.raw_type = "tabBar".into();
tab_bar.role = Some(Role::TabBar);
let mut root = mk_node(None, rect(0.0, 0.0, 390.0, 844.0), vec![tab_bar]);
root.raw_type = "application".into();
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(root))
.mount(&server)
.await;
let d = driver_for(&server);
let sel = Selector::Text {
text: Pattern::text("Same"),
modifiers: Modifiers {
ancestor: Some(Box::new(Selector::Role {
role: Role::TabBar,
name: None,
modifiers: Modifiers::default(),
})),
..Default::default()
},
};
assert!(
d.find(&sel, None)
.await
.expect("ancestor selector should dispatch to host-resolve via /tree")
);
}
#[tokio::test]
async fn tap_at_norm_coord_passthrough() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/tap-at-norm-coord"))
.and(body_partial_json(
serde_json::json!({ "nx": 0.5, "ny": 0.95 }),
))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ "ok": true })))
.expect(1)
.mount(&server)
.await;
let d = driver_for(&server);
d.tap_at_norm_coord(0.5, 0.95)
.await
.expect("tap_at_norm_coord should pass through to runner");
}
#[tokio::test]
async fn find_host_resolve_retries_transient_tree_500() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(
ResponseTemplate::new(500)
.set_body_json(serde_json::json!({"ok": false, "error": "snapshot_unavailable"})),
)
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(tree_with_id_and_role()))
.mount(&server)
.await;
let d = driver_for(&server);
let present = d
.find(&id_sel("login-btn"), None)
.await
.expect("find host-resolve branch must retry transient /tree 500");
assert!(present, "Login id node is present after retry");
}
#[tokio::test]
async fn find_runner_route_retries_transient_find_500() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/find"))
.respond_with(
ResponseTemplate::new(500)
.set_body_json(serde_json::json!({"ok": false, "error": "transient"})),
)
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/find"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({"exists": true, "ok": true})),
)
.mount(&server)
.await;
let d = driver_for(&server);
let present = d
.find(&text_sel("Login"), None)
.await
.expect("find runner-route branch must retry transient /find 500");
assert!(present, "/find returns exists=true after retry");
}
#[tokio::test]
async fn tap_retries_transient_tree_500() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(
ResponseTemplate::new(500)
.set_body_json(serde_json::json!({"ok": false, "error": "snapshot_unavailable"})),
)
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(tree_with_login()))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/tap-at-norm-coord"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"ok": true})))
.mount(&server)
.await;
let d = driver_for(&server);
d.tap(&text_sel("Login"), None)
.await
.expect("tap must retry transient /tree 500 and proceed");
}
#[tokio::test]
async fn find_one_and_find_all_retry_transient_tree_500() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(
ResponseTemplate::new(500)
.set_body_json(serde_json::json!({"ok": false, "error": "snapshot_unavailable"})),
)
.up_to_n_times(2) .mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/tree"))
.respond_with(ResponseTemplate::new(200).set_body_json(tree_with_id_and_role()))
.mount(&server)
.await;
let d = driver_for(&server);
let one = d
.find_one(&id_sel("login-btn"), None)
.await
.expect("find_one must retry transient /tree 500");
assert!(one.is_some(), "find_one should resolve after retry");
let all = d
.find_all(&id_sel("login-btn"), None)
.await
.expect("find_all must retry transient /tree 500");
assert_eq!(all.len(), 1, "find_all should match 1 node after retry");
}
#[tokio::test]
async fn system_popup_action_returns_true_on_runner_ok() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/system-popup-action"))
.and(body_partial_json(
serde_json::json!({"popupId": "p-1", "buttonId": "b-open"}),
))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"ok": true})))
.mount(&server)
.await;
let d = driver_for(&server);
let ok = d
.system_popup_action("p-1", "b-open")
.await
.expect("system_popup_action ok path");
assert!(ok);
}
#[tokio::test]
async fn system_popup_action_returns_false_on_runner_404_not_found() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/system-popup-action"))
.respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
"ok": false,
"error": "not_found",
"popupId": "p-x",
"buttonId": "b-x"
})))
.mount(&server)
.await;
let d = driver_for(&server);
let ok = d
.system_popup_action("p-x", "b-x")
.await
.expect("system_popup_action 404 surfaces as Ok(false)");
assert!(!ok);
}