1use presentar_core::{Event, Key, MouseButton, Rect, Widget};
6use std::collections::VecDeque;
7
8use crate::selector::Selector;
9
10pub struct Harness {
12 root: Box<dyn Widget>,
14 event_queue: VecDeque<Event>,
16 viewport: Rect,
18}
19
20impl Harness {
21 pub fn new(root: impl Widget + 'static) -> Self {
23 Self {
24 root: Box::new(root),
25 event_queue: VecDeque::new(),
26 viewport: Rect::new(0.0, 0.0, 1280.0, 720.0),
27 }
28 }
29
30 #[must_use]
32 pub const fn viewport(mut self, width: f32, height: f32) -> Self {
33 self.viewport = Rect::new(0.0, 0.0, width, height);
34 self
35 }
36
37 pub fn click(&mut self, selector: &str) -> &mut Self {
41 if let Some(bounds) = self.query_bounds(selector) {
42 let center = bounds.center();
43 self.event_queue
44 .push_back(Event::MouseMove { position: center });
45 self.event_queue.push_back(Event::MouseDown {
46 position: center,
47 button: MouseButton::Left,
48 });
49 self.event_queue.push_back(Event::MouseUp {
50 position: center,
51 button: MouseButton::Left,
52 });
53 self.process_events();
54 }
55 self
56 }
57
58 pub fn type_text(&mut self, selector: &str, text: &str) -> &mut Self {
60 if self.query(selector).is_some() {
61 self.event_queue.push_back(Event::FocusIn);
63
64 for c in text.chars() {
66 self.event_queue.push_back(Event::TextInput {
67 text: c.to_string(),
68 });
69 }
70
71 self.process_events();
72 }
73 self
74 }
75
76 pub fn press_key(&mut self, key: Key) -> &mut Self {
78 self.event_queue.push_back(Event::KeyDown { key });
79 self.event_queue.push_back(Event::KeyUp { key });
80 self.process_events();
81 self
82 }
83
84 pub fn scroll(&mut self, selector: &str, delta: f32) -> &mut Self {
86 if self.query(selector).is_some() {
87 self.event_queue.push_back(Event::Scroll {
88 delta_x: 0.0,
89 delta_y: delta,
90 });
91 self.process_events();
92 }
93 self
94 }
95
96 #[must_use]
100 pub fn query(&self, selector: &str) -> Option<&dyn Widget> {
101 let sel = Selector::parse(selector).ok()?;
102 self.find_widget(&*self.root, &sel)
103 }
104
105 #[must_use]
107 pub fn query_all(&self, selector: &str) -> Vec<&dyn Widget> {
108 let Ok(sel) = Selector::parse(selector) else {
109 return Vec::new();
110 };
111 let mut results = Vec::new();
112 self.find_all_widgets(&*self.root, &sel, &mut results);
113 results
114 }
115
116 #[must_use]
118 pub fn text(&self, selector: &str) -> String {
119 if let Some(widget) = self.query(selector) {
121 if let Some(name) = widget.accessible_name() {
122 return name.to_string();
123 }
124 }
125 String::new()
126 }
127
128 #[must_use]
130 pub fn exists(&self, selector: &str) -> bool {
131 self.query(selector).is_some()
132 }
133
134 pub fn assert_exists(&self, selector: &str) -> &Self {
142 assert!(
143 self.exists(selector),
144 "Expected widget matching '{selector}' to exist"
145 );
146 self
147 }
148
149 pub fn assert_not_exists(&self, selector: &str) -> &Self {
155 assert!(
156 !self.exists(selector),
157 "Expected widget matching '{selector}' to not exist"
158 );
159 self
160 }
161
162 pub fn assert_text(&self, selector: &str, expected: &str) -> &Self {
168 let actual = self.text(selector);
169 assert_eq!(
170 actual, expected,
171 "Expected text '{expected}' but got '{actual}' for '{selector}'"
172 );
173 self
174 }
175
176 pub fn assert_text_contains(&self, selector: &str, substring: &str) -> &Self {
182 let actual = self.text(selector);
183 assert!(
184 actual.contains(substring),
185 "Expected text for '{selector}' to contain '{substring}' but got '{actual}'"
186 );
187 self
188 }
189
190 pub fn assert_count(&self, selector: &str, expected: usize) -> &Self {
196 let actual = self.query_all(selector).len();
197 assert_eq!(
198 actual, expected,
199 "Expected {expected} widgets matching '{selector}' but found {actual}"
200 );
201 self
202 }
203
204 fn process_events(&mut self) {
207 while let Some(event) = self.event_queue.pop_front() {
208 self.root.event(&event);
209 }
210 }
211
212 #[allow(unknown_lints)]
213 #[allow(clippy::only_used_in_recursion, clippy::self_only_used_in_recursion)]
214 fn find_widget<'a>(
215 &'a self,
216 widget: &'a dyn Widget,
217 selector: &Selector,
218 ) -> Option<&'a dyn Widget> {
219 if selector.matches(widget) {
220 return Some(widget);
221 }
222
223 for child in widget.children() {
224 if let Some(found) = self.find_widget(child.as_ref(), selector) {
225 return Some(found);
226 }
227 }
228
229 None
230 }
231
232 #[allow(unknown_lints)]
233 #[allow(clippy::only_used_in_recursion, clippy::self_only_used_in_recursion)]
234 fn find_all_widgets<'a>(
235 &'a self,
236 widget: &'a dyn Widget,
237 selector: &Selector,
238 results: &mut Vec<&'a dyn Widget>,
239 ) {
240 if selector.matches(widget) {
241 results.push(widget);
242 }
243
244 for child in widget.children() {
245 self.find_all_widgets(child.as_ref(), selector, results);
246 }
247 }
248
249 fn query_bounds(&self, selector: &str) -> Option<Rect> {
250 if self.exists(selector) {
252 Some(Rect::new(0.0, 0.0, 100.0, 50.0))
253 } else {
254 None
255 }
256 }
257
258 pub fn tick(&mut self, _ms: u64) {
260 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use presentar_core::{widget::LayoutResult, Canvas, Constraints, Size, TypeId};
268 use std::any::Any;
269
270 struct MockWidget {
272 test_id: Option<String>,
273 accessible_name: Option<String>,
274 children: Vec<Box<dyn Widget>>,
275 }
276
277 impl MockWidget {
278 fn new() -> Self {
279 Self {
280 test_id: None,
281 accessible_name: None,
282 children: Vec::new(),
283 }
284 }
285
286 fn with_test_id(mut self, id: &str) -> Self {
287 self.test_id = Some(id.to_string());
288 self
289 }
290
291 fn with_name(mut self, name: &str) -> Self {
292 self.accessible_name = Some(name.to_string());
293 self
294 }
295
296 fn with_child(mut self, child: MockWidget) -> Self {
297 self.children.push(Box::new(child));
298 self
299 }
300 }
301
302 impl Widget for MockWidget {
303 fn type_id(&self) -> TypeId {
304 TypeId::of::<Self>()
305 }
306 fn measure(&self, c: Constraints) -> Size {
307 c.constrain(Size::new(100.0, 50.0))
308 }
309 fn layout(&mut self, b: Rect) -> LayoutResult {
310 LayoutResult { size: b.size() }
311 }
312 fn paint(&self, _: &mut dyn Canvas) {}
313 fn event(&mut self, _: &Event) -> Option<Box<dyn Any + Send>> {
314 None
315 }
316 fn children(&self) -> &[Box<dyn Widget>] {
317 &self.children
318 }
319 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
320 &mut self.children
321 }
322 fn test_id(&self) -> Option<&str> {
323 self.test_id.as_deref()
324 }
325 fn accessible_name(&self) -> Option<&str> {
326 self.accessible_name.as_deref()
327 }
328 }
329
330 #[test]
331 fn test_harness_exists() {
332 let widget = MockWidget::new().with_test_id("root");
333 let harness = Harness::new(widget);
334 assert!(harness.exists("[data-testid='root']"));
335 assert!(!harness.exists("[data-testid='nonexistent']"));
336 }
337
338 #[test]
339 fn test_harness_assert_exists() {
340 let widget = MockWidget::new().with_test_id("root");
341 let harness = Harness::new(widget);
342 harness.assert_exists("[data-testid='root']");
343 }
344
345 #[test]
346 #[should_panic(expected = "Expected widget matching")]
347 fn test_harness_assert_exists_fails() {
348 let widget = MockWidget::new();
349 let harness = Harness::new(widget);
350 harness.assert_exists("[data-testid='missing']");
351 }
352
353 #[test]
354 fn test_harness_text() {
355 let widget = MockWidget::new()
356 .with_test_id("greeting")
357 .with_name("Hello World");
358 let harness = Harness::new(widget);
359 assert_eq!(harness.text("[data-testid='greeting']"), "Hello World");
360 }
361
362 #[test]
363 fn test_harness_query_all() {
364 let widget = MockWidget::new()
365 .with_test_id("parent")
366 .with_child(MockWidget::new().with_test_id("child"))
367 .with_child(MockWidget::new().with_test_id("child"));
368
369 let harness = Harness::new(widget);
370 let children = harness.query_all("[data-testid='child']");
371 assert_eq!(children.len(), 2);
372 }
373
374 #[test]
375 fn test_harness_assert_count() {
376 let widget = MockWidget::new()
377 .with_child(MockWidget::new().with_test_id("item"))
378 .with_child(MockWidget::new().with_test_id("item"))
379 .with_child(MockWidget::new().with_test_id("item"));
380
381 let harness = Harness::new(widget);
382 harness.assert_count("[data-testid='item']", 3);
383 }
384
385 #[test]
390 fn test_harness_assert_not_exists() {
391 let widget = MockWidget::new().with_test_id("root");
392 let harness = Harness::new(widget);
393 harness.assert_not_exists("[data-testid='nonexistent']");
394 }
395
396 #[test]
397 #[should_panic(expected = "Expected widget matching")]
398 fn test_harness_assert_not_exists_fails() {
399 let widget = MockWidget::new().with_test_id("root");
400 let harness = Harness::new(widget);
401 harness.assert_not_exists("[data-testid='root']");
402 }
403
404 #[test]
409 fn test_harness_assert_text() {
410 let widget = MockWidget::new().with_test_id("label").with_name("Welcome");
411 let harness = Harness::new(widget);
412 harness.assert_text("[data-testid='label']", "Welcome");
413 }
414
415 #[test]
416 #[should_panic(expected = "Expected text")]
417 fn test_harness_assert_text_fails() {
418 let widget = MockWidget::new().with_test_id("label").with_name("Hello");
419 let harness = Harness::new(widget);
420 harness.assert_text("[data-testid='label']", "Goodbye");
421 }
422
423 #[test]
424 fn test_harness_assert_text_contains() {
425 let widget = MockWidget::new()
426 .with_test_id("message")
427 .with_name("Welcome to the app");
428 let harness = Harness::new(widget);
429 harness.assert_text_contains("[data-testid='message']", "Welcome");
430 harness.assert_text_contains("[data-testid='message']", "app");
431 }
432
433 #[test]
434 #[should_panic(expected = "Expected text")]
435 fn test_harness_assert_text_contains_fails() {
436 let widget = MockWidget::new()
437 .with_test_id("message")
438 .with_name("Hello World");
439 let harness = Harness::new(widget);
440 harness.assert_text_contains("[data-testid='message']", "Goodbye");
441 }
442
443 #[test]
448 fn test_harness_viewport() {
449 let widget = MockWidget::new();
450 let harness = Harness::new(widget).viewport(1920.0, 1080.0);
451 assert_eq!(harness.viewport.width, 1920.0);
452 assert_eq!(harness.viewport.height, 1080.0);
453 }
454
455 #[test]
456 fn test_harness_default_viewport() {
457 let widget = MockWidget::new();
458 let harness = Harness::new(widget);
459 assert_eq!(harness.viewport.width, 1280.0);
460 assert_eq!(harness.viewport.height, 720.0);
461 }
462
463 #[test]
468 fn test_harness_click() {
469 let widget = MockWidget::new().with_test_id("button");
470 let mut harness = Harness::new(widget);
471 harness.click("[data-testid='button']");
473 }
474
475 #[test]
476 fn test_harness_click_nonexistent() {
477 let widget = MockWidget::new();
478 let mut harness = Harness::new(widget);
479 harness.click("[data-testid='nonexistent']");
481 }
482
483 #[test]
484 fn test_harness_type_text() {
485 let widget = MockWidget::new().with_test_id("input");
486 let mut harness = Harness::new(widget);
487 harness.type_text("[data-testid='input']", "Hello World");
489 }
490
491 #[test]
492 fn test_harness_type_text_nonexistent() {
493 let widget = MockWidget::new();
494 let mut harness = Harness::new(widget);
495 harness.type_text("[data-testid='nonexistent']", "Hello");
497 }
498
499 #[test]
500 fn test_harness_press_key() {
501 let widget = MockWidget::new();
502 let mut harness = Harness::new(widget);
503 harness.press_key(Key::Enter);
505 harness.press_key(Key::Escape);
506 harness.press_key(Key::Tab);
507 }
508
509 #[test]
510 fn test_harness_scroll() {
511 let widget = MockWidget::new().with_test_id("list");
512 let mut harness = Harness::new(widget);
513 harness.scroll("[data-testid='list']", 100.0);
515 harness.scroll("[data-testid='list']", -50.0);
516 }
517
518 #[test]
519 fn test_harness_scroll_nonexistent() {
520 let widget = MockWidget::new();
521 let mut harness = Harness::new(widget);
522 harness.scroll("[data-testid='nonexistent']", 100.0);
524 }
525
526 #[test]
531 fn test_harness_query_returns_widget() {
532 let widget = MockWidget::new().with_test_id("root").with_name("Root");
533 let harness = Harness::new(widget);
534 let result = harness.query("[data-testid='root']");
535 assert!(result.is_some());
536 assert_eq!(result.unwrap().accessible_name(), Some("Root"));
537 }
538
539 #[test]
540 fn test_harness_query_returns_none() {
541 let widget = MockWidget::new();
542 let harness = Harness::new(widget);
543 let result = harness.query("[data-testid='missing']");
544 assert!(result.is_none());
545 }
546
547 #[test]
548 fn test_harness_query_nested() {
549 let widget = MockWidget::new().with_child(
550 MockWidget::new()
551 .with_test_id("nested")
552 .with_name("Nested Widget"),
553 );
554 let harness = Harness::new(widget);
555 let result = harness.query("[data-testid='nested']");
556 assert!(result.is_some());
557 assert_eq!(result.unwrap().accessible_name(), Some("Nested Widget"));
558 }
559
560 #[test]
561 fn test_harness_query_all_empty() {
562 let widget = MockWidget::new();
563 let harness = Harness::new(widget);
564 let results = harness.query_all("[data-testid='missing']");
565 assert!(results.is_empty());
566 }
567
568 #[test]
569 fn test_harness_query_all_nested() {
570 let widget = MockWidget::new()
571 .with_child(
572 MockWidget::new()
573 .with_test_id("item")
574 .with_child(MockWidget::new().with_test_id("item")),
575 )
576 .with_child(MockWidget::new().with_test_id("item"));
577
578 let harness = Harness::new(widget);
579 let results = harness.query_all("[data-testid='item']");
580 assert_eq!(results.len(), 3);
581 }
582
583 #[test]
588 fn test_harness_tick() {
589 let widget = MockWidget::new();
590 let mut harness = Harness::new(widget);
591 harness.tick(100);
593 harness.tick(1000);
594 }
595
596 #[test]
601 fn test_harness_text_empty() {
602 let widget = MockWidget::new().with_test_id("empty");
603 let harness = Harness::new(widget);
604 assert_eq!(harness.text("[data-testid='empty']"), "");
605 }
606
607 #[test]
608 fn test_harness_text_nonexistent() {
609 let widget = MockWidget::new();
610 let harness = Harness::new(widget);
611 assert_eq!(harness.text("[data-testid='missing']"), "");
612 }
613
614 #[test]
619 fn test_harness_method_chaining() {
620 let widget = MockWidget::new()
621 .with_test_id("form")
622 .with_child(MockWidget::new().with_test_id("input"))
623 .with_child(MockWidget::new().with_test_id("submit"));
624
625 let mut harness = Harness::new(widget);
626
627 harness
629 .click("[data-testid='input']")
630 .type_text("[data-testid='input']", "user@example.com")
631 .press_key(Key::Tab)
632 .click("[data-testid='submit']");
633
634 harness
636 .assert_exists("[data-testid='form']")
637 .assert_exists("[data-testid='input']")
638 .assert_exists("[data-testid='submit']");
639 }
640
641 #[test]
646 fn test_harness_assert_count_zero() {
647 let widget = MockWidget::new();
648 let harness = Harness::new(widget);
649 harness.assert_count("[data-testid='missing']", 0);
650 }
651
652 #[test]
653 #[should_panic(expected = "Expected")]
654 fn test_harness_assert_count_fails() {
655 let widget = MockWidget::new()
656 .with_child(MockWidget::new().with_test_id("item"))
657 .with_child(MockWidget::new().with_test_id("item"));
658
659 let harness = Harness::new(widget);
660 harness.assert_count("[data-testid='item']", 5);
661 }
662}