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