Skip to main content

presentar_test/
bdd.rs

1//! BDD-style testing helpers for Presentar.
2//!
3//! Provides `describe`, `it`, and context management for expressive tests.
4//!
5//! # Example
6//!
7//! ```rust
8//! use presentar_test::bdd::*;
9//!
10//! #[test]
11//! fn button_widget_tests() {
12//!     describe("Button", |ctx| {
13//!         ctx.before(|| {
14//!             // Setup code
15//!         });
16//!
17//!         ctx.it("renders with label", |_| {
18//!             // Test code
19//!             expect(true).to_be_true();
20//!         });
21//!
22//!         ctx.it("responds to click", |_| {
23//!             expect(1 + 1).to_equal(2);
24//!         });
25//!     });
26//! }
27//! ```
28
29use std::cell::RefCell;
30use std::rc::Rc;
31
32/// Test context for BDD-style tests.
33#[derive(Default)]
34pub struct TestContext {
35    /// Description of current test
36    description: String,
37    /// Before hooks
38    before_hooks: Vec<Box<dyn Fn()>>,
39    /// After hooks
40    after_hooks: Vec<Box<dyn Fn()>>,
41    /// Passed test count
42    passed: Rc<RefCell<u32>>,
43    /// Failed test count
44    failed: Rc<RefCell<u32>>,
45    /// Failure messages
46    failures: Rc<RefCell<Vec<String>>>,
47}
48
49impl TestContext {
50    /// Create a new test context.
51    pub fn new(description: impl Into<String>) -> Self {
52        Self {
53            description: description.into(),
54            before_hooks: Vec::new(),
55            after_hooks: Vec::new(),
56            passed: Rc::new(RefCell::new(0)),
57            failed: Rc::new(RefCell::new(0)),
58            failures: Rc::new(RefCell::new(Vec::new())),
59        }
60    }
61
62    /// Register a before hook.
63    pub fn before<F: Fn() + 'static>(&mut self, f: F) {
64        self.before_hooks.push(Box::new(f));
65    }
66
67    /// Register an after hook.
68    pub fn after<F: Fn() + 'static>(&mut self, f: F) {
69        self.after_hooks.push(Box::new(f));
70    }
71
72    /// Define a test case.
73    pub fn it<F: Fn(&TestContext)>(&self, description: &str, test: F) {
74        // Run before hooks
75        for hook in &self.before_hooks {
76            hook();
77        }
78
79        // Run test with panic catching
80        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
81            test(self);
82        }));
83
84        // Run after hooks
85        for hook in &self.after_hooks {
86            hook();
87        }
88
89        match result {
90            Ok(()) => {
91                *self.passed.borrow_mut() += 1;
92            }
93            Err(e) => {
94                *self.failed.borrow_mut() += 1;
95                let msg = if let Some(s) = e.downcast_ref::<&str>() {
96                    format!("{} - {}: {}", self.description, description, s)
97                } else if let Some(s) = e.downcast_ref::<String>() {
98                    format!("{} - {}: {}", self.description, description, s)
99                } else {
100                    format!("{} - {}: test panicked", self.description, description)
101                };
102                self.failures.borrow_mut().push(msg);
103            }
104        }
105    }
106
107    /// Get passed count.
108    pub fn passed(&self) -> u32 {
109        *self.passed.borrow()
110    }
111
112    /// Get failed count.
113    pub fn failed(&self) -> u32 {
114        *self.failed.borrow()
115    }
116
117    /// Get failures.
118    pub fn failures(&self) -> Vec<String> {
119        self.failures.borrow().clone()
120    }
121
122    /// Check if all tests passed.
123    pub fn all_passed(&self) -> bool {
124        *self.failed.borrow() == 0
125    }
126}
127
128/// Describe a test suite.
129pub fn describe<F: FnOnce(&mut TestContext)>(description: &str, f: F) -> TestContext {
130    let mut ctx = TestContext::new(description);
131    f(&mut ctx);
132    ctx
133}
134
135/// Run a describe block and assert all tests pass.
136pub fn describe_and_assert<F: FnOnce(&mut TestContext)>(description: &str, f: F) {
137    let ctx = describe(description, f);
138    if !ctx.all_passed() {
139        panic!(
140            "Test suite '{}' failed: {} passed, {} failed\n{}",
141            description,
142            ctx.passed(),
143            ctx.failed(),
144            ctx.failures().join("\n")
145        );
146    }
147}
148
149// =============================================================================
150// Expectations API
151// =============================================================================
152
153/// Wrapper for making assertions.
154pub struct Expectation<T> {
155    value: T,
156    negated: bool,
157}
158
159/// Create an expectation from a value.
160pub fn expect<T>(value: T) -> Expectation<T> {
161    Expectation {
162        value,
163        negated: false,
164    }
165}
166
167impl<T> Expectation<T> {
168    /// Negate the expectation.
169    pub fn not(mut self) -> Self {
170        self.negated = !self.negated;
171        self
172    }
173}
174
175impl<T: PartialEq + std::fmt::Debug> Expectation<T> {
176    /// Assert equality.
177    pub fn to_equal(self, expected: T) {
178        let matches = self.value == expected;
179        if self.negated {
180            if matches {
181                panic!("Expected {:?} not to equal {:?}", self.value, expected);
182            }
183        } else if !matches {
184            panic!("Expected {:?} to equal {:?}", self.value, expected);
185        }
186    }
187}
188
189impl<T: PartialOrd + std::fmt::Debug> Expectation<T> {
190    /// Assert greater than.
191    pub fn to_be_greater_than(self, other: T) {
192        let matches = self.value > other;
193        if self.negated {
194            if matches {
195                panic!(
196                    "Expected {:?} not to be greater than {:?}",
197                    self.value, other
198                );
199            }
200        } else if !matches {
201            panic!("Expected {:?} to be greater than {:?}", self.value, other);
202        }
203    }
204
205    /// Assert less than.
206    pub fn to_be_less_than(self, other: T) {
207        let matches = self.value < other;
208        if self.negated {
209            if matches {
210                panic!("Expected {:?} not to be less than {:?}", self.value, other);
211            }
212        } else if !matches {
213            panic!("Expected {:?} to be less than {:?}", self.value, other);
214        }
215    }
216}
217
218impl Expectation<bool> {
219    /// Assert true.
220    pub fn to_be_true(self) {
221        if self.negated {
222            if self.value {
223                panic!("Expected false but got true");
224            }
225        } else if !self.value {
226            panic!("Expected true but got false");
227        }
228    }
229
230    /// Assert false.
231    pub fn to_be_false(self) {
232        if self.negated {
233            if !self.value {
234                panic!("Expected true but got false");
235            }
236        } else if self.value {
237            panic!("Expected false but got true");
238        }
239    }
240}
241
242impl<T> Expectation<Option<T>> {
243    /// Assert Some.
244    pub fn to_be_some(self) {
245        let is_some = self.value.is_some();
246        if self.negated {
247            if is_some {
248                panic!("Expected None but got Some");
249            }
250        } else if !is_some {
251            panic!("Expected Some but got None");
252        }
253    }
254
255    /// Assert None.
256    pub fn to_be_none(self) {
257        let is_none = self.value.is_none();
258        if self.negated {
259            if is_none {
260                panic!("Expected Some but got None");
261            }
262        } else if !is_none {
263            panic!("Expected None but got Some");
264        }
265    }
266}
267
268impl<T, E> Expectation<Result<T, E>> {
269    /// Assert Ok.
270    pub fn to_be_ok(self) {
271        let is_ok = self.value.is_ok();
272        if self.negated {
273            if is_ok {
274                panic!("Expected Err but got Ok");
275            }
276        } else if !is_ok {
277            panic!("Expected Ok but got Err");
278        }
279    }
280
281    /// Assert Err.
282    pub fn to_be_err(self) {
283        let is_err = self.value.is_err();
284        if self.negated {
285            if is_err {
286                panic!("Expected Ok but got Err");
287            }
288        } else if !is_err {
289            panic!("Expected Err but got Ok");
290        }
291    }
292}
293
294impl<T> Expectation<Vec<T>> {
295    /// Assert empty.
296    pub fn to_be_empty(self) {
297        let is_empty = self.value.is_empty();
298        if self.negated {
299            if is_empty {
300                panic!("Expected non-empty but got empty");
301            }
302        } else if !is_empty {
303            panic!("Expected empty but got {} elements", self.value.len());
304        }
305    }
306
307    /// Assert length.
308    pub fn to_have_length(self, expected: usize) {
309        let len = self.value.len();
310        if self.negated {
311            if len == expected {
312                panic!("Expected length not to be {} but it was", expected);
313            }
314        } else if len != expected {
315            panic!("Expected length {} but got {}", expected, len);
316        }
317    }
318}
319
320impl Expectation<&str> {
321    /// Assert contains.
322    pub fn to_contain(self, needle: &str) {
323        let contains = self.value.contains(needle);
324        if self.negated {
325            if contains {
326                panic!("Expected {:?} not to contain {:?}", self.value, needle);
327            }
328        } else if !contains {
329            panic!("Expected {:?} to contain {:?}", self.value, needle);
330        }
331    }
332
333    /// Assert starts with.
334    pub fn to_start_with(self, prefix: &str) {
335        let starts = self.value.starts_with(prefix);
336        if self.negated {
337            if starts {
338                panic!("Expected {:?} not to start with {:?}", self.value, prefix);
339            }
340        } else if !starts {
341            panic!("Expected {:?} to start with {:?}", self.value, prefix);
342        }
343    }
344
345    /// Assert ends with.
346    pub fn to_end_with(self, suffix: &str) {
347        let ends = self.value.ends_with(suffix);
348        if self.negated {
349            if ends {
350                panic!("Expected {:?} not to end with {:?}", self.value, suffix);
351            }
352        } else if !ends {
353            panic!("Expected {:?} to end with {:?}", self.value, suffix);
354        }
355    }
356}
357
358impl Expectation<String> {
359    /// Assert contains.
360    pub fn to_contain(self, needle: &str) {
361        let contains = self.value.contains(needle);
362        if self.negated {
363            if contains {
364                panic!("Expected {:?} not to contain {:?}", self.value, needle);
365            }
366        } else if !contains {
367            panic!("Expected {:?} to contain {:?}", self.value, needle);
368        }
369    }
370
371    /// Assert empty.
372    pub fn to_be_empty(self) {
373        let is_empty = self.value.is_empty();
374        if self.negated {
375            if is_empty {
376                panic!("Expected non-empty string but got empty");
377            }
378        } else if !is_empty {
379            panic!("Expected empty string but got {:?}", self.value);
380        }
381    }
382}
383
384impl Expectation<f32> {
385    /// Assert close to (within epsilon).
386    pub fn to_be_close_to(self, expected: f32, epsilon: f32) {
387        let diff = (self.value - expected).abs();
388        let close = diff <= epsilon;
389        if self.negated {
390            if close {
391                panic!(
392                    "Expected {} not to be close to {} (within {})",
393                    self.value, expected, epsilon
394                );
395            }
396        } else if !close {
397            panic!(
398                "Expected {} to be close to {} (within {}), diff was {}",
399                self.value, expected, epsilon, diff
400            );
401        }
402    }
403}
404
405impl Expectation<f64> {
406    /// Assert close to (within epsilon).
407    pub fn to_be_close_to(self, expected: f64, epsilon: f64) {
408        let diff = (self.value - expected).abs();
409        let close = diff <= epsilon;
410        if self.negated {
411            if close {
412                panic!(
413                    "Expected {} not to be close to {} (within {})",
414                    self.value, expected, epsilon
415                );
416            }
417        } else if !close {
418            panic!(
419                "Expected {} to be close to {} (within {}), diff was {}",
420                self.value, expected, epsilon, diff
421            );
422        }
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    #[test]
431    fn test_describe_basic() {
432        let ctx = describe("Math operations", |ctx| {
433            ctx.it("adds numbers", |_| {
434                expect(1 + 1).to_equal(2);
435            });
436
437            ctx.it("subtracts numbers", |_| {
438                expect(5 - 3).to_equal(2);
439            });
440        });
441
442        assert_eq!(ctx.passed(), 2);
443        assert_eq!(ctx.failed(), 0);
444        assert!(ctx.all_passed());
445    }
446
447    #[test]
448    fn test_describe_with_failure() {
449        let ctx = describe("Failing tests", |ctx| {
450            ctx.it("passes", |_| {
451                expect(1).to_equal(1);
452            });
453
454            ctx.it("fails", |_| {
455                expect(1).to_equal(2);
456            });
457        });
458
459        assert_eq!(ctx.passed(), 1);
460        assert_eq!(ctx.failed(), 1);
461        assert!(!ctx.all_passed());
462    }
463
464    #[test]
465    fn test_expect_equality() {
466        expect(42).to_equal(42);
467        expect("hello").to_equal("hello");
468        expect(vec![1, 2, 3]).to_equal(vec![1, 2, 3]);
469    }
470
471    #[test]
472    fn test_expect_not() {
473        expect(1).not().to_equal(2);
474        expect(true).not().to_be_false();
475    }
476
477    #[test]
478    fn test_expect_bool() {
479        expect(true).to_be_true();
480        expect(false).to_be_false();
481        expect(1 > 0).to_be_true();
482    }
483
484    #[test]
485    fn test_expect_comparison() {
486        expect(10).to_be_greater_than(5);
487        expect(3).to_be_less_than(7);
488    }
489
490    #[test]
491    fn test_expect_option() {
492        expect(Some(42)).to_be_some();
493        expect(None::<i32>).to_be_none();
494    }
495
496    #[test]
497    fn test_expect_result() {
498        expect(Ok::<i32, &str>(42)).to_be_ok();
499        expect(Err::<i32, &str>("error")).to_be_err();
500    }
501
502    #[test]
503    fn test_expect_vec() {
504        expect(Vec::<i32>::new()).to_be_empty();
505        expect(vec![1, 2, 3]).to_have_length(3);
506        expect(vec![1, 2]).not().to_be_empty();
507    }
508
509    #[test]
510    fn test_expect_string() {
511        expect("hello world").to_contain("world");
512        expect("hello").to_start_with("hel");
513        expect("hello").to_end_with("llo");
514        expect("hello").not().to_contain("xyz");
515    }
516
517    #[test]
518    fn test_expect_float_close_to() {
519        expect(0.1 + 0.2_f32).to_be_close_to(0.3, 0.001);
520        expect(3.14159_f64).to_be_close_to(3.14, 0.01);
521    }
522
523    #[test]
524    fn test_before_after_hooks() {
525        use std::cell::Cell;
526        use std::rc::Rc;
527
528        let counter = Rc::new(Cell::new(0));
529        let counter_clone = counter.clone();
530        let counter_clone2 = counter.clone();
531
532        let ctx = describe("Hooks", |ctx| {
533            ctx.before(move || {
534                counter_clone.set(counter_clone.get() + 1);
535            });
536
537            ctx.after(move || {
538                counter_clone2.set(counter_clone2.get() + 10);
539            });
540
541            ctx.it("first test", |_| {});
542            ctx.it("second test", |_| {});
543        });
544
545        // 2 tests * (1 before + 10 after) = 22
546        assert_eq!(counter.get(), 22);
547        assert!(ctx.all_passed());
548    }
549
550    #[test]
551    fn test_nested_describe() {
552        let outer = describe("Outer", |outer_ctx| {
553            outer_ctx.it("outer test", |_| {
554                expect(true).to_be_true();
555            });
556
557            let inner = describe("Inner", |inner_ctx| {
558                inner_ctx.it("inner test", |_| {
559                    expect(1 + 1).to_equal(2);
560                });
561            });
562
563            assert!(inner.all_passed());
564        });
565
566        assert!(outer.all_passed());
567    }
568
569    // =========================================================================
570    // Additional Coverage Tests
571    // =========================================================================
572
573    #[test]
574    fn test_expect_string_owned_to_contain() {
575        expect("hello world".to_string()).to_contain("world");
576    }
577
578    #[test]
579    fn test_expect_string_owned_to_be_empty() {
580        expect(String::new()).to_be_empty();
581    }
582
583    #[test]
584    fn test_expect_string_owned_not_empty() {
585        expect("hello".to_string()).not().to_be_empty();
586    }
587
588    #[test]
589    fn test_expect_f32_close_to_negated() {
590        expect(10.0_f32).not().to_be_close_to(1.0, 0.1);
591    }
592
593    #[test]
594    fn test_expect_f64_close_to_negated() {
595        expect(10.0_f64).not().to_be_close_to(1.0, 0.1);
596    }
597
598    #[test]
599    fn test_expect_str_not_start_with() {
600        expect("hello").not().to_start_with("xyz");
601    }
602
603    #[test]
604    fn test_expect_str_not_end_with() {
605        expect("hello").not().to_end_with("xyz");
606    }
607
608    #[test]
609    fn test_expect_option_negated() {
610        expect(Some(42)).not().to_be_none();
611        expect(None::<i32>).not().to_be_some();
612    }
613
614    #[test]
615    fn test_expect_result_negated() {
616        expect(Ok::<i32, &str>(42)).not().to_be_err();
617        expect(Err::<i32, &str>("error")).not().to_be_ok();
618    }
619
620    #[test]
621    fn test_expect_vec_not_length() {
622        expect(vec![1, 2, 3]).not().to_have_length(5);
623    }
624
625    #[test]
626    fn test_expect_bool_negated() {
627        expect(true).not().to_be_false();
628        expect(false).not().to_be_true();
629    }
630
631    #[test]
632    fn test_expect_comparison_negated() {
633        expect(3).not().to_be_greater_than(10);
634        expect(10).not().to_be_less_than(3);
635    }
636
637    #[test]
638    fn test_expect_equality_negated() {
639        expect(1).not().to_equal(2);
640    }
641
642    #[test]
643    fn test_context_passed_plus_failed() {
644        let ctx = describe("Test", |ctx| {
645            ctx.it("pass1", |_| {});
646            ctx.it("pass2", |_| {});
647            ctx.it("fail", |_| {
648                expect(1).to_equal(2);
649            });
650        });
651        assert_eq!(ctx.passed() + ctx.failed(), 3);
652    }
653}