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::{widget::LayoutResult, Canvas, Constraints, Size, TypeId};
268    use std::any::Any;
269
270    // Mock widget for testing
271    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    // =========================================================================
386    // assert_not_exists Tests
387    // =========================================================================
388
389    #[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    // =========================================================================
405    // assert_text Tests
406    // =========================================================================
407
408    #[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    // =========================================================================
444    // Viewport Tests
445    // =========================================================================
446
447    #[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    // =========================================================================
464    // Event Simulation Tests
465    // =========================================================================
466
467    #[test]
468    fn test_harness_click() {
469        let widget = MockWidget::new().with_test_id("button");
470        let mut harness = Harness::new(widget);
471        // Should not panic
472        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        // Should not panic for nonexistent widget
480        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        // Should not panic
488        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        // Should not panic for nonexistent widget
496        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        // Should not panic
504        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        // Should not panic
514        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        // Should not panic for nonexistent widget
523        harness.scroll("[data-testid='nonexistent']", 100.0);
524    }
525
526    // =========================================================================
527    // Query Tests
528    // =========================================================================
529
530    #[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    // =========================================================================
584    // Tick Test
585    // =========================================================================
586
587    #[test]
588    fn test_harness_tick() {
589        let widget = MockWidget::new();
590        let mut harness = Harness::new(widget);
591        // Should not panic
592        harness.tick(100);
593        harness.tick(1000);
594    }
595
596    // =========================================================================
597    // Text Edge Cases
598    // =========================================================================
599
600    #[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    // =========================================================================
615    // Method Chaining Tests
616    // =========================================================================
617
618    #[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        // Chain multiple operations
628        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        // Assertions also chain
635        harness
636            .assert_exists("[data-testid='form']")
637            .assert_exists("[data-testid='input']")
638            .assert_exists("[data-testid='submit']");
639    }
640
641    // =========================================================================
642    // assert_count Edge Cases
643    // =========================================================================
644
645    #[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}