const ROW_ROLE: &str = "AXRow";
const OUTLINE_ROLE: &str = "AXOutline";
const TABLE_ROLE: &str = "AXTable";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum RowResolution {
Resolved {
row_idx: usize,
container_idx: usize,
},
NoRow,
NoContainer {
row_idx: usize,
},
}
pub(crate) fn resolve_row_and_container(ancestor_roles: &[Option<&str>]) -> RowResolution {
let Some(row_idx) = ancestor_roles
.iter()
.position(|r| matches!(r, Some(role) if *role == ROW_ROLE))
else {
return RowResolution::NoRow;
};
let Some(offset) = ancestor_roles[row_idx + 1..]
.iter()
.position(|r| matches!(r, Some(role) if *role == OUTLINE_ROLE || *role == TABLE_ROLE))
else {
return RowResolution::NoContainer { row_idx };
};
RowResolution::Resolved {
row_idx,
container_idx: row_idx + 1 + offset,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn some(s: &str) -> Option<&str> {
Some(s)
}
fn chain<'a>(roles: &[&'a str]) -> Vec<Option<&'a str>> {
roles.iter().map(|r| Some(*r)).collect()
}
#[test]
fn row_as_start_resolves_to_itself() {
let roles = chain(&["AXRow", "AXOutline", "AXScrollArea", "AXWindow"]);
assert_eq!(
resolve_row_and_container(&roles),
RowResolution::Resolved {
row_idx: 0,
container_idx: 1,
}
);
}
#[test]
fn cell_start_resolves_to_parent_row() {
let roles = chain(&["AXCell", "AXRow", "AXOutline", "AXScrollArea", "AXWindow"]);
assert_eq!(
resolve_row_and_container(&roles),
RowResolution::Resolved {
row_idx: 1,
container_idx: 2,
}
);
}
#[test]
fn deeper_descendant_resolves_to_enclosing_row() {
let roles = chain(&[
"AXStaticText",
"AXCell",
"AXRow",
"AXOutline",
"AXScrollArea",
"AXWindow",
]);
assert_eq!(
resolve_row_and_container(&roles),
RowResolution::Resolved {
row_idx: 2,
container_idx: 3,
}
);
}
#[test]
fn table_instead_of_outline_also_resolves() {
let roles = chain(&["AXCell", "AXRow", "AXTable", "AXScrollArea", "AXWindow"]);
assert_eq!(
resolve_row_and_container(&roles),
RowResolution::Resolved {
row_idx: 1,
container_idx: 2,
}
);
}
#[test]
fn non_row_descendant_chain_reports_no_row() {
let roles = chain(&["AXButton", "AXGroup", "AXWindow", "AXApplication"]);
assert_eq!(resolve_row_and_container(&roles), RowResolution::NoRow);
}
#[test]
fn row_without_outline_container_reports_no_container() {
let roles = chain(&["AXCell", "AXRow", "AXGroup", "AXWindow", "AXApplication"]);
assert_eq!(
resolve_row_and_container(&roles),
RowResolution::NoContainer { row_idx: 1 }
);
}
#[test]
fn unreadable_ancestor_roles_do_not_abort_walk() {
let roles = vec![
some("AXStaticText"),
None, some("AXRow"),
None, some("AXOutline"),
some("AXWindow"),
];
assert_eq!(
resolve_row_and_container(&roles),
RowResolution::Resolved {
row_idx: 2,
container_idx: 4,
}
);
}
#[test]
fn empty_chain_reports_no_row() {
let roles: Vec<Option<&str>> = Vec::new();
assert_eq!(resolve_row_and_container(&roles), RowResolution::NoRow);
}
#[test]
fn row_with_no_further_ancestors_reports_no_container() {
let roles = chain(&["AXRow"]);
assert_eq!(
resolve_row_and_container(&roles),
RowResolution::NoContainer { row_idx: 0 }
);
}
}