use cranpose_ui_graphics::Rect;
pub fn assert_approx_eq(actual: f32, expected: f32, tolerance: f32, msg: &str) {
let diff = (actual - expected).abs();
assert!(
diff <= tolerance,
"{}: expected {} (±{}), got {} (diff: {})",
msg,
expected,
tolerance,
actual,
diff
);
}
pub fn assert_rect_approx_eq(actual: Rect, expected: Rect, tolerance: f32, msg: &str) {
assert_approx_eq(actual.x, expected.x, tolerance, &format!("{} - x", msg));
assert_approx_eq(actual.y, expected.y, tolerance, &format!("{} - y", msg));
assert_approx_eq(
actual.width,
expected.width,
tolerance,
&format!("{} - width", msg),
);
assert_approx_eq(
actual.height,
expected.height,
tolerance,
&format!("{} - height", msg),
);
}
pub fn assert_rect_contains_point(rect: Rect, x: f32, y: f32, msg: &str) {
assert!(
x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height,
"{}: point ({}, {}) not in rect {:?}",
msg,
x,
y,
rect
);
}
pub fn assert_contains_text(texts: &[String], fragment: &str, msg: &str) {
assert!(
texts.iter().any(|t| t.contains(fragment)),
"{}: text '{}' not found in {:?}",
msg,
fragment,
texts
);
}
pub fn assert_not_contains_text(texts: &[String], fragment: &str, msg: &str) {
assert!(
!texts.iter().any(|t| t.contains(fragment)),
"{}: text '{}' unexpectedly found in {:?}",
msg,
fragment,
texts
);
}
pub fn assert_count<T>(items: &[T], expected: usize, msg: &str) {
assert_eq!(
items.len(),
expected,
"{}: expected {} items, got {}",
msg,
expected,
items.len()
);
}
#[derive(Clone, Copy, Debug)]
pub struct Bounds {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl Bounds {
pub fn center(&self) -> (f32, f32) {
(self.x + self.width / 2.0, self.y + self.height / 2.0)
}
}
pub trait SemanticElementLike {
fn text(&self) -> Option<&str>;
fn role(&self) -> &str;
fn clickable(&self) -> bool;
fn bounds(&self) -> Bounds;
fn children(&self) -> &[Self]
where
Self: Sized;
}
pub fn find_text_center<E: SemanticElementLike>(elements: &[E], text: &str) -> Option<(f32, f32)> {
fn search<E: SemanticElementLike>(elem: &E, text: &str) -> Option<(f32, f32)> {
if let Some(t) = elem.text() {
if t.contains(text) {
return Some(elem.bounds().center());
}
}
for child in elem.children() {
if let Some(pos) = search(child, text) {
return Some(pos);
}
}
None
}
for elem in elements {
if let Some(pos) = search(elem, text) {
return Some(pos);
}
}
None
}
pub fn find_text_bounds<E: SemanticElementLike>(elements: &[E], text: &str) -> Option<Bounds> {
fn search<E: SemanticElementLike>(elem: &E, text: &str) -> Option<Bounds> {
if let Some(t) = elem.text() {
if t.contains(text) {
return Some(elem.bounds());
}
}
for child in elem.children() {
if let Some(bounds) = search(child, text) {
return Some(bounds);
}
}
None
}
for elem in elements {
if let Some(bounds) = search(elem, text) {
return Some(bounds);
}
}
None
}
pub fn find_button_bounds<E: SemanticElementLike>(elements: &[E], text: &str) -> Option<Bounds> {
fn has_text<E: SemanticElementLike>(elem: &E, text: &str) -> bool {
if let Some(t) = elem.text() {
if t.contains(text) {
return true;
}
}
elem.children().iter().any(|c| has_text(c, text))
}
fn search<E: SemanticElementLike>(elem: &E, text: &str) -> Option<Bounds> {
if elem.clickable() && has_text(elem, text) {
return Some(elem.bounds());
}
for child in elem.children() {
if let Some(bounds) = search(child, text) {
return Some(bounds);
}
}
None
}
for elem in elements {
if let Some(bounds) = search(elem, text) {
return Some(bounds);
}
}
None
}
pub fn find_elements_by_role<E: SemanticElementLike>(elements: &[E], role: &str) -> Vec<Bounds> {
fn search<E: SemanticElementLike>(elem: &E, role: &str, results: &mut Vec<Bounds>) {
if elem.role() == role {
results.push(elem.bounds());
}
for child in elem.children() {
search(child, role, results);
}
}
let mut results = Vec::new();
for elem in elements {
search(elem, role, &mut results);
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_approx_eq() {
assert_approx_eq(100.0, 100.0, 0.1, "exact match");
assert_approx_eq(100.05, 100.0, 0.1, "within tolerance");
}
#[test]
#[should_panic]
fn test_approx_eq_fails() {
assert_approx_eq(100.5, 100.0, 0.1, "should fail");
}
#[test]
fn test_rect_approx_eq() {
let rect1 = Rect {
x: 10.0,
y: 20.0,
width: 100.0,
height: 50.0,
};
let rect2 = Rect {
x: 10.05,
y: 20.05,
width: 100.05,
height: 50.05,
};
assert_rect_approx_eq(rect1, rect2, 0.1, "nearly equal rects");
}
#[test]
fn test_rect_contains_point() {
let rect = Rect {
x: 10.0,
y: 20.0,
width: 100.0,
height: 50.0,
};
assert_rect_contains_point(rect, 50.0, 30.0, "center point");
assert_rect_contains_point(rect, 10.0, 20.0, "top-left corner");
assert_rect_contains_point(rect, 110.0, 70.0, "bottom-right corner");
}
#[test]
fn test_contains_text() {
let texts = vec!["Hello".to_string(), "World".to_string()];
assert_contains_text(&texts, "Hello", "exact match");
assert_contains_text(&texts, "Wor", "partial match");
assert_not_contains_text(&texts, "Goodbye", "not present");
}
#[test]
fn test_count() {
let items = vec![1, 2, 3];
assert_count(&items, 3, "correct count");
}
#[test]
fn test_bounds_center() {
let bounds = Bounds {
x: 10.0,
y: 20.0,
width: 100.0,
height: 50.0,
};
let (cx, cy) = bounds.center();
assert_eq!(cx, 60.0);
assert_eq!(cy, 45.0);
}
}