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 {
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    /// Simulate scrolling.
91    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    // === Queries ===
103
104    /// Query for a widget matching the selector.
105    #[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    /// Query for all widgets matching the selector.
112    #[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    /// Get text content from a widget.
123    #[must_use]
124    pub fn text(&self, selector: &str) -> String {
125        // Simplified - would extract text from Text widgets
126        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    /// Check if a widget exists.
135    #[must_use]
136    pub fn exists(&self, selector: &str) -> bool {
137        self.query(selector).is_some()
138    }
139
140    // === Assertions ===
141
142    /// Assert that a widget exists.
143    ///
144    /// # Panics
145    ///
146    /// Panics if the widget does not exist.
147    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    /// Assert that a widget does not exist.
156    ///
157    /// # Panics
158    ///
159    /// Panics if the widget exists.
160    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    /// Assert that text matches exactly.
169    ///
170    /// # Panics
171    ///
172    /// Panics if the text does not match.
173    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    /// Assert that text contains a substring.
183    ///
184    /// # Panics
185    ///
186    /// Panics if the text does not contain the substring.
187    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    /// Assert the count of matching widgets.
197    ///
198    /// # Panics
199    ///
200    /// Panics if the count does not match.
201    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    // === Internal ===
211
212    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        // Simplified - would return actual widget bounds
257        if self.exists(selector) {
258            Some(Rect::new(0.0, 0.0, 100.0, 50.0))
259        } else {
260            None
261        }
262    }
263
264    /// Advance simulated time.
265    pub fn tick(&mut self, _ms: u64) {
266        // Would trigger animations, timers, etc.
267    }
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    // Mock widget for testing
281    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    // =========================================================================
426    // assert_not_exists Tests
427    // =========================================================================
428
429    #[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    // =========================================================================
445    // assert_text Tests
446    // =========================================================================
447
448    #[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    // =========================================================================
484    // Viewport Tests
485    // =========================================================================
486
487    #[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    // =========================================================================
504    // Event Simulation Tests
505    // =========================================================================
506
507    #[test]
508    fn test_harness_click() {
509        let widget = MockWidget::new().with_test_id("button");
510        let mut harness = Harness::new(widget);
511        // Should not panic
512        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        // Should not panic for nonexistent widget
520        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        // Should not panic
528        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        // Should not panic for nonexistent widget
536        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        // Should not panic
544        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        // Should not panic
554        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        // Should not panic for nonexistent widget
563        harness.scroll("[data-testid='nonexistent']", 100.0);
564    }
565
566    // =========================================================================
567    // Query Tests
568    // =========================================================================
569
570    #[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    // =========================================================================
624    // Tick Test
625    // =========================================================================
626
627    #[test]
628    fn test_harness_tick() {
629        let widget = MockWidget::new();
630        let mut harness = Harness::new(widget);
631        // Should not panic
632        harness.tick(100);
633        harness.tick(1000);
634    }
635
636    // =========================================================================
637    // Text Edge Cases
638    // =========================================================================
639
640    #[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    // =========================================================================
655    // Method Chaining Tests
656    // =========================================================================
657
658    #[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        // Chain multiple operations
668        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        // Assertions also chain
675        harness
676            .assert_exists("[data-testid='form']")
677            .assert_exists("[data-testid='input']")
678            .assert_exists("[data-testid='submit']");
679    }
680
681    // =========================================================================
682    // assert_count Edge Cases
683    // =========================================================================
684
685    #[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}