use std::collections::HashMap;
use std::fmt;
use std::ops::Deref;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::error::Error;
use crate::provider::Provider;
use crate::role::Role;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElementData {
pub role: Role,
pub name: Option<String>,
pub value: Option<String>,
pub description: Option<String>,
pub bounds: Option<Rect>,
pub actions: Vec<String>,
pub states: StateSet,
pub numeric_value: Option<f64>,
pub min_value: Option<f64>,
pub max_value: Option<f64>,
pub stable_id: Option<String>,
pub pid: Option<u32>,
pub raw: RawPlatformData,
#[serde(skip, default)]
pub handle: u64,
}
#[derive(Clone)]
pub struct Element {
data: ElementData,
provider: Arc<dyn Provider>,
}
impl Deref for Element {
type Target = ElementData;
fn deref(&self) -> &ElementData {
&self.data
}
}
impl fmt::Debug for Element {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.data, f)
}
}
impl fmt::Display for Element {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name_part = self
.data
.name
.as_ref()
.map(|n| format!(" \"{}\"", n))
.unwrap_or_default();
let value_part = self
.data
.value
.as_ref()
.map(|v| format!(" value=\"{}\"", v))
.unwrap_or_default();
write!(
f,
"{}{}{}",
self.data.role.to_snake_case(),
name_part,
value_part,
)
}
}
impl Serialize for Element {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
self.data.serialize(serializer)
}
}
impl Element {
pub fn new(data: ElementData, provider: Arc<dyn Provider>) -> Self {
Self { data, provider }
}
pub fn data(&self) -> &ElementData {
&self.data
}
pub fn provider(&self) -> &Arc<dyn Provider> {
&self.provider
}
pub fn children(&self) -> crate::error::Result<Vec<Element>> {
let children = self.provider.get_children(Some(&self.data))?;
Ok(children
.into_iter()
.map(|d| Element::new(d, Arc::clone(&self.provider)))
.collect())
}
pub fn parent(&self) -> crate::error::Result<Option<Element>> {
let parent = self.provider.get_parent(&self.data)?;
Ok(parent.map(|d| Element::new(d, Arc::clone(&self.provider))))
}
pub fn pid(&self) -> Option<u32> {
self.data.pid
}
pub fn tree(&self, max_depth: Option<usize>) -> crate::error::Result<TreeNode> {
build_tree_node(self, max_depth, 0)
}
pub fn dump(&self, max_depth: Option<usize>) -> crate::error::Result<String> {
let node = self.tree(max_depth)?;
let mut out = String::new();
write_tree_node(&node, 0, &mut out);
Ok(out)
}
pub fn press(&self) -> crate::error::Result<()> {
self.provider.press(&self.data)
}
pub fn focus(&self) -> crate::error::Result<()> {
self.provider.focus(&self.data)
}
pub fn blur(&self) -> crate::error::Result<()> {
self.provider.blur(&self.data)
}
pub fn toggle(&self) -> crate::error::Result<()> {
self.provider.toggle(&self.data)
}
pub fn select(&self) -> crate::error::Result<()> {
self.provider.select(&self.data)
}
pub fn expand(&self) -> crate::error::Result<()> {
self.provider.expand(&self.data)
}
pub fn collapse(&self) -> crate::error::Result<()> {
self.provider.collapse(&self.data)
}
pub fn show_menu(&self) -> crate::error::Result<()> {
self.provider.show_menu(&self.data)
}
pub fn increment(&self) -> crate::error::Result<()> {
self.provider.increment(&self.data)
}
pub fn decrement(&self) -> crate::error::Result<()> {
self.provider.decrement(&self.data)
}
pub fn scroll_into_view(&self) -> crate::error::Result<()> {
self.provider.scroll_into_view(&self.data)
}
pub fn set_value(&self, value: &str) -> crate::error::Result<()> {
self.provider.set_value(&self.data, value)
}
pub fn set_numeric_value(&self, value: f64) -> crate::error::Result<()> {
if !value.is_finite() {
return Err(Error::InvalidActionData {
message: format!("set_numeric_value requires a finite value, got {}", value),
});
}
self.provider.set_numeric_value(&self.data, value)
}
pub fn type_text(&self, text: &str) -> crate::error::Result<()> {
self.provider.type_text(&self.data, text)
}
pub fn select_text(&self, start: u32, end: u32) -> crate::error::Result<()> {
if start > end {
return Err(Error::InvalidActionData {
message: format!("select_text start ({}) must be <= end ({})", start, end),
});
}
self.provider.set_text_selection(&self.data, start, end)
}
pub fn perform_action(&self, action: &str) -> crate::error::Result<()> {
self.provider.perform_action(&self.data, action)
}
}
fn build_tree_node(
element: &Element,
max_depth: Option<usize>,
depth: usize,
) -> crate::error::Result<TreeNode> {
let children = if max_depth.is_none_or(|d| depth < d) {
element
.children()?
.into_iter()
.map(|child| build_tree_node(&child, max_depth, depth + 1))
.collect::<crate::error::Result<Vec<_>>>()?
} else {
vec![]
};
Ok(TreeNode {
role: element.data.role.to_snake_case().to_string(),
name: element.data.name.clone(),
value: element.data.value.clone(),
children,
})
}
fn write_tree_node(node: &TreeNode, depth: usize, out: &mut String) {
use fmt::Write as _;
let indent = " ".repeat(depth);
write!(out, "{}{}", indent, node.role).unwrap();
if let Some(ref n) = node.name {
write!(out, " \"{}\"", n).unwrap();
}
if let Some(ref v) = node.value {
write!(out, " value=\"{}\"", v).unwrap();
}
out.push('\n');
for child in &node.children {
write_tree_node(child, depth + 1, out);
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StateSet {
pub enabled: bool,
pub visible: bool,
pub focused: bool,
pub checked: Option<Toggled>,
pub selected: bool,
pub expanded: Option<bool>,
pub editable: bool,
pub focusable: bool,
pub modal: bool,
pub required: bool,
pub busy: bool,
}
impl Default for StateSet {
fn default() -> Self {
Self {
enabled: true,
visible: true,
focused: false,
checked: None,
selected: false,
expanded: None,
editable: false,
focusable: false,
modal: false,
required: false,
busy: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Toggled {
Off,
On,
Mixed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Rect {
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
}
pub type RawPlatformData = HashMap<String, serde_json::Value>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TreeNode {
pub role: String,
pub name: Option<String>,
pub value: Option<String>,
pub children: Vec<TreeNode>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mock::{build_provider, MockProvider};
use crate::selector::Selector;
fn find_element(provider: &Arc<MockProvider>, selector: &str) -> Element {
let parsed = Selector::parse(selector).expect("selector must parse");
let provider_dyn: Arc<dyn Provider> = provider.clone();
let root = provider_dyn
.list_apps()
.expect("list_apps must succeed")
.into_iter()
.next()
.expect("mock provider must expose an application root");
let mut matches = provider_dyn
.find_elements(&root, &parsed, Some(1), None)
.expect("find_elements must succeed");
let data = matches.pop().expect("selector matched no elements");
Element::new(data, provider_dyn)
}
fn last_action(provider: &Arc<MockProvider>) -> (u64, String, Option<String>) {
provider
.actions()
.last()
.cloned()
.expect("expected at least one recorded action")
}
#[test]
fn nullary_actions_record_correct_name() {
let provider = build_provider();
let cases = [
(r#"button[name="Back"]"#, "press" as &str),
(r#"button[name="Back"]"#, "focus"),
(r#"button[name="Back"]"#, "blur"),
(r#"check_box[name="Agree"]"#, "toggle"),
(r#"list_item[name="Item 1"]"#, "select"),
(r#"list[name="Items"]"#, "expand"),
(r#"list[name="Items"]"#, "collapse"),
(r#"button[name="Back"]"#, "show_menu"),
(r#"slider[name="Volume"]"#, "increment"),
(r#"slider[name="Volume"]"#, "decrement"),
(r#"button[name="Back"]"#, "scroll_into_view"),
];
for (selector, action) in cases {
provider.clear_actions();
let el = find_element(&provider, selector);
match action {
"press" => el.press().unwrap(),
"focus" => el.focus().unwrap(),
"blur" => el.blur().unwrap(),
"toggle" => el.toggle().unwrap(),
"select" => el.select().unwrap(),
"expand" => el.expand().unwrap(),
"collapse" => el.collapse().unwrap(),
"show_menu" => el.show_menu().unwrap(),
"increment" => el.increment().unwrap(),
"decrement" => el.decrement().unwrap(),
"scroll_into_view" => el.scroll_into_view().unwrap(),
_ => unreachable!(),
}
let (handle, name, data) = last_action(&provider);
assert_eq!(
name, action,
"wrong action recorded for selector {selector}"
);
assert_eq!(data, None, "nullary action should not carry data");
assert_eq!(handle, el.data.handle);
}
}
#[test]
fn set_value_records_text_payload() {
let provider = build_provider();
let el = find_element(&provider, r#"text_field[name="Search"]"#);
el.set_value("world").unwrap();
let (handle, name, data) = last_action(&provider);
assert_eq!(handle, el.data.handle);
assert_eq!(name, "set_value");
assert_eq!(data.as_deref(), Some("world"));
}
#[test]
fn set_numeric_value_records_payload() {
let provider = build_provider();
let el = find_element(&provider, r#"slider[name="Volume"]"#);
el.set_numeric_value(42.0).unwrap();
let (_, name, data) = last_action(&provider);
assert_eq!(name, "set_numeric_value");
assert_eq!(data.as_deref(), Some("42"));
}
#[test]
fn set_numeric_value_rejects_non_finite() {
let provider = build_provider();
let el = find_element(&provider, r#"slider[name="Volume"]"#);
for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(matches!(
el.set_numeric_value(bad),
Err(Error::InvalidActionData { .. })
));
}
assert!(provider.actions().is_empty());
}
#[test]
fn type_text_records_payload() {
let provider = build_provider();
let el = find_element(&provider, r#"text_field[name="Search"]"#);
el.type_text("abc").unwrap();
let (_, name, data) = last_action(&provider);
assert_eq!(name, "type_text");
assert_eq!(data.as_deref(), Some("abc"));
}
#[test]
fn select_text_records_range() {
let provider = build_provider();
let el = find_element(&provider, r#"text_field[name="Search"]"#);
el.select_text(1, 4).unwrap();
let (_, name, data) = last_action(&provider);
assert_eq!(name, "set_text_selection");
assert_eq!(data.as_deref(), Some("1..4"));
}
#[test]
fn select_text_rejects_inverted_range() {
let provider = build_provider();
let el = find_element(&provider, r#"text_field[name="Search"]"#);
assert!(matches!(
el.select_text(5, 2),
Err(Error::InvalidActionData { .. })
));
assert!(provider.actions().is_empty());
}
#[test]
fn perform_action_records_arbitrary_name() {
let provider = build_provider();
let el = find_element(&provider, r#"button[name="Back"]"#);
el.perform_action("raise").unwrap();
let (_, name, _) = last_action(&provider);
assert_eq!(name, "raise");
}
#[test]
fn locator_actions_desugar_to_element_actions() {
let provider = build_provider();
let provider_dyn: Arc<dyn Provider> = provider.clone();
let locator = crate::locator::Locator::new(provider_dyn, None, r#"button[name="Back"]"#);
locator.press().unwrap();
let (_, name, data) = last_action(&provider);
assert_eq!(name, "press");
assert_eq!(data, None);
}
#[test]
fn locator_validation_runs_before_auto_wait() {
let provider = build_provider();
let provider_dyn: Arc<dyn Provider> = provider.clone();
let locator =
crate::locator::Locator::new(provider_dyn, None, r#"button[name="never-matches"]"#);
let started = std::time::Instant::now();
let err = locator.set_numeric_value(f64::NAN).unwrap_err();
assert!(matches!(err, Error::InvalidActionData { .. }));
assert!(
started.elapsed() < std::time::Duration::from_secs(1),
"validation must short-circuit auto-wait",
);
}
}