Skip to main content

jugar_probar/tui/
assertions.rs

1//! TUI Frame Assertions (Feature 21 - EDD Compliance)
2//!
3//! Provides Playwright-style assertions for TUI frames.
4//!
5//! ## EXTREME TDD: Tests written FIRST per spec
6//!
7//! ## Toyota Way Application
8//!
9//! - **Poka-Yoke**: Type-safe assertions prevent invalid comparisons
10//! - **Muda**: Fail-fast on first mismatch
11//! - **Jidoka**: Clear error messages with visual diff
12
13use super::backend::TuiFrame;
14use crate::result::{ProbarError, ProbarResult};
15use std::collections::HashMap;
16
17/// Frame assertion builder (Playwright-style API)
18#[derive(Debug)]
19pub struct FrameAssertion<'a> {
20    frame: &'a TuiFrame,
21    soft_mode: bool,
22    errors: Vec<String>,
23}
24
25impl<'a> FrameAssertion<'a> {
26    /// Create a new frame assertion
27    #[must_use]
28    pub fn new(frame: &'a TuiFrame) -> Self {
29        Self {
30            frame,
31            soft_mode: false,
32            errors: Vec::new(),
33        }
34    }
35
36    /// Enable soft assertion mode (collect errors instead of failing immediately)
37    #[must_use]
38    pub fn soft(mut self) -> Self {
39        self.soft_mode = true;
40        self
41    }
42
43    /// Assert frame contains text
44    pub fn to_contain_text(&mut self, text: &str) -> ProbarResult<&mut Self> {
45        if !self.frame.contains(text) {
46            let msg = format!(
47                "Expected frame to contain text '{}'\nFrame content:\n{}",
48                text,
49                self.frame.as_text()
50            );
51            if self.soft_mode {
52                self.errors.push(msg);
53            } else {
54                return Err(ProbarError::AssertionFailed { message: msg });
55            }
56        }
57        Ok(self)
58    }
59
60    /// Assert frame does not contain text
61    pub fn not_to_contain_text(&mut self, text: &str) -> ProbarResult<&mut Self> {
62        if self.frame.contains(text) {
63            let msg = format!(
64                "Expected frame NOT to contain text '{}'\nFrame content:\n{}",
65                text,
66                self.frame.as_text()
67            );
68            if self.soft_mode {
69                self.errors.push(msg);
70            } else {
71                return Err(ProbarError::AssertionFailed { message: msg });
72            }
73        }
74        Ok(self)
75    }
76
77    /// Assert frame matches regex pattern
78    pub fn to_match(&mut self, pattern: &str) -> ProbarResult<&mut Self> {
79        let matches = self.frame.matches(pattern)?;
80        if !matches {
81            let msg = format!(
82                "Expected frame to match pattern '{}'\nFrame content:\n{}",
83                pattern,
84                self.frame.as_text()
85            );
86            if self.soft_mode {
87                self.errors.push(msg);
88            } else {
89                return Err(ProbarError::AssertionFailed { message: msg });
90            }
91        }
92        Ok(self)
93    }
94
95    /// Assert specific line contains text
96    pub fn line_to_contain(&mut self, line_num: usize, text: &str) -> ProbarResult<&mut Self> {
97        let line = self.frame.line(line_num);
98        match line {
99            Some(content) if content.contains(text) => Ok(self),
100            Some(content) => {
101                let msg = format!(
102                    "Expected line {} to contain '{}'\nActual: '{}'",
103                    line_num, text, content
104                );
105                if self.soft_mode {
106                    self.errors.push(msg);
107                    Ok(self)
108                } else {
109                    Err(ProbarError::AssertionFailed { message: msg })
110                }
111            }
112            None => {
113                let msg = format!(
114                    "Line {} does not exist (frame has {} lines)",
115                    line_num,
116                    self.frame.height()
117                );
118                if self.soft_mode {
119                    self.errors.push(msg);
120                    Ok(self)
121                } else {
122                    Err(ProbarError::AssertionFailed { message: msg })
123                }
124            }
125        }
126    }
127
128    /// Assert specific line equals exact text
129    pub fn line_to_equal(&mut self, line_num: usize, expected: &str) -> ProbarResult<&mut Self> {
130        let line = self.frame.line(line_num);
131        match line {
132            Some(content) if content == expected => Ok(self),
133            Some(content) => {
134                let msg = format!(
135                    "Expected line {} to equal '{}'\nActual: '{}'",
136                    line_num, expected, content
137                );
138                if self.soft_mode {
139                    self.errors.push(msg);
140                    Ok(self)
141                } else {
142                    Err(ProbarError::AssertionFailed { message: msg })
143                }
144            }
145            None => {
146                let msg = format!(
147                    "Line {} does not exist (frame has {} lines)",
148                    line_num,
149                    self.frame.height()
150                );
151                if self.soft_mode {
152                    self.errors.push(msg);
153                    Ok(self)
154                } else {
155                    Err(ProbarError::AssertionFailed { message: msg })
156                }
157            }
158        }
159    }
160
161    /// Assert frame has expected dimensions
162    pub fn to_have_size(&mut self, width: u16, height: u16) -> ProbarResult<&mut Self> {
163        let actual_width = self.frame.width();
164        let actual_height = self.frame.height();
165
166        if actual_width != width || actual_height != height {
167            let msg = format!(
168                "Expected frame size {}x{}, got {}x{}",
169                width, height, actual_width, actual_height
170            );
171            if self.soft_mode {
172                self.errors.push(msg);
173            } else {
174                return Err(ProbarError::AssertionFailed { message: msg });
175            }
176        }
177        Ok(self)
178    }
179
180    /// Assert frame is identical to another frame
181    pub fn to_be_identical_to(&mut self, other: &TuiFrame) -> ProbarResult<&mut Self> {
182        if !self.frame.is_identical(other) {
183            let diff = self.frame.diff(other);
184            let msg = format!("Frames are not identical:\n{diff}");
185            if self.soft_mode {
186                self.errors.push(msg);
187            } else {
188                return Err(ProbarError::AssertionFailed { message: msg });
189            }
190        }
191        Ok(self)
192    }
193
194    /// Finalize soft assertions and return any collected errors
195    pub fn finalize(&self) -> ProbarResult<()> {
196        if self.errors.is_empty() {
197            Ok(())
198        } else {
199            Err(ProbarError::AssertionFailed {
200                message: format!(
201                    "{} assertion(s) failed:\n{}",
202                    self.errors.len(),
203                    self.errors.join("\n\n")
204                ),
205            })
206        }
207    }
208
209    /// Get collected errors (for soft assertions)
210    #[must_use]
211    pub fn errors(&self) -> &[String] {
212        &self.errors
213    }
214}
215
216/// Create a frame assertion
217#[must_use]
218pub fn expect_frame(frame: &TuiFrame) -> FrameAssertion<'_> {
219    FrameAssertion::new(frame)
220}
221
222/// Value tracker for monitoring changes over time
223///
224/// Useful for EDD (Equation-Driven Development) where you want to
225/// verify that values change according to expected patterns.
226#[derive(Debug, Clone)]
227pub struct ValueTracker<T: Clone> {
228    values: Vec<(u64, T)>, // (timestamp_ms, value)
229    name: String,
230}
231
232impl<T: Clone> ValueTracker<T> {
233    /// Create a new value tracker
234    #[must_use]
235    pub fn new(name: &str) -> Self {
236        Self {
237            values: Vec::new(),
238            name: name.to_string(),
239        }
240    }
241
242    /// Record a value at a timestamp
243    pub fn record(&mut self, timestamp_ms: u64, value: T) {
244        self.values.push((timestamp_ms, value));
245    }
246
247    /// Get the tracker name
248    #[must_use]
249    pub fn name(&self) -> &str {
250        &self.name
251    }
252
253    /// Get all recorded values
254    #[must_use]
255    pub fn values(&self) -> &[(u64, T)] {
256        &self.values
257    }
258
259    /// Get the latest value
260    #[must_use]
261    pub fn latest(&self) -> Option<&T> {
262        self.values.last().map(|(_, v)| v)
263    }
264
265    /// Get value at specific index
266    #[must_use]
267    pub fn at(&self, index: usize) -> Option<&T> {
268        self.values.get(index).map(|(_, v)| v)
269    }
270
271    /// Get the number of recorded values
272    #[must_use]
273    pub fn len(&self) -> usize {
274        self.values.len()
275    }
276
277    /// Check if tracker is empty
278    #[must_use]
279    pub fn is_empty(&self) -> bool {
280        self.values.is_empty()
281    }
282
283    /// Clear all recorded values
284    pub fn clear(&mut self) {
285        self.values.clear();
286    }
287}
288
289impl<T: Clone + PartialEq> ValueTracker<T> {
290    /// Check if value changed since last recording
291    #[must_use]
292    pub fn has_changed(&self) -> bool {
293        if self.values.len() < 2 {
294            return false;
295        }
296        let last = &self.values[self.values.len() - 1].1;
297        let prev = &self.values[self.values.len() - 2].1;
298        last != prev
299    }
300
301    /// Count how many times the value changed
302    #[must_use]
303    pub fn change_count(&self) -> usize {
304        if self.values.len() < 2 {
305            return 0;
306        }
307        self.values.windows(2).filter(|w| w[0].1 != w[1].1).count()
308    }
309}
310
311impl ValueTracker<f64> {
312    /// Calculate the rate of change (delta per millisecond)
313    #[must_use]
314    pub fn rate_of_change(&self) -> Option<f64> {
315        if self.values.len() < 2 {
316            return None;
317        }
318        let (t1, v1) = &self.values[self.values.len() - 2];
319        let (t2, v2) = &self.values[self.values.len() - 1];
320        let dt = (*t2 as f64) - (*t1 as f64);
321        if dt.abs() < f64::EPSILON {
322            return None;
323        }
324        Some((v2 - v1) / dt)
325    }
326
327    /// Check if value is monotonically increasing
328    #[must_use]
329    pub fn is_increasing(&self) -> bool {
330        if self.values.len() < 2 {
331            return true;
332        }
333        self.values.windows(2).all(|w| w[1].1 >= w[0].1)
334    }
335
336    /// Check if value is monotonically decreasing
337    #[must_use]
338    pub fn is_decreasing(&self) -> bool {
339        if self.values.len() < 2 {
340            return true;
341        }
342        self.values.windows(2).all(|w| w[1].1 <= w[0].1)
343    }
344
345    /// Get the minimum value
346    #[must_use]
347    pub fn min(&self) -> Option<f64> {
348        self.values
349            .iter()
350            .map(|(_, v)| *v)
351            .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
352    }
353
354    /// Get the maximum value
355    #[must_use]
356    pub fn max(&self) -> Option<f64> {
357        self.values
358            .iter()
359            .map(|(_, v)| *v)
360            .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
361    }
362
363    /// Get the average value
364    #[must_use]
365    pub fn average(&self) -> Option<f64> {
366        if self.values.is_empty() {
367            return None;
368        }
369        let sum: f64 = self.values.iter().map(|(_, v)| v).sum();
370        Some(sum / self.values.len() as f64)
371    }
372}
373
374impl ValueTracker<i64> {
375    /// Check if value is monotonically increasing
376    #[must_use]
377    pub fn is_increasing(&self) -> bool {
378        if self.values.len() < 2 {
379            return true;
380        }
381        self.values.windows(2).all(|w| w[1].1 >= w[0].1)
382    }
383
384    /// Check if value is monotonically decreasing
385    #[must_use]
386    pub fn is_decreasing(&self) -> bool {
387        if self.values.len() < 2 {
388            return true;
389        }
390        self.values.windows(2).all(|w| w[1].1 <= w[0].1)
391    }
392}
393
394/// Multi-value tracker for monitoring multiple named values
395#[derive(Debug, Default)]
396pub struct MultiValueTracker {
397    trackers: HashMap<String, ValueTracker<f64>>,
398}
399
400impl MultiValueTracker {
401    /// Create a new multi-value tracker
402    #[must_use]
403    pub fn new() -> Self {
404        Self {
405            trackers: HashMap::new(),
406        }
407    }
408
409    /// Record a value for a named tracker
410    pub fn record(&mut self, name: &str, timestamp_ms: u64, value: f64) {
411        let tracker = self
412            .trackers
413            .entry(name.to_string())
414            .or_insert_with(|| ValueTracker::new(name));
415        tracker.record(timestamp_ms, value);
416    }
417
418    /// Get a specific tracker
419    #[must_use]
420    pub fn get(&self, name: &str) -> Option<&ValueTracker<f64>> {
421        self.trackers.get(name)
422    }
423
424    /// Get all tracker names
425    #[must_use]
426    pub fn names(&self) -> Vec<&str> {
427        self.trackers.keys().map(String::as_str).collect()
428    }
429
430    /// Check if all tracked values are within expected bounds
431    pub fn assert_bounds(&self, bounds: &HashMap<String, (f64, f64)>) -> ProbarResult<()> {
432        let mut errors = Vec::new();
433
434        for (name, (min, max)) in bounds {
435            if let Some(tracker) = self.trackers.get(name) {
436                for (ts, value) in tracker.values() {
437                    if value < min || value > max {
438                        errors.push(format!(
439                            "{}: value {} at {}ms is outside bounds [{}, {}]",
440                            name, value, ts, min, max
441                        ));
442                    }
443                }
444            }
445        }
446
447        if errors.is_empty() {
448            Ok(())
449        } else {
450            Err(ProbarError::AssertionFailed {
451                message: errors.join("\n"),
452            })
453        }
454    }
455}
456
457#[cfg(test)]
458#[allow(clippy::unwrap_used, clippy::expect_used)]
459mod tests {
460    use super::*;
461
462    mod frame_assertion_tests {
463        use super::*;
464
465        #[test]
466        fn test_to_contain_text_pass() {
467            let frame = TuiFrame::from_lines(&["Hello World", "Goodbye"]);
468            let mut assertion = expect_frame(&frame);
469            assert!(assertion.to_contain_text("World").is_ok());
470        }
471
472        #[test]
473        fn test_to_contain_text_fail() {
474            let frame = TuiFrame::from_lines(&["Hello World"]);
475            let mut assertion = expect_frame(&frame);
476            assert!(assertion.to_contain_text("Missing").is_err());
477        }
478
479        #[test]
480        fn test_not_to_contain_text_pass() {
481            let frame = TuiFrame::from_lines(&["Hello World"]);
482            let mut assertion = expect_frame(&frame);
483            assert!(assertion.not_to_contain_text("Missing").is_ok());
484        }
485
486        #[test]
487        fn test_not_to_contain_text_fail() {
488            let frame = TuiFrame::from_lines(&["Hello World"]);
489            let mut assertion = expect_frame(&frame);
490            assert!(assertion.not_to_contain_text("World").is_err());
491        }
492
493        #[test]
494        fn test_to_match_pass() {
495            let frame = TuiFrame::from_lines(&["Score: 100"]);
496            let mut assertion = expect_frame(&frame);
497            assert!(assertion.to_match(r"Score: \d+").is_ok());
498        }
499
500        #[test]
501        fn test_to_match_fail() {
502            let frame = TuiFrame::from_lines(&["Score: abc"]);
503            let mut assertion = expect_frame(&frame);
504            assert!(assertion.to_match(r"Score: \d+").is_err());
505        }
506
507        #[test]
508        fn test_line_to_contain_pass() {
509            let frame = TuiFrame::from_lines(&["First", "Second", "Third"]);
510            let mut assertion = expect_frame(&frame);
511            assert!(assertion.line_to_contain(1, "Sec").is_ok());
512        }
513
514        #[test]
515        fn test_line_to_contain_fail() {
516            let frame = TuiFrame::from_lines(&["First", "Second"]);
517            let mut assertion = expect_frame(&frame);
518            assert!(assertion.line_to_contain(0, "Second").is_err());
519        }
520
521        #[test]
522        fn test_line_to_contain_invalid_line() {
523            let frame = TuiFrame::from_lines(&["Only one line"]);
524            let mut assertion = expect_frame(&frame);
525            assert!(assertion.line_to_contain(5, "text").is_err());
526        }
527
528        #[test]
529        fn test_line_to_equal_pass() {
530            let frame = TuiFrame::from_lines(&["Exact Match"]);
531            let mut assertion = expect_frame(&frame);
532            assert!(assertion.line_to_equal(0, "Exact Match").is_ok());
533        }
534
535        #[test]
536        fn test_line_to_equal_fail() {
537            let frame = TuiFrame::from_lines(&["Exact Match"]);
538            let mut assertion = expect_frame(&frame);
539            assert!(assertion.line_to_equal(0, "Different").is_err());
540        }
541
542        #[test]
543        fn test_to_have_size_pass() {
544            let frame = TuiFrame::from_lines(&["12345", "12345"]); // 5x2
545            let mut assertion = expect_frame(&frame);
546            assert!(assertion.to_have_size(5, 2).is_ok());
547        }
548
549        #[test]
550        fn test_to_have_size_fail() {
551            let frame = TuiFrame::from_lines(&["123"]); // 3x1
552            let mut assertion = expect_frame(&frame);
553            assert!(assertion.to_have_size(10, 10).is_err());
554        }
555
556        #[test]
557        fn test_to_be_identical_to_pass() {
558            let frame1 = TuiFrame::from_lines(&["Same", "Content"]);
559            let frame2 = TuiFrame::from_lines(&["Same", "Content"]);
560            let mut assertion = expect_frame(&frame1);
561            assert!(assertion.to_be_identical_to(&frame2).is_ok());
562        }
563
564        #[test]
565        fn test_to_be_identical_to_fail() {
566            let frame1 = TuiFrame::from_lines(&["Different"]);
567            let frame2 = TuiFrame::from_lines(&["Content"]);
568            let mut assertion = expect_frame(&frame1);
569            assert!(assertion.to_be_identical_to(&frame2).is_err());
570        }
571
572        #[test]
573        fn test_soft_assertions_collect_errors() {
574            let frame = TuiFrame::from_lines(&["Hello"]);
575            let mut assertion = expect_frame(&frame).soft();
576
577            let _ = assertion.to_contain_text("Missing1");
578            let _ = assertion.to_contain_text("Missing2");
579
580            assert_eq!(assertion.errors().len(), 2);
581            assert!(assertion.finalize().is_err());
582        }
583
584        #[test]
585        fn test_soft_assertions_no_errors() {
586            let frame = TuiFrame::from_lines(&["Hello World"]);
587            let mut assertion = expect_frame(&frame).soft();
588
589            let _ = assertion.to_contain_text("Hello");
590            let _ = assertion.to_contain_text("World");
591
592            assert!(assertion.errors().is_empty());
593            assert!(assertion.finalize().is_ok());
594        }
595
596        #[test]
597        fn test_chained_assertions() {
598            let frame = TuiFrame::from_lines(&["Score: 100", "Lives: 3"]);
599            let mut assertion = expect_frame(&frame);
600
601            assert!(assertion
602                .to_contain_text("Score")
603                .and_then(|a| a.to_contain_text("Lives"))
604                .and_then(|a| a.to_match(r"\d+"))
605                .is_ok());
606        }
607    }
608
609    mod value_tracker_tests {
610        use super::*;
611
612        #[test]
613        fn test_new() {
614            let tracker: ValueTracker<f64> = ValueTracker::new("score");
615            assert_eq!(tracker.name(), "score");
616            assert!(tracker.is_empty());
617        }
618
619        #[test]
620        fn test_record_and_latest() {
621            let mut tracker: ValueTracker<i64> = ValueTracker::new("score");
622            tracker.record(0, 100);
623            tracker.record(100, 200);
624
625            assert_eq!(tracker.len(), 2);
626            assert_eq!(tracker.latest(), Some(&200));
627        }
628
629        #[test]
630        fn test_at() {
631            let mut tracker: ValueTracker<i64> = ValueTracker::new("test");
632            tracker.record(0, 10);
633            tracker.record(100, 20);
634            tracker.record(200, 30);
635
636            assert_eq!(tracker.at(0), Some(&10));
637            assert_eq!(tracker.at(1), Some(&20));
638            assert_eq!(tracker.at(2), Some(&30));
639            assert_eq!(tracker.at(3), None);
640        }
641
642        #[test]
643        fn test_has_changed() {
644            let mut tracker: ValueTracker<i64> = ValueTracker::new("test");
645
646            assert!(!tracker.has_changed()); // Empty
647
648            tracker.record(0, 100);
649            assert!(!tracker.has_changed()); // Only one value
650
651            tracker.record(100, 100);
652            assert!(!tracker.has_changed()); // Same value
653
654            tracker.record(200, 200);
655            assert!(tracker.has_changed()); // Different value
656        }
657
658        #[test]
659        fn test_change_count() {
660            let mut tracker: ValueTracker<i64> = ValueTracker::new("test");
661            tracker.record(0, 1);
662            tracker.record(100, 1);
663            tracker.record(200, 2);
664            tracker.record(300, 2);
665            tracker.record(400, 3);
666
667            assert_eq!(tracker.change_count(), 2); // 1->2 and 2->3
668        }
669
670        #[test]
671        fn test_clear() {
672            let mut tracker: ValueTracker<f64> = ValueTracker::new("test");
673            tracker.record(0, 1.0);
674            tracker.record(100, 2.0);
675
676            tracker.clear();
677            assert!(tracker.is_empty());
678        }
679    }
680
681    mod value_tracker_f64_tests {
682        use super::*;
683
684        #[test]
685        fn test_rate_of_change() {
686            let mut tracker = ValueTracker::new("position");
687            tracker.record(0, 0.0);
688            tracker.record(1000, 100.0);
689
690            let rate = tracker.rate_of_change().unwrap();
691            assert!((rate - 0.1).abs() < 0.001); // 100 units / 1000 ms = 0.1 per ms
692        }
693
694        #[test]
695        fn test_rate_of_change_no_time() {
696            let mut tracker = ValueTracker::new("test");
697            tracker.record(100, 0.0);
698            tracker.record(100, 100.0); // Same timestamp
699
700            assert!(tracker.rate_of_change().is_none());
701        }
702
703        #[test]
704        fn test_is_increasing() {
705            let mut tracker = ValueTracker::new("score");
706            tracker.record(0, 0.0);
707            tracker.record(100, 10.0);
708            tracker.record(200, 20.0);
709
710            assert!(tracker.is_increasing());
711        }
712
713        #[test]
714        fn test_is_not_increasing() {
715            let mut tracker = ValueTracker::new("health");
716            tracker.record(0, 100.0);
717            tracker.record(100, 80.0);
718            tracker.record(200, 90.0);
719
720            assert!(!tracker.is_increasing());
721        }
722
723        #[test]
724        fn test_is_decreasing() {
725            let mut tracker = ValueTracker::new("health");
726            tracker.record(0, 100.0);
727            tracker.record(100, 80.0);
728            tracker.record(200, 60.0);
729
730            assert!(tracker.is_decreasing());
731        }
732
733        #[test]
734        fn test_min_max_average() {
735            let mut tracker = ValueTracker::new("test");
736            tracker.record(0, 10.0);
737            tracker.record(100, 20.0);
738            tracker.record(200, 30.0);
739
740            assert!((tracker.min().unwrap() - 10.0).abs() < f64::EPSILON);
741            assert!((tracker.max().unwrap() - 30.0).abs() < f64::EPSILON);
742            assert!((tracker.average().unwrap() - 20.0).abs() < f64::EPSILON);
743        }
744
745        #[test]
746        fn test_empty_stats() {
747            let tracker: ValueTracker<f64> = ValueTracker::new("empty");
748            assert!(tracker.min().is_none());
749            assert!(tracker.max().is_none());
750            assert!(tracker.average().is_none());
751            assert!(tracker.rate_of_change().is_none());
752        }
753    }
754
755    mod multi_value_tracker_tests {
756        use super::*;
757
758        #[test]
759        fn test_record_and_get() {
760            let mut multi = MultiValueTracker::new();
761            multi.record("score", 0, 100.0);
762            multi.record("health", 0, 100.0);
763
764            assert!(multi.get("score").is_some());
765            assert!(multi.get("health").is_some());
766            assert!(multi.get("missing").is_none());
767        }
768
769        #[test]
770        fn test_names() {
771            let mut multi = MultiValueTracker::new();
772            multi.record("a", 0, 1.0);
773            multi.record("b", 0, 2.0);
774
775            let names = multi.names();
776            assert_eq!(names.len(), 2);
777            assert!(names.contains(&"a"));
778            assert!(names.contains(&"b"));
779        }
780
781        #[test]
782        fn test_assert_bounds_pass() {
783            let mut multi = MultiValueTracker::new();
784            multi.record("health", 0, 100.0);
785            multi.record("health", 100, 80.0);
786
787            let mut bounds = HashMap::new();
788            bounds.insert("health".to_string(), (0.0, 100.0));
789
790            assert!(multi.assert_bounds(&bounds).is_ok());
791        }
792
793        #[test]
794        fn test_assert_bounds_fail() {
795            let mut multi = MultiValueTracker::new();
796            multi.record("health", 0, 150.0); // Above max
797
798            let mut bounds = HashMap::new();
799            bounds.insert("health".to_string(), (0.0, 100.0));
800
801            assert!(multi.assert_bounds(&bounds).is_err());
802        }
803
804        #[test]
805        fn test_default_implementation() {
806            let multi = MultiValueTracker::default();
807            assert!(multi.names().is_empty());
808        }
809
810        #[test]
811        fn test_assert_bounds_below_min() {
812            let mut multi = MultiValueTracker::new();
813            multi.record("health", 0, -10.0); // Below min
814
815            let mut bounds = HashMap::new();
816            bounds.insert("health".to_string(), (0.0, 100.0));
817
818            let result = multi.assert_bounds(&bounds);
819            assert!(result.is_err());
820            let err = result.unwrap_err();
821            match err {
822                ProbarError::AssertionFailed { message } => {
823                    assert!(message.contains("outside bounds"));
824                }
825                _ => panic!("Expected AssertionFailed error"),
826            }
827        }
828
829        #[test]
830        fn test_assert_bounds_missing_tracker() {
831            let multi = MultiValueTracker::new();
832            let mut bounds = HashMap::new();
833            bounds.insert("nonexistent".to_string(), (0.0, 100.0));
834
835            // Should pass because the tracker doesn't exist
836            assert!(multi.assert_bounds(&bounds).is_ok());
837        }
838
839        #[test]
840        fn test_record_multiple_values_same_tracker() {
841            let mut multi = MultiValueTracker::new();
842            multi.record("score", 0, 100.0);
843            multi.record("score", 100, 200.0);
844            multi.record("score", 200, 300.0);
845
846            let tracker = multi.get("score").unwrap();
847            assert_eq!(tracker.len(), 3);
848            assert_eq!(*tracker.latest().unwrap(), 300.0);
849        }
850    }
851
852    mod soft_assertion_edge_cases {
853        use super::*;
854
855        #[test]
856        fn test_soft_not_to_contain_text_fail() {
857            let frame = TuiFrame::from_lines(&["Hello World"]);
858            let mut assertion = expect_frame(&frame).soft();
859
860            let _ = assertion.not_to_contain_text("World");
861            assert_eq!(assertion.errors().len(), 1);
862            assert!(assertion.errors()[0].contains("NOT to contain"));
863        }
864
865        #[test]
866        fn test_soft_to_match_fail() {
867            let frame = TuiFrame::from_lines(&["No numbers here"]);
868            let mut assertion = expect_frame(&frame).soft();
869
870            let _ = assertion.to_match(r"\d+");
871            assert_eq!(assertion.errors().len(), 1);
872            assert!(assertion.errors()[0].contains("match pattern"));
873        }
874
875        #[test]
876        fn test_soft_to_have_size_fail() {
877            let frame = TuiFrame::from_lines(&["Short"]);
878            let mut assertion = expect_frame(&frame).soft();
879
880            let _ = assertion.to_have_size(100, 100);
881            assert_eq!(assertion.errors().len(), 1);
882            assert!(assertion.errors()[0].contains("Expected frame size"));
883        }
884
885        #[test]
886        fn test_soft_to_be_identical_to_fail() {
887            let frame1 = TuiFrame::from_lines(&["Frame A"]);
888            let frame2 = TuiFrame::from_lines(&["Frame B"]);
889            let mut assertion = expect_frame(&frame1).soft();
890
891            let _ = assertion.to_be_identical_to(&frame2);
892            assert_eq!(assertion.errors().len(), 1);
893            assert!(assertion.errors()[0].contains("not identical"));
894        }
895
896        #[test]
897        fn test_soft_line_to_equal_nonexistent() {
898            let frame = TuiFrame::from_lines(&["Only line"]);
899            let mut assertion = expect_frame(&frame).soft();
900
901            let _ = assertion.line_to_equal(10, "anything");
902            assert_eq!(assertion.errors().len(), 1);
903            assert!(assertion.errors()[0].contains("does not exist"));
904        }
905
906        #[test]
907        fn test_soft_line_to_equal_mismatch() {
908            let frame = TuiFrame::from_lines(&["Actual"]);
909            let mut assertion = expect_frame(&frame).soft();
910
911            let _ = assertion.line_to_equal(0, "Expected");
912            assert_eq!(assertion.errors().len(), 1);
913            assert!(assertion.errors()[0].contains("Expected line 0 to equal"));
914        }
915
916        #[test]
917        fn test_soft_line_to_contain_nonexistent() {
918            let frame = TuiFrame::from_lines(&["Only line"]);
919            let mut assertion = expect_frame(&frame).soft();
920
921            let _ = assertion.line_to_contain(10, "anything");
922            assert_eq!(assertion.errors().len(), 1);
923            assert!(assertion.errors()[0].contains("does not exist"));
924        }
925
926        #[test]
927        fn test_soft_line_to_contain_mismatch() {
928            let frame = TuiFrame::from_lines(&["Actual content"]);
929            let mut assertion = expect_frame(&frame).soft();
930
931            let _ = assertion.line_to_contain(0, "Missing");
932            assert_eq!(assertion.errors().len(), 1);
933            assert!(assertion.errors()[0].contains("Expected line 0 to contain"));
934        }
935
936        #[test]
937        fn test_soft_multiple_mixed_errors() {
938            let frame = TuiFrame::from_lines(&["Hello"]);
939            let mut assertion = expect_frame(&frame).soft();
940
941            let _ = assertion.to_contain_text("Missing1");
942            let _ = assertion.not_to_contain_text("Hello");
943            let _ = assertion.to_have_size(999, 999);
944
945            assert_eq!(assertion.errors().len(), 3);
946            let result = assertion.finalize();
947            assert!(result.is_err());
948            let err_msg = format!("{:?}", result.unwrap_err());
949            assert!(err_msg.contains("3 assertion(s) failed"));
950        }
951    }
952
953    mod to_match_edge_cases {
954        use super::*;
955
956        #[test]
957        fn test_to_match_invalid_regex() {
958            let frame = TuiFrame::from_lines(&["Test"]);
959            let mut assertion = expect_frame(&frame);
960
961            // Invalid regex pattern
962            let result = assertion.to_match("[invalid");
963            assert!(result.is_err());
964        }
965    }
966
967    mod value_tracker_i64_tests {
968        use super::*;
969
970        #[test]
971        fn test_i64_is_increasing() {
972            let mut tracker: ValueTracker<i64> = ValueTracker::new("score");
973            tracker.record(0, 10);
974            tracker.record(100, 20);
975            tracker.record(200, 30);
976
977            assert!(tracker.is_increasing());
978        }
979
980        #[test]
981        fn test_i64_is_not_increasing() {
982            let mut tracker: ValueTracker<i64> = ValueTracker::new("health");
983            tracker.record(0, 100);
984            tracker.record(100, 50);
985            tracker.record(200, 80);
986
987            assert!(!tracker.is_increasing());
988        }
989
990        #[test]
991        fn test_i64_is_decreasing() {
992            let mut tracker: ValueTracker<i64> = ValueTracker::new("health");
993            tracker.record(0, 100);
994            tracker.record(100, 80);
995            tracker.record(200, 60);
996
997            assert!(tracker.is_decreasing());
998        }
999
1000        #[test]
1001        fn test_i64_is_not_decreasing() {
1002            let mut tracker: ValueTracker<i64> = ValueTracker::new("score");
1003            tracker.record(0, 10);
1004            tracker.record(100, 30);
1005            tracker.record(200, 20);
1006
1007            assert!(!tracker.is_decreasing());
1008        }
1009
1010        #[test]
1011        fn test_i64_single_value_is_increasing_and_decreasing() {
1012            let mut tracker: ValueTracker<i64> = ValueTracker::new("single");
1013            tracker.record(0, 50);
1014
1015            // With less than 2 values, both should return true
1016            assert!(tracker.is_increasing());
1017            assert!(tracker.is_decreasing());
1018        }
1019
1020        #[test]
1021        fn test_i64_empty_is_increasing_and_decreasing() {
1022            let tracker: ValueTracker<i64> = ValueTracker::new("empty");
1023
1024            // Empty tracker should return true for both
1025            assert!(tracker.is_increasing());
1026            assert!(tracker.is_decreasing());
1027        }
1028
1029        #[test]
1030        fn test_i64_equal_values_is_both() {
1031            let mut tracker: ValueTracker<i64> = ValueTracker::new("constant");
1032            tracker.record(0, 50);
1033            tracker.record(100, 50);
1034            tracker.record(200, 50);
1035
1036            // Equal values satisfy both >= and <=
1037            assert!(tracker.is_increasing());
1038            assert!(tracker.is_decreasing());
1039        }
1040    }
1041
1042    mod value_tracker_additional_tests {
1043        use super::*;
1044
1045        #[test]
1046        fn test_values_accessor() {
1047            let mut tracker: ValueTracker<f64> = ValueTracker::new("test");
1048            tracker.record(0, 1.0);
1049            tracker.record(100, 2.0);
1050            tracker.record(200, 3.0);
1051
1052            let values = tracker.values();
1053            assert_eq!(values.len(), 3);
1054            assert_eq!(values[0], (0, 1.0));
1055            assert_eq!(values[1], (100, 2.0));
1056            assert_eq!(values[2], (200, 3.0));
1057        }
1058
1059        #[test]
1060        fn test_latest_empty() {
1061            let tracker: ValueTracker<f64> = ValueTracker::new("empty");
1062            assert!(tracker.latest().is_none());
1063        }
1064
1065        #[test]
1066        fn test_change_count_empty() {
1067            let tracker: ValueTracker<i64> = ValueTracker::new("empty");
1068            assert_eq!(tracker.change_count(), 0);
1069        }
1070
1071        #[test]
1072        fn test_change_count_single_value() {
1073            let mut tracker: ValueTracker<i64> = ValueTracker::new("single");
1074            tracker.record(0, 100);
1075            assert_eq!(tracker.change_count(), 0);
1076        }
1077
1078        #[test]
1079        fn test_change_count_no_changes() {
1080            let mut tracker: ValueTracker<i64> = ValueTracker::new("constant");
1081            tracker.record(0, 100);
1082            tracker.record(100, 100);
1083            tracker.record(200, 100);
1084            assert_eq!(tracker.change_count(), 0);
1085        }
1086
1087        #[test]
1088        fn test_f64_single_value_is_increasing_and_decreasing() {
1089            let mut tracker: ValueTracker<f64> = ValueTracker::new("single");
1090            tracker.record(0, 50.0);
1091
1092            assert!(tracker.is_increasing());
1093            assert!(tracker.is_decreasing());
1094        }
1095
1096        #[test]
1097        fn test_f64_empty_is_increasing_and_decreasing() {
1098            let tracker: ValueTracker<f64> = ValueTracker::new("empty");
1099
1100            assert!(tracker.is_increasing());
1101            assert!(tracker.is_decreasing());
1102        }
1103
1104        #[test]
1105        fn test_f64_is_not_decreasing() {
1106            let mut tracker: ValueTracker<f64> = ValueTracker::new("up_down");
1107            tracker.record(0, 10.0);
1108            tracker.record(100, 20.0);
1109            tracker.record(200, 15.0);
1110
1111            assert!(!tracker.is_decreasing());
1112        }
1113
1114        #[test]
1115        fn test_rate_of_change_single_value() {
1116            let mut tracker: ValueTracker<f64> = ValueTracker::new("single");
1117            tracker.record(0, 100.0);
1118            assert!(tracker.rate_of_change().is_none());
1119        }
1120
1121        #[test]
1122        fn test_rate_of_change_negative() {
1123            let mut tracker: ValueTracker<f64> = ValueTracker::new("decreasing");
1124            tracker.record(0, 100.0);
1125            tracker.record(1000, 0.0);
1126
1127            let rate = tracker.rate_of_change().unwrap();
1128            assert!((rate - (-0.1)).abs() < 0.001);
1129        }
1130    }
1131
1132    mod frame_assertion_error_messages {
1133        use super::*;
1134
1135        #[test]
1136        fn test_to_contain_text_error_shows_content() {
1137            let frame = TuiFrame::from_lines(&["Line one", "Line two"]);
1138            let mut assertion = expect_frame(&frame);
1139
1140            let result = assertion.to_contain_text("NotFound");
1141            assert!(result.is_err());
1142            let err = result.unwrap_err();
1143            match err {
1144                ProbarError::AssertionFailed { message } => {
1145                    assert!(message.contains("NotFound"));
1146                    assert!(message.contains("Frame content:"));
1147                    assert!(message.contains("Line one"));
1148                }
1149                _ => panic!("Expected AssertionFailed error"),
1150            }
1151        }
1152
1153        #[test]
1154        fn test_not_to_contain_text_error_shows_content() {
1155            let frame = TuiFrame::from_lines(&["Hello World"]);
1156            let mut assertion = expect_frame(&frame);
1157
1158            let result = assertion.not_to_contain_text("Hello");
1159            assert!(result.is_err());
1160            let err = result.unwrap_err();
1161            match err {
1162                ProbarError::AssertionFailed { message } => {
1163                    assert!(message.contains("NOT to contain"));
1164                    assert!(message.contains("Hello"));
1165                }
1166                _ => panic!("Expected AssertionFailed error"),
1167            }
1168        }
1169
1170        #[test]
1171        fn test_to_match_error_shows_pattern() {
1172            let frame = TuiFrame::from_lines(&["No numbers"]);
1173            let mut assertion = expect_frame(&frame);
1174
1175            let result = assertion.to_match(r"\d+");
1176            assert!(result.is_err());
1177            let err = result.unwrap_err();
1178            match err {
1179                ProbarError::AssertionFailed { message } => {
1180                    assert!(message.contains(r"\d+"));
1181                    assert!(message.contains("No numbers"));
1182                }
1183                _ => panic!("Expected AssertionFailed error"),
1184            }
1185        }
1186
1187        #[test]
1188        fn test_line_to_contain_error_shows_actual() {
1189            let frame = TuiFrame::from_lines(&["Actual content"]);
1190            let mut assertion = expect_frame(&frame);
1191
1192            let result = assertion.line_to_contain(0, "Missing");
1193            assert!(result.is_err());
1194            let err = result.unwrap_err();
1195            match err {
1196                ProbarError::AssertionFailed { message } => {
1197                    assert!(message.contains("Expected line 0"));
1198                    assert!(message.contains("Missing"));
1199                    assert!(message.contains("Actual content"));
1200                }
1201                _ => panic!("Expected AssertionFailed error"),
1202            }
1203        }
1204
1205        #[test]
1206        fn test_line_to_equal_error_shows_actual() {
1207            let frame = TuiFrame::from_lines(&["Actual"]);
1208            let mut assertion = expect_frame(&frame);
1209
1210            let result = assertion.line_to_equal(0, "Expected");
1211            assert!(result.is_err());
1212            let err = result.unwrap_err();
1213            match err {
1214                ProbarError::AssertionFailed { message } => {
1215                    assert!(message.contains("Expected line 0 to equal"));
1216                    assert!(message.contains("Expected"));
1217                    assert!(message.contains("Actual"));
1218                }
1219                _ => panic!("Expected AssertionFailed error"),
1220            }
1221        }
1222
1223        #[test]
1224        fn test_line_to_equal_nonexistent_line_error() {
1225            let frame = TuiFrame::from_lines(&["Only line"]);
1226            let mut assertion = expect_frame(&frame);
1227
1228            let result = assertion.line_to_equal(5, "Anything");
1229            assert!(result.is_err());
1230            let err = result.unwrap_err();
1231            match err {
1232                ProbarError::AssertionFailed { message } => {
1233                    assert!(message.contains("Line 5 does not exist"));
1234                    assert!(message.contains("1 lines"));
1235                }
1236                _ => panic!("Expected AssertionFailed error"),
1237            }
1238        }
1239
1240        #[test]
1241        fn test_to_have_size_error_shows_dimensions() {
1242            let frame = TuiFrame::from_lines(&["Short"]);
1243            let mut assertion = expect_frame(&frame);
1244
1245            let result = assertion.to_have_size(100, 50);
1246            assert!(result.is_err());
1247            let err = result.unwrap_err();
1248            match err {
1249                ProbarError::AssertionFailed { message } => {
1250                    assert!(message.contains("100x50"));
1251                    assert!(message.contains("5x1"));
1252                }
1253                _ => panic!("Expected AssertionFailed error"),
1254            }
1255        }
1256
1257        #[test]
1258        fn test_to_be_identical_to_error_shows_diff() {
1259            let frame1 = TuiFrame::from_lines(&["Line A"]);
1260            let frame2 = TuiFrame::from_lines(&["Line B"]);
1261            let mut assertion = expect_frame(&frame1);
1262
1263            let result = assertion.to_be_identical_to(&frame2);
1264            assert!(result.is_err());
1265            let err = result.unwrap_err();
1266            match err {
1267                ProbarError::AssertionFailed { message } => {
1268                    assert!(message.contains("not identical"));
1269                }
1270                _ => panic!("Expected AssertionFailed error"),
1271            }
1272        }
1273    }
1274
1275    mod frame_assertion_debug {
1276        use super::*;
1277
1278        #[test]
1279        fn test_frame_assertion_debug() {
1280            let frame = TuiFrame::from_lines(&["Test"]);
1281            let assertion = expect_frame(&frame);
1282            let debug = format!("{:?}", assertion);
1283            assert!(debug.contains("FrameAssertion"));
1284        }
1285    }
1286
1287    mod value_tracker_clone {
1288        use super::*;
1289
1290        #[test]
1291        fn test_value_tracker_clone() {
1292            let mut tracker: ValueTracker<f64> = ValueTracker::new("test");
1293            tracker.record(0, 1.0);
1294            tracker.record(100, 2.0);
1295
1296            let cloned = tracker.clone();
1297            assert_eq!(cloned.name(), tracker.name());
1298            assert_eq!(cloned.len(), tracker.len());
1299            assert_eq!(cloned.latest(), tracker.latest());
1300        }
1301
1302        #[test]
1303        fn test_value_tracker_debug() {
1304            let mut tracker: ValueTracker<i64> = ValueTracker::new("debug_test");
1305            tracker.record(0, 42);
1306            let debug = format!("{:?}", tracker);
1307            assert!(debug.contains("ValueTracker"));
1308            assert!(debug.contains("debug_test"));
1309        }
1310    }
1311
1312    mod multi_value_tracker_debug {
1313        use super::*;
1314
1315        #[test]
1316        fn test_multi_value_tracker_debug() {
1317            let mut multi = MultiValueTracker::new();
1318            multi.record("test", 0, 1.0);
1319            let debug = format!("{:?}", multi);
1320            assert!(debug.contains("MultiValueTracker"));
1321        }
1322    }
1323}