1use cranpose_ui_graphics::Rect;
7
8pub fn assert_approx_eq(actual: f32, expected: f32, tolerance: f32, msg: &str) {
13 let diff = (actual - expected).abs();
14 assert!(
15 diff <= tolerance,
16 "{}: expected {} (±{}), got {} (diff: {})",
17 msg,
18 expected,
19 tolerance,
20 actual,
21 diff
22 );
23}
24
25pub fn assert_rect_approx_eq(actual: Rect, expected: Rect, tolerance: f32, msg: &str) {
27 assert_approx_eq(actual.x, expected.x, tolerance, &format!("{} - x", msg));
28 assert_approx_eq(actual.y, expected.y, tolerance, &format!("{} - y", msg));
29 assert_approx_eq(
30 actual.width,
31 expected.width,
32 tolerance,
33 &format!("{} - width", msg),
34 );
35 assert_approx_eq(
36 actual.height,
37 expected.height,
38 tolerance,
39 &format!("{} - height", msg),
40 );
41}
42
43pub fn assert_rect_contains_point(rect: Rect, x: f32, y: f32, msg: &str) {
45 assert!(
46 x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height,
47 "{}: point ({}, {}) not in rect {:?}",
48 msg,
49 x,
50 y,
51 rect
52 );
53}
54
55pub fn assert_contains_text(texts: &[String], fragment: &str, msg: &str) {
57 assert!(
58 texts.iter().any(|t| t.contains(fragment)),
59 "{}: text '{}' not found in {:?}",
60 msg,
61 fragment,
62 texts
63 );
64}
65
66pub fn assert_not_contains_text(texts: &[String], fragment: &str, msg: &str) {
68 assert!(
69 !texts.iter().any(|t| t.contains(fragment)),
70 "{}: text '{}' unexpectedly found in {:?}",
71 msg,
72 fragment,
73 texts
74 );
75}
76
77pub fn assert_count<T>(items: &[T], expected: usize, msg: &str) {
79 assert_eq!(
80 items.len(),
81 expected,
82 "{}: expected {} items, got {}",
83 msg,
84 expected,
85 items.len()
86 );
87}
88
89#[derive(Clone, Copy, Debug)]
95pub struct Bounds {
96 pub x: f32,
97 pub y: f32,
98 pub width: f32,
99 pub height: f32,
100}
101
102impl Bounds {
103 pub fn center(&self) -> (f32, f32) {
105 (self.x + self.width / 2.0, self.y + self.height / 2.0)
106 }
107}
108
109pub trait SemanticElementLike {
112 fn text(&self) -> Option<&str>;
113 fn role(&self) -> &str;
114 fn clickable(&self) -> bool;
115 fn bounds(&self) -> Bounds;
116 fn children(&self) -> &[Self]
117 where
118 Self: Sized;
119}
120
121pub fn find_text_center<E: SemanticElementLike>(elements: &[E], text: &str) -> Option<(f32, f32)> {
124 fn search<E: SemanticElementLike>(elem: &E, text: &str) -> Option<(f32, f32)> {
125 if let Some(t) = elem.text() {
126 if t.contains(text) {
127 return Some(elem.bounds().center());
128 }
129 }
130 for child in elem.children() {
131 if let Some(pos) = search(child, text) {
132 return Some(pos);
133 }
134 }
135 None
136 }
137
138 for elem in elements {
139 if let Some(pos) = search(elem, text) {
140 return Some(pos);
141 }
142 }
143 None
144}
145
146pub fn find_text_bounds<E: SemanticElementLike>(elements: &[E], text: &str) -> Option<Bounds> {
148 fn search<E: SemanticElementLike>(elem: &E, text: &str) -> Option<Bounds> {
149 if let Some(t) = elem.text() {
150 if t.contains(text) {
151 return Some(elem.bounds());
152 }
153 }
154 for child in elem.children() {
155 if let Some(bounds) = search(child, text) {
156 return Some(bounds);
157 }
158 }
159 None
160 }
161
162 for elem in elements {
163 if let Some(bounds) = search(elem, text) {
164 return Some(bounds);
165 }
166 }
167 None
168}
169
170pub fn find_button_bounds<E: SemanticElementLike>(elements: &[E], text: &str) -> Option<Bounds> {
173 fn has_text<E: SemanticElementLike>(elem: &E, text: &str) -> bool {
174 if let Some(t) = elem.text() {
175 if t.contains(text) {
176 return true;
177 }
178 }
179 elem.children().iter().any(|c| has_text(c, text))
180 }
181
182 fn search<E: SemanticElementLike>(elem: &E, text: &str) -> Option<Bounds> {
183 if elem.clickable() && has_text(elem, text) {
184 return Some(elem.bounds());
185 }
186 for child in elem.children() {
187 if let Some(bounds) = search(child, text) {
188 return Some(bounds);
189 }
190 }
191 None
192 }
193
194 for elem in elements {
195 if let Some(bounds) = search(elem, text) {
196 return Some(bounds);
197 }
198 }
199 None
200}
201
202pub fn find_elements_by_role<E: SemanticElementLike>(elements: &[E], role: &str) -> Vec<Bounds> {
204 fn search<E: SemanticElementLike>(elem: &E, role: &str, results: &mut Vec<Bounds>) {
205 if elem.role() == role {
206 results.push(elem.bounds());
207 }
208 for child in elem.children() {
209 search(child, role, results);
210 }
211 }
212
213 let mut results = Vec::new();
214 for elem in elements {
215 search(elem, role, &mut results);
216 }
217 results
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_approx_eq() {
226 assert_approx_eq(100.0, 100.0, 0.1, "exact match");
227 assert_approx_eq(100.05, 100.0, 0.1, "within tolerance");
228 }
229
230 #[test]
231 #[should_panic]
232 fn test_approx_eq_fails() {
233 assert_approx_eq(100.5, 100.0, 0.1, "should fail");
234 }
235
236 #[test]
237 fn test_rect_approx_eq() {
238 let rect1 = Rect {
239 x: 10.0,
240 y: 20.0,
241 width: 100.0,
242 height: 50.0,
243 };
244 let rect2 = Rect {
245 x: 10.05,
246 y: 20.05,
247 width: 100.05,
248 height: 50.05,
249 };
250 assert_rect_approx_eq(rect1, rect2, 0.1, "nearly equal rects");
251 }
252
253 #[test]
254 fn test_rect_contains_point() {
255 let rect = Rect {
256 x: 10.0,
257 y: 20.0,
258 width: 100.0,
259 height: 50.0,
260 };
261 assert_rect_contains_point(rect, 50.0, 30.0, "center point");
262 assert_rect_contains_point(rect, 10.0, 20.0, "top-left corner");
263 assert_rect_contains_point(rect, 110.0, 70.0, "bottom-right corner");
264 }
265
266 #[test]
267 fn test_contains_text() {
268 let texts = vec!["Hello".to_string(), "World".to_string()];
269 assert_contains_text(&texts, "Hello", "exact match");
270 assert_contains_text(&texts, "Wor", "partial match");
271 assert_not_contains_text(&texts, "Goodbye", "not present");
272 }
273
274 #[test]
275 fn test_count() {
276 let items = vec![1, 2, 3];
277 assert_count(&items, 3, "correct count");
278 }
279
280 #[test]
281 fn test_bounds_center() {
282 let bounds = Bounds {
283 x: 10.0,
284 y: 20.0,
285 width: 100.0,
286 height: 50.0,
287 };
288 let (cx, cy) = bounds.center();
289 assert_eq!(cx, 60.0);
290 assert_eq!(cy, 45.0);
291 }
292}