#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum Condition {
Name(String),
AutomationId(String),
ControlType(String),
ClassName(String),
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct Step {
pub conditions: Vec<Condition>,
pub index: Option<usize>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct Selector {
pub steps: Vec<Step>,
}
impl Selector {
pub fn is_single_step(&self) -> bool {
self.steps.len() == 1
}
}
#[allow(dead_code)]
pub fn parse(input: &str) -> Result<Selector, String> {
let input = input.trim();
if input.is_empty() {
return Err("empty selector".into());
}
let raw_steps: Vec<&str> = input.split('>').collect();
let mut steps = Vec::with_capacity(raw_steps.len());
for raw in raw_steps {
let raw = raw.trim();
if raw.is_empty() {
return Err("empty step in selector (double > ?)".into());
}
steps.push(parse_step(raw)?);
}
Ok(Selector { steps })
}
fn parse_step(input: &str) -> Result<Step, String> {
let (body, index) = if let Some(bracket) = input.rfind('[') {
if input.ends_with(']') {
let idx_str = &input[bracket + 1..input.len() - 1];
let idx = idx_str
.parse::<usize>()
.map_err(|_| format!("invalid index: [{}]", idx_str))?;
(&input[..bracket], Some(idx))
} else {
(input, None)
}
} else {
(input, None)
};
let parts: Vec<&str> = body.split('&').collect();
let mut conditions = Vec::with_capacity(parts.len());
for part in parts {
let part = part.trim();
if let Some(val) = part.strip_prefix("name:") {
conditions.push(Condition::Name(val.to_string()));
} else if let Some(val) = part.strip_prefix("aid:") {
conditions.push(Condition::AutomationId(val.to_string()));
} else if let Some(val) = part.strip_prefix("type:") {
conditions.push(Condition::ControlType(val.to_string()));
} else if let Some(val) = part.strip_prefix("class:") {
conditions.push(Condition::ClassName(val.to_string()));
} else {
conditions.push(Condition::Name(part.to_string()));
}
}
if conditions.is_empty() {
return Err("step has no conditions".into());
}
Ok(Step { conditions, index })
}
#[allow(dead_code)]
pub fn control_type_id(name: &str) -> Option<i32> {
match name.to_lowercase().as_str() {
"button" => Some(50000),
"calendar" => Some(50001),
"checkbox" => Some(50002),
"combobox" => Some(50003),
"edit" => Some(50004),
"hyperlink" => Some(50005),
"image" => Some(50006),
"listitem" => Some(50007),
"list" => Some(50008),
"menu" => Some(50009),
"menubar" => Some(50010),
"menuitem" => Some(50011),
"progressbar" => Some(50012),
"radiobutton" => Some(50013),
"scrollbar" => Some(50014),
"slider" => Some(50015),
"spinner" => Some(50016),
"statusbar" => Some(50017),
"tab" => Some(50018),
"tabitem" => Some(50019),
"text" => Some(50020),
"toolbar" => Some(50021),
"tooltip" => Some(50022),
"tree" => Some(50023),
"treeitem" => Some(50024),
"custom" => Some(50025),
"group" => Some(50026),
"thumb" => Some(50027),
"datagrid" => Some(50028),
"dataitem" => Some(50029),
"document" => Some(50030),
"splitbutton" => Some(50031),
"window" => Some(50032),
"pane" => Some(50033),
"header" => Some(50034),
"headeritem" => Some(50035),
"table" => Some(50036),
"titlebar" => Some(50037),
"separator" => Some(50038),
_ => None,
}
}
#[allow(dead_code)]
pub fn control_type_name(id: i32) -> &'static str {
match id {
50000 => "Button",
50001 => "Calendar",
50002 => "CheckBox",
50003 => "ComboBox",
50004 => "Edit",
50005 => "Hyperlink",
50006 => "Image",
50007 => "ListItem",
50008 => "List",
50009 => "Menu",
50010 => "MenuBar",
50011 => "MenuItem",
50012 => "ProgressBar",
50013 => "RadioButton",
50014 => "ScrollBar",
50015 => "Slider",
50016 => "Spinner",
50017 => "StatusBar",
50018 => "Tab",
50019 => "TabItem",
50020 => "Text",
50021 => "ToolBar",
50022 => "ToolTip",
50023 => "Tree",
50024 => "TreeItem",
50025 => "Custom",
50026 => "Group",
50027 => "Thumb",
50028 => "DataGrid",
50029 => "DataItem",
50030 => "Document",
50031 => "SplitButton",
50032 => "Window",
50033 => "Pane",
50034 => "Header",
50035 => "HeaderItem",
50036 => "Table",
50037 => "TitleBar",
50038 => "Separator",
_ => "Unknown",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple_name() {
let sel = parse("name:Business Number").unwrap();
assert_eq!(sel.steps.len(), 1);
assert!(matches!(&sel.steps[0].conditions[0], Condition::Name(n) if n == "Business Number"));
}
#[test]
fn parse_and_condition() {
let sel = parse("name:Save&type:Button").unwrap();
assert_eq!(sel.steps[0].conditions.len(), 2);
}
#[test]
fn parse_tree_descent() {
let sel = parse("name:Main > type:Tab > aid:txtField").unwrap();
assert_eq!(sel.steps.len(), 3);
}
#[test]
fn parse_with_index() {
let sel = parse("type:Edit[2]").unwrap();
assert_eq!(sel.steps[0].index, Some(2));
}
#[test]
fn parse_bare_string_is_name() {
let sel = parse("Save").unwrap();
assert!(matches!(&sel.steps[0].conditions[0], Condition::Name(n) if n == "Save"));
}
#[test]
fn control_type_lookup() {
assert_eq!(control_type_id("Edit"), Some(50004));
assert_eq!(control_type_id("button"), Some(50000));
assert_eq!(control_type_id("nonsense"), None);
}
}