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::{
268 widget::LayoutResult, Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas,
269 Constraints, Size, TypeId,
270 };
271 use std::any::Any;
272 use std::time::Duration;
273
274 struct MockWidget {
276 test_id: Option<String>,
277 accessible_name: Option<String>,
278 children: Vec<Box<dyn Widget>>,
279 }
280
281 impl MockWidget {
282 fn new() -> Self {
283 Self {
284 test_id: None,
285 accessible_name: None,
286 children: Vec::new(),
287 }
288 }
289
290 fn with_test_id(mut self, id: &str) -> Self {
291 self.test_id = Some(id.to_string());
292 self
293 }
294
295 fn with_name(mut self, name: &str) -> Self {
296 self.accessible_name = Some(name.to_string());
297 self
298 }
299
300 fn with_child(mut self, child: MockWidget) -> Self {
301 self.children.push(Box::new(child));
302 self
303 }
304 }
305
306 impl Brick for MockWidget {
307 fn brick_name(&self) -> &'static str {
308 "MockWidget"
309 }
310
311 fn assertions(&self) -> &[BrickAssertion] {
312 &[]
313 }
314
315 fn budget(&self) -> BrickBudget {
316 BrickBudget::uniform(16)
317 }
318
319 fn verify(&self) -> BrickVerification {
320 BrickVerification {
321 passed: vec![],
322 failed: vec![],
323 verification_time: Duration::from_micros(1),
324 }
325 }
326
327 fn to_html(&self) -> String {
328 String::new()
329 }
330
331 fn to_css(&self) -> String {
332 String::new()
333 }
334 }
335
336 impl Widget for MockWidget {
337 fn type_id(&self) -> TypeId {
338 TypeId::of::<Self>()
339 }
340 fn measure(&self, c: Constraints) -> Size {
341 c.constrain(Size::new(100.0, 50.0))
342 }
343 fn layout(&mut self, b: Rect) -> LayoutResult {
344 LayoutResult { size: b.size() }
345 }
346 fn paint(&self, _: &mut dyn Canvas) {}
347 fn event(&mut self, _: &Event) -> Option<Box<dyn Any + Send>> {
348 None
349 }
350 fn children(&self) -> &[Box<dyn Widget>] {
351 &self.children
352 }
353 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
354 &mut self.children
355 }
356 fn test_id(&self) -> Option<&str> {
357 self.test_id.as_deref()
358 }
359 fn accessible_name(&self) -> Option<&str> {
360 self.accessible_name.as_deref()
361 }
362 }
363
364 #[test]
365 fn test_harness_exists() {
366 let widget = MockWidget::new().with_test_id("root");
367 let harness = Harness::new(widget);
368 assert!(harness.exists("[data-testid='root']"));
369 assert!(!harness.exists("[data-testid='nonexistent']"));
370 }
371
372 #[test]
373 fn test_harness_assert_exists() {
374 let widget = MockWidget::new().with_test_id("root");
375 let harness = Harness::new(widget);
376 harness.assert_exists("[data-testid='root']");
377 }
378
379 #[test]
380 #[should_panic(expected = "Expected widget matching")]
381 fn test_harness_assert_exists_fails() {
382 let widget = MockWidget::new();
383 let harness = Harness::new(widget);
384 harness.assert_exists("[data-testid='missing']");
385 }
386
387 #[test]
388 fn test_harness_text() {
389 let widget = MockWidget::new()
390 .with_test_id("greeting")
391 .with_name("Hello World");
392 let harness = Harness::new(widget);
393 assert_eq!(harness.text("[data-testid='greeting']"), "Hello World");
394 }
395
396 #[test]
397 fn test_harness_query_all() {
398 let widget = MockWidget::new()
399 .with_test_id("parent")
400 .with_child(MockWidget::new().with_test_id("child"))
401 .with_child(MockWidget::new().with_test_id("child"));
402
403 let harness = Harness::new(widget);
404 let children = harness.query_all("[data-testid='child']");
405 assert_eq!(children.len(), 2);
406 }
407
408 #[test]
409 fn test_harness_assert_count() {
410 let widget = MockWidget::new()
411 .with_child(MockWidget::new().with_test_id("item"))
412 .with_child(MockWidget::new().with_test_id("item"))
413 .with_child(MockWidget::new().with_test_id("item"));
414
415 let harness = Harness::new(widget);
416 harness.assert_count("[data-testid='item']", 3);
417 }
418
419 #[test]
424 fn test_harness_assert_not_exists() {
425 let widget = MockWidget::new().with_test_id("root");
426 let harness = Harness::new(widget);
427 harness.assert_not_exists("[data-testid='nonexistent']");
428 }
429
430 #[test]
431 #[should_panic(expected = "Expected widget matching")]
432 fn test_harness_assert_not_exists_fails() {
433 let widget = MockWidget::new().with_test_id("root");
434 let harness = Harness::new(widget);
435 harness.assert_not_exists("[data-testid='root']");
436 }
437
438 #[test]
443 fn test_harness_assert_text() {
444 let widget = MockWidget::new().with_test_id("label").with_name("Welcome");
445 let harness = Harness::new(widget);
446 harness.assert_text("[data-testid='label']", "Welcome");
447 }
448
449 #[test]
450 #[should_panic(expected = "Expected text")]
451 fn test_harness_assert_text_fails() {
452 let widget = MockWidget::new().with_test_id("label").with_name("Hello");
453 let harness = Harness::new(widget);
454 harness.assert_text("[data-testid='label']", "Goodbye");
455 }
456
457 #[test]
458 fn test_harness_assert_text_contains() {
459 let widget = MockWidget::new()
460 .with_test_id("message")
461 .with_name("Welcome to the app");
462 let harness = Harness::new(widget);
463 harness.assert_text_contains("[data-testid='message']", "Welcome");
464 harness.assert_text_contains("[data-testid='message']", "app");
465 }
466
467 #[test]
468 #[should_panic(expected = "Expected text")]
469 fn test_harness_assert_text_contains_fails() {
470 let widget = MockWidget::new()
471 .with_test_id("message")
472 .with_name("Hello World");
473 let harness = Harness::new(widget);
474 harness.assert_text_contains("[data-testid='message']", "Goodbye");
475 }
476
477 #[test]
482 fn test_harness_viewport() {
483 let widget = MockWidget::new();
484 let harness = Harness::new(widget).viewport(1920.0, 1080.0);
485 assert_eq!(harness.viewport.width, 1920.0);
486 assert_eq!(harness.viewport.height, 1080.0);
487 }
488
489 #[test]
490 fn test_harness_default_viewport() {
491 let widget = MockWidget::new();
492 let harness = Harness::new(widget);
493 assert_eq!(harness.viewport.width, 1280.0);
494 assert_eq!(harness.viewport.height, 720.0);
495 }
496
497 #[test]
502 fn test_harness_click() {
503 let widget = MockWidget::new().with_test_id("button");
504 let mut harness = Harness::new(widget);
505 harness.click("[data-testid='button']");
507 }
508
509 #[test]
510 fn test_harness_click_nonexistent() {
511 let widget = MockWidget::new();
512 let mut harness = Harness::new(widget);
513 harness.click("[data-testid='nonexistent']");
515 }
516
517 #[test]
518 fn test_harness_type_text() {
519 let widget = MockWidget::new().with_test_id("input");
520 let mut harness = Harness::new(widget);
521 harness.type_text("[data-testid='input']", "Hello World");
523 }
524
525 #[test]
526 fn test_harness_type_text_nonexistent() {
527 let widget = MockWidget::new();
528 let mut harness = Harness::new(widget);
529 harness.type_text("[data-testid='nonexistent']", "Hello");
531 }
532
533 #[test]
534 fn test_harness_press_key() {
535 let widget = MockWidget::new();
536 let mut harness = Harness::new(widget);
537 harness.press_key(Key::Enter);
539 harness.press_key(Key::Escape);
540 harness.press_key(Key::Tab);
541 }
542
543 #[test]
544 fn test_harness_scroll() {
545 let widget = MockWidget::new().with_test_id("list");
546 let mut harness = Harness::new(widget);
547 harness.scroll("[data-testid='list']", 100.0);
549 harness.scroll("[data-testid='list']", -50.0);
550 }
551
552 #[test]
553 fn test_harness_scroll_nonexistent() {
554 let widget = MockWidget::new();
555 let mut harness = Harness::new(widget);
556 harness.scroll("[data-testid='nonexistent']", 100.0);
558 }
559
560 #[test]
565 fn test_harness_query_returns_widget() {
566 let widget = MockWidget::new().with_test_id("root").with_name("Root");
567 let harness = Harness::new(widget);
568 let result = harness.query("[data-testid='root']");
569 assert!(result.is_some());
570 assert_eq!(result.unwrap().accessible_name(), Some("Root"));
571 }
572
573 #[test]
574 fn test_harness_query_returns_none() {
575 let widget = MockWidget::new();
576 let harness = Harness::new(widget);
577 let result = harness.query("[data-testid='missing']");
578 assert!(result.is_none());
579 }
580
581 #[test]
582 fn test_harness_query_nested() {
583 let widget = MockWidget::new().with_child(
584 MockWidget::new()
585 .with_test_id("nested")
586 .with_name("Nested Widget"),
587 );
588 let harness = Harness::new(widget);
589 let result = harness.query("[data-testid='nested']");
590 assert!(result.is_some());
591 assert_eq!(result.unwrap().accessible_name(), Some("Nested Widget"));
592 }
593
594 #[test]
595 fn test_harness_query_all_empty() {
596 let widget = MockWidget::new();
597 let harness = Harness::new(widget);
598 let results = harness.query_all("[data-testid='missing']");
599 assert!(results.is_empty());
600 }
601
602 #[test]
603 fn test_harness_query_all_nested() {
604 let widget = MockWidget::new()
605 .with_child(
606 MockWidget::new()
607 .with_test_id("item")
608 .with_child(MockWidget::new().with_test_id("item")),
609 )
610 .with_child(MockWidget::new().with_test_id("item"));
611
612 let harness = Harness::new(widget);
613 let results = harness.query_all("[data-testid='item']");
614 assert_eq!(results.len(), 3);
615 }
616
617 #[test]
622 fn test_harness_tick() {
623 let widget = MockWidget::new();
624 let mut harness = Harness::new(widget);
625 harness.tick(100);
627 harness.tick(1000);
628 }
629
630 #[test]
635 fn test_harness_text_empty() {
636 let widget = MockWidget::new().with_test_id("empty");
637 let harness = Harness::new(widget);
638 assert_eq!(harness.text("[data-testid='empty']"), "");
639 }
640
641 #[test]
642 fn test_harness_text_nonexistent() {
643 let widget = MockWidget::new();
644 let harness = Harness::new(widget);
645 assert_eq!(harness.text("[data-testid='missing']"), "");
646 }
647
648 #[test]
653 fn test_harness_method_chaining() {
654 let widget = MockWidget::new()
655 .with_test_id("form")
656 .with_child(MockWidget::new().with_test_id("input"))
657 .with_child(MockWidget::new().with_test_id("submit"));
658
659 let mut harness = Harness::new(widget);
660
661 harness
663 .click("[data-testid='input']")
664 .type_text("[data-testid='input']", "user@example.com")
665 .press_key(Key::Tab)
666 .click("[data-testid='submit']");
667
668 harness
670 .assert_exists("[data-testid='form']")
671 .assert_exists("[data-testid='input']")
672 .assert_exists("[data-testid='submit']");
673 }
674
675 #[test]
680 fn test_harness_assert_count_zero() {
681 let widget = MockWidget::new();
682 let harness = Harness::new(widget);
683 harness.assert_count("[data-testid='missing']", 0);
684 }
685
686 #[test]
687 #[should_panic(expected = "Expected")]
688 fn test_harness_assert_count_fails() {
689 let widget = MockWidget::new()
690 .with_child(MockWidget::new().with_test_id("item"))
691 .with_child(MockWidget::new().with_test_id("item"));
692
693 let harness = Harness::new(widget);
694 harness.assert_count("[data-testid='item']", 5);
695 }
696}