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 {
79 key,
80 modifiers: Default::default(),
81 });
82 self.event_queue.push_back(Event::KeyUp {
83 key,
84 modifiers: Default::default(),
85 });
86 self.process_events();
87 self
88 }
89
90 pub fn scroll(&mut self, selector: &str, delta: f32) -> &mut Self {
92 if self.query(selector).is_some() {
93 self.event_queue.push_back(Event::Scroll {
94 delta_x: 0.0,
95 delta_y: delta,
96 });
97 self.process_events();
98 }
99 self
100 }
101
102 #[must_use]
106 pub fn query(&self, selector: &str) -> Option<&dyn Widget> {
107 let sel = Selector::parse(selector).ok()?;
108 self.find_widget(&*self.root, &sel)
109 }
110
111 #[must_use]
113 pub fn query_all(&self, selector: &str) -> Vec<&dyn Widget> {
114 let Ok(sel) = Selector::parse(selector) else {
115 return Vec::new();
116 };
117 let mut results = Vec::new();
118 self.find_all_widgets(&*self.root, &sel, &mut results);
119 results
120 }
121
122 #[must_use]
124 pub fn text(&self, selector: &str) -> String {
125 if let Some(widget) = self.query(selector) {
127 if let Some(name) = widget.accessible_name() {
128 return name.to_string();
129 }
130 }
131 String::new()
132 }
133
134 #[must_use]
136 pub fn exists(&self, selector: &str) -> bool {
137 self.query(selector).is_some()
138 }
139
140 pub fn assert_exists(&self, selector: &str) -> &Self {
148 assert!(
149 self.exists(selector),
150 "Expected widget matching '{selector}' to exist"
151 );
152 self
153 }
154
155 pub fn assert_not_exists(&self, selector: &str) -> &Self {
161 assert!(
162 !self.exists(selector),
163 "Expected widget matching '{selector}' to not exist"
164 );
165 self
166 }
167
168 pub fn assert_text(&self, selector: &str, expected: &str) -> &Self {
174 let actual = self.text(selector);
175 assert_eq!(
176 actual, expected,
177 "Expected text '{expected}' but got '{actual}' for '{selector}'"
178 );
179 self
180 }
181
182 pub fn assert_text_contains(&self, selector: &str, substring: &str) -> &Self {
188 let actual = self.text(selector);
189 assert!(
190 actual.contains(substring),
191 "Expected text for '{selector}' to contain '{substring}' but got '{actual}'"
192 );
193 self
194 }
195
196 pub fn assert_count(&self, selector: &str, expected: usize) -> &Self {
202 let actual = self.query_all(selector).len();
203 assert_eq!(
204 actual, expected,
205 "Expected {expected} widgets matching '{selector}' but found {actual}"
206 );
207 self
208 }
209
210 fn process_events(&mut self) {
213 while let Some(event) = self.event_queue.pop_front() {
214 self.root.event(&event);
215 }
216 }
217
218 #[allow(unknown_lints)]
219 #[allow(clippy::only_used_in_recursion, clippy::self_only_used_in_recursion)]
220 fn find_widget<'a>(
221 &'a self,
222 widget: &'a dyn Widget,
223 selector: &Selector,
224 ) -> Option<&'a dyn Widget> {
225 if selector.matches(widget) {
226 return Some(widget);
227 }
228
229 for child in widget.children() {
230 if let Some(found) = self.find_widget(child.as_ref(), selector) {
231 return Some(found);
232 }
233 }
234
235 None
236 }
237
238 #[allow(unknown_lints)]
239 #[allow(clippy::only_used_in_recursion, clippy::self_only_used_in_recursion)]
240 fn find_all_widgets<'a>(
241 &'a self,
242 widget: &'a dyn Widget,
243 selector: &Selector,
244 results: &mut Vec<&'a dyn Widget>,
245 ) {
246 if selector.matches(widget) {
247 results.push(widget);
248 }
249
250 for child in widget.children() {
251 self.find_all_widgets(child.as_ref(), selector, results);
252 }
253 }
254
255 fn query_bounds(&self, selector: &str) -> Option<Rect> {
256 if self.exists(selector) {
258 Some(Rect::new(0.0, 0.0, 100.0, 50.0))
259 } else {
260 None
261 }
262 }
263
264 pub fn tick(&mut self, _ms: u64) {
266 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use presentar_core::{
274 widget::LayoutResult, Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas,
275 Constraints, Size, TypeId,
276 };
277 use std::any::Any;
278 use std::time::Duration;
279
280 struct MockWidget {
282 test_id: Option<String>,
283 accessible_name: Option<String>,
284 children: Vec<Box<dyn Widget>>,
285 }
286
287 impl MockWidget {
288 fn new() -> Self {
289 Self {
290 test_id: None,
291 accessible_name: None,
292 children: Vec::new(),
293 }
294 }
295
296 fn with_test_id(mut self, id: &str) -> Self {
297 self.test_id = Some(id.to_string());
298 self
299 }
300
301 fn with_name(mut self, name: &str) -> Self {
302 self.accessible_name = Some(name.to_string());
303 self
304 }
305
306 fn with_child(mut self, child: MockWidget) -> Self {
307 self.children.push(Box::new(child));
308 self
309 }
310 }
311
312 impl Brick for MockWidget {
313 fn brick_name(&self) -> &'static str {
314 "MockWidget"
315 }
316
317 fn assertions(&self) -> &[BrickAssertion] {
318 &[]
319 }
320
321 fn budget(&self) -> BrickBudget {
322 BrickBudget::uniform(16)
323 }
324
325 fn verify(&self) -> BrickVerification {
326 BrickVerification {
327 passed: vec![],
328 failed: vec![],
329 verification_time: Duration::from_micros(1),
330 }
331 }
332
333 fn to_html(&self) -> String {
334 String::new()
335 }
336
337 fn to_css(&self) -> String {
338 String::new()
339 }
340 }
341
342 impl Widget for MockWidget {
343 fn type_id(&self) -> TypeId {
344 TypeId::of::<Self>()
345 }
346 fn measure(&self, c: Constraints) -> Size {
347 c.constrain(Size::new(100.0, 50.0))
348 }
349 fn layout(&mut self, b: Rect) -> LayoutResult {
350 LayoutResult { size: b.size() }
351 }
352 fn paint(&self, _: &mut dyn Canvas) {}
353 fn event(&mut self, _: &Event) -> Option<Box<dyn Any + Send>> {
354 None
355 }
356 fn children(&self) -> &[Box<dyn Widget>] {
357 &self.children
358 }
359 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
360 &mut self.children
361 }
362 fn test_id(&self) -> Option<&str> {
363 self.test_id.as_deref()
364 }
365 fn accessible_name(&self) -> Option<&str> {
366 self.accessible_name.as_deref()
367 }
368 }
369
370 #[test]
371 fn test_harness_exists() {
372 let widget = MockWidget::new().with_test_id("root");
373 let harness = Harness::new(widget);
374 assert!(harness.exists("[data-testid='root']"));
375 assert!(!harness.exists("[data-testid='nonexistent']"));
376 }
377
378 #[test]
379 fn test_harness_assert_exists() {
380 let widget = MockWidget::new().with_test_id("root");
381 let harness = Harness::new(widget);
382 harness.assert_exists("[data-testid='root']");
383 }
384
385 #[test]
386 #[should_panic(expected = "Expected widget matching")]
387 fn test_harness_assert_exists_fails() {
388 let widget = MockWidget::new();
389 let harness = Harness::new(widget);
390 harness.assert_exists("[data-testid='missing']");
391 }
392
393 #[test]
394 fn test_harness_text() {
395 let widget = MockWidget::new()
396 .with_test_id("greeting")
397 .with_name("Hello World");
398 let harness = Harness::new(widget);
399 assert_eq!(harness.text("[data-testid='greeting']"), "Hello World");
400 }
401
402 #[test]
403 fn test_harness_query_all() {
404 let widget = MockWidget::new()
405 .with_test_id("parent")
406 .with_child(MockWidget::new().with_test_id("child"))
407 .with_child(MockWidget::new().with_test_id("child"));
408
409 let harness = Harness::new(widget);
410 let children = harness.query_all("[data-testid='child']");
411 assert_eq!(children.len(), 2);
412 }
413
414 #[test]
415 fn test_harness_assert_count() {
416 let widget = MockWidget::new()
417 .with_child(MockWidget::new().with_test_id("item"))
418 .with_child(MockWidget::new().with_test_id("item"))
419 .with_child(MockWidget::new().with_test_id("item"));
420
421 let harness = Harness::new(widget);
422 harness.assert_count("[data-testid='item']", 3);
423 }
424
425 #[test]
430 fn test_harness_assert_not_exists() {
431 let widget = MockWidget::new().with_test_id("root");
432 let harness = Harness::new(widget);
433 harness.assert_not_exists("[data-testid='nonexistent']");
434 }
435
436 #[test]
437 #[should_panic(expected = "Expected widget matching")]
438 fn test_harness_assert_not_exists_fails() {
439 let widget = MockWidget::new().with_test_id("root");
440 let harness = Harness::new(widget);
441 harness.assert_not_exists("[data-testid='root']");
442 }
443
444 #[test]
449 fn test_harness_assert_text() {
450 let widget = MockWidget::new().with_test_id("label").with_name("Welcome");
451 let harness = Harness::new(widget);
452 harness.assert_text("[data-testid='label']", "Welcome");
453 }
454
455 #[test]
456 #[should_panic(expected = "Expected text")]
457 fn test_harness_assert_text_fails() {
458 let widget = MockWidget::new().with_test_id("label").with_name("Hello");
459 let harness = Harness::new(widget);
460 harness.assert_text("[data-testid='label']", "Goodbye");
461 }
462
463 #[test]
464 fn test_harness_assert_text_contains() {
465 let widget = MockWidget::new()
466 .with_test_id("message")
467 .with_name("Welcome to the app");
468 let harness = Harness::new(widget);
469 harness.assert_text_contains("[data-testid='message']", "Welcome");
470 harness.assert_text_contains("[data-testid='message']", "app");
471 }
472
473 #[test]
474 #[should_panic(expected = "Expected text")]
475 fn test_harness_assert_text_contains_fails() {
476 let widget = MockWidget::new()
477 .with_test_id("message")
478 .with_name("Hello World");
479 let harness = Harness::new(widget);
480 harness.assert_text_contains("[data-testid='message']", "Goodbye");
481 }
482
483 #[test]
488 fn test_harness_viewport() {
489 let widget = MockWidget::new();
490 let harness = Harness::new(widget).viewport(1920.0, 1080.0);
491 assert_eq!(harness.viewport.width, 1920.0);
492 assert_eq!(harness.viewport.height, 1080.0);
493 }
494
495 #[test]
496 fn test_harness_default_viewport() {
497 let widget = MockWidget::new();
498 let harness = Harness::new(widget);
499 assert_eq!(harness.viewport.width, 1280.0);
500 assert_eq!(harness.viewport.height, 720.0);
501 }
502
503 #[test]
508 fn test_harness_click() {
509 let widget = MockWidget::new().with_test_id("button");
510 let mut harness = Harness::new(widget);
511 harness.click("[data-testid='button']");
513 }
514
515 #[test]
516 fn test_harness_click_nonexistent() {
517 let widget = MockWidget::new();
518 let mut harness = Harness::new(widget);
519 harness.click("[data-testid='nonexistent']");
521 }
522
523 #[test]
524 fn test_harness_type_text() {
525 let widget = MockWidget::new().with_test_id("input");
526 let mut harness = Harness::new(widget);
527 harness.type_text("[data-testid='input']", "Hello World");
529 }
530
531 #[test]
532 fn test_harness_type_text_nonexistent() {
533 let widget = MockWidget::new();
534 let mut harness = Harness::new(widget);
535 harness.type_text("[data-testid='nonexistent']", "Hello");
537 }
538
539 #[test]
540 fn test_harness_press_key() {
541 let widget = MockWidget::new();
542 let mut harness = Harness::new(widget);
543 harness.press_key(Key::Enter);
545 harness.press_key(Key::Escape);
546 harness.press_key(Key::Tab);
547 }
548
549 #[test]
550 fn test_harness_scroll() {
551 let widget = MockWidget::new().with_test_id("list");
552 let mut harness = Harness::new(widget);
553 harness.scroll("[data-testid='list']", 100.0);
555 harness.scroll("[data-testid='list']", -50.0);
556 }
557
558 #[test]
559 fn test_harness_scroll_nonexistent() {
560 let widget = MockWidget::new();
561 let mut harness = Harness::new(widget);
562 harness.scroll("[data-testid='nonexistent']", 100.0);
564 }
565
566 #[test]
571 fn test_harness_query_returns_widget() {
572 let widget = MockWidget::new().with_test_id("root").with_name("Root");
573 let harness = Harness::new(widget);
574 let result = harness.query("[data-testid='root']");
575 assert!(result.is_some());
576 assert_eq!(result.unwrap().accessible_name(), Some("Root"));
577 }
578
579 #[test]
580 fn test_harness_query_returns_none() {
581 let widget = MockWidget::new();
582 let harness = Harness::new(widget);
583 let result = harness.query("[data-testid='missing']");
584 assert!(result.is_none());
585 }
586
587 #[test]
588 fn test_harness_query_nested() {
589 let widget = MockWidget::new().with_child(
590 MockWidget::new()
591 .with_test_id("nested")
592 .with_name("Nested Widget"),
593 );
594 let harness = Harness::new(widget);
595 let result = harness.query("[data-testid='nested']");
596 assert!(result.is_some());
597 assert_eq!(result.unwrap().accessible_name(), Some("Nested Widget"));
598 }
599
600 #[test]
601 fn test_harness_query_all_empty() {
602 let widget = MockWidget::new();
603 let harness = Harness::new(widget);
604 let results = harness.query_all("[data-testid='missing']");
605 assert!(results.is_empty());
606 }
607
608 #[test]
609 fn test_harness_query_all_nested() {
610 let widget = MockWidget::new()
611 .with_child(
612 MockWidget::new()
613 .with_test_id("item")
614 .with_child(MockWidget::new().with_test_id("item")),
615 )
616 .with_child(MockWidget::new().with_test_id("item"));
617
618 let harness = Harness::new(widget);
619 let results = harness.query_all("[data-testid='item']");
620 assert_eq!(results.len(), 3);
621 }
622
623 #[test]
628 fn test_harness_tick() {
629 let widget = MockWidget::new();
630 let mut harness = Harness::new(widget);
631 harness.tick(100);
633 harness.tick(1000);
634 }
635
636 #[test]
641 fn test_harness_text_empty() {
642 let widget = MockWidget::new().with_test_id("empty");
643 let harness = Harness::new(widget);
644 assert_eq!(harness.text("[data-testid='empty']"), "");
645 }
646
647 #[test]
648 fn test_harness_text_nonexistent() {
649 let widget = MockWidget::new();
650 let harness = Harness::new(widget);
651 assert_eq!(harness.text("[data-testid='missing']"), "");
652 }
653
654 #[test]
659 fn test_harness_method_chaining() {
660 let widget = MockWidget::new()
661 .with_test_id("form")
662 .with_child(MockWidget::new().with_test_id("input"))
663 .with_child(MockWidget::new().with_test_id("submit"));
664
665 let mut harness = Harness::new(widget);
666
667 harness
669 .click("[data-testid='input']")
670 .type_text("[data-testid='input']", "user@example.com")
671 .press_key(Key::Tab)
672 .click("[data-testid='submit']");
673
674 harness
676 .assert_exists("[data-testid='form']")
677 .assert_exists("[data-testid='input']")
678 .assert_exists("[data-testid='submit']");
679 }
680
681 #[test]
686 fn test_harness_assert_count_zero() {
687 let widget = MockWidget::new();
688 let harness = Harness::new(widget);
689 harness.assert_count("[data-testid='missing']", 0);
690 }
691
692 #[test]
693 #[should_panic(expected = "Expected")]
694 fn test_harness_assert_count_fails() {
695 let widget = MockWidget::new()
696 .with_child(MockWidget::new().with_test_id("item"))
697 .with_child(MockWidget::new().with_test_id("item"));
698
699 let harness = Harness::new(widget);
700 harness.assert_count("[data-testid='item']", 5);
701 }
702}