Skip to main content

presentar_test/
harness.rs

1//! Test harness for Presentar applications.
2//!
3//! Zero external dependencies - pure Rust testing.
4
5use presentar_core::{Event, Key, MouseButton, Rect, Widget};
6use std::collections::VecDeque;
7
8use crate::selector::Selector;
9
10/// Test harness for interacting with Presentar widgets.
11pub struct Harness {
12    /// Root widget being tested
13    root: Box<dyn Widget>,
14    /// Event queue for simulation
15    event_queue: VecDeque<Event>,
16    /// Current viewport size
17    viewport: Rect,
18}
19
20impl Harness {
21    /// Create a new harness with a root widget.
22    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    /// Set the viewport size.
31    #[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    // === Event Simulation ===
38
39    /// Simulate a click on a widget matching the selector.
40    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    /// Simulate typing text into a widget.
59    pub fn type_text(&mut self, selector: &str, text: &str) -> &mut Self {
60        if self.query(selector).is_some() {
61            // Focus the element
62            self.event_queue.push_back(Event::FocusIn);
63
64            // Type each character
65            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    /// Simulate a key press.
77    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    /// Simulate scrolling.
85    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    // === Queries ===
97
98    /// Query for a widget matching the selector.
99    #[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    /// Query for all widgets matching the selector.
106    #[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    /// Get text content from a widget.
117    #[must_use]
118    pub fn text(&self, selector: &str) -> String {
119        // Simplified - would extract text from Text widgets
120        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    /// Check if a widget exists.
129    #[must_use]
130    pub fn exists(&self, selector: &str) -> bool {
131        self.query(selector).is_some()
132    }
133
134    // === Assertions ===
135
136    /// Assert that a widget exists.
137    ///
138    /// # Panics
139    ///
140    /// Panics if the widget does not exist.
141    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    /// Assert that a widget does not exist.
150    ///
151    /// # Panics
152    ///
153    /// Panics if the widget exists.
154    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    /// Assert that text matches exactly.
163    ///
164    /// # Panics
165    ///
166    /// Panics if the text does not match.
167    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    /// Assert that text contains a substring.
177    ///
178    /// # Panics
179    ///
180    /// Panics if the text does not contain the substring.
181    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    /// Assert the count of matching widgets.
191    ///
192    /// # Panics
193    ///
194    /// Panics if the count does not match.
195    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    // === Internal ===
205
206    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        // Simplified - would return actual widget bounds
251        if self.exists(selector) {
252            Some(Rect::new(0.0, 0.0, 100.0, 50.0))
253        } else {
254            None
255        }
256    }
257
258    /// Advance simulated time.
259    pub fn tick(&mut self, _ms: u64) {
260        // Would trigger animations, timers, etc.
261    }
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    // Mock widget for testing
275    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    // =========================================================================
420    // assert_not_exists Tests
421    // =========================================================================
422
423    #[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    // =========================================================================
439    // assert_text Tests
440    // =========================================================================
441
442    #[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    // =========================================================================
478    // Viewport Tests
479    // =========================================================================
480
481    #[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    // =========================================================================
498    // Event Simulation Tests
499    // =========================================================================
500
501    #[test]
502    fn test_harness_click() {
503        let widget = MockWidget::new().with_test_id("button");
504        let mut harness = Harness::new(widget);
505        // Should not panic
506        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        // Should not panic for nonexistent widget
514        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        // Should not panic
522        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        // Should not panic for nonexistent widget
530        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        // Should not panic
538        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        // Should not panic
548        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        // Should not panic for nonexistent widget
557        harness.scroll("[data-testid='nonexistent']", 100.0);
558    }
559
560    // =========================================================================
561    // Query Tests
562    // =========================================================================
563
564    #[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    // =========================================================================
618    // Tick Test
619    // =========================================================================
620
621    #[test]
622    fn test_harness_tick() {
623        let widget = MockWidget::new();
624        let mut harness = Harness::new(widget);
625        // Should not panic
626        harness.tick(100);
627        harness.tick(1000);
628    }
629
630    // =========================================================================
631    // Text Edge Cases
632    // =========================================================================
633
634    #[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    // =========================================================================
649    // Method Chaining Tests
650    // =========================================================================
651
652    #[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        // Chain multiple operations
662        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        // Assertions also chain
669        harness
670            .assert_exists("[data-testid='form']")
671            .assert_exists("[data-testid='input']")
672            .assert_exists("[data-testid='submit']");
673    }
674
675    // =========================================================================
676    // assert_count Edge Cases
677    // =========================================================================
678
679    #[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}