1use super::buffer::TextGrid;
9use crate::result::{ProbarError, ProbarResult};
10use serde::{Deserialize, Serialize};
11use std::fmt;
12
13#[derive(Clone, Serialize, Deserialize)]
15pub struct TuiFrame {
16 content: Vec<String>,
18 width: u16,
20 height: u16,
22 timestamp_ms: u64,
24}
25
26impl TuiFrame {
27 #[must_use]
29 pub fn from_grid(grid: &TextGrid, timestamp_ms: u64) -> Self {
30 Self {
31 content: grid.to_lines(),
32 width: grid.width(),
33 height: grid.height(),
34 timestamp_ms,
35 }
36 }
37
38 #[must_use]
40 pub fn from_lines(lines: &[&str]) -> Self {
41 let height = lines.len() as u16;
42 let width = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0) as u16;
44 let content = lines.iter().map(|s| (*s).to_string()).collect();
45
46 Self {
47 content,
48 width,
49 height,
50 timestamp_ms: 0,
51 }
52 }
53
54 #[must_use]
56 pub fn width(&self) -> u16 {
57 self.width
58 }
59
60 #[must_use]
62 pub fn height(&self) -> u16 {
63 self.height
64 }
65
66 #[must_use]
68 pub fn timestamp_ms(&self) -> u64 {
69 self.timestamp_ms
70 }
71
72 #[must_use]
74 pub fn lines(&self) -> &[String] {
75 &self.content
76 }
77
78 #[must_use]
80 pub fn as_text(&self) -> String {
81 self.content.join("\n")
82 }
83
84 #[must_use]
86 pub fn contains(&self, text: &str) -> bool {
87 self.content.iter().any(|line| line.contains(text))
88 }
89
90 #[must_use]
92 pub fn matches(&self, pattern: &str) -> ProbarResult<bool> {
93 let re = regex::Regex::new(pattern).map_err(|e| ProbarError::TuiError {
94 message: format!("Invalid regex pattern: {e}"),
95 })?;
96 Ok(self.content.iter().any(|line| re.is_match(line)))
97 }
98
99 #[must_use]
101 pub fn find_matches(&self, pattern: &str) -> ProbarResult<Vec<&str>> {
102 let re = regex::Regex::new(pattern).map_err(|e| ProbarError::TuiError {
103 message: format!("Invalid regex pattern: {e}"),
104 })?;
105 Ok(self
106 .content
107 .iter()
108 .filter(|line| re.is_match(line))
109 .map(String::as_str)
110 .collect())
111 }
112
113 #[must_use]
115 pub fn line(&self, index: usize) -> Option<&str> {
116 self.content.get(index).map(String::as_str)
117 }
118
119 #[must_use]
121 pub fn is_identical(&self, other: &TuiFrame) -> bool {
122 self.content == other.content
123 }
124
125 #[must_use]
127 pub fn diff(&self, other: &TuiFrame) -> FrameDiff {
128 let mut changed_lines = Vec::new();
129
130 let max_lines = self.content.len().max(other.content.len());
131 for i in 0..max_lines {
132 let self_line = self.content.get(i).map(String::as_str).unwrap_or("");
133 let other_line = other.content.get(i).map(String::as_str).unwrap_or("");
134
135 if self_line != other_line {
136 changed_lines.push(LineDiff {
137 line_number: i,
138 expected: self_line.to_string(),
139 actual: other_line.to_string(),
140 });
141 }
142 }
143
144 FrameDiff {
145 is_identical: changed_lines.is_empty(),
146 changed_lines,
147 }
148 }
149}
150
151impl fmt::Debug for TuiFrame {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 writeln!(f, "TuiFrame({}x{}):", self.width, self.height)?;
154 for (i, line) in self.content.iter().enumerate() {
155 writeln!(f, " {i:3}: {line}")?;
156 }
157 Ok(())
158 }
159}
160
161#[derive(Debug, Clone)]
163pub struct FrameDiff {
164 pub is_identical: bool,
166 pub changed_lines: Vec<LineDiff>,
168}
169
170#[derive(Debug, Clone)]
172pub struct LineDiff {
173 pub line_number: usize,
175 pub expected: String,
177 pub actual: String,
179}
180
181impl fmt::Display for FrameDiff {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 if self.is_identical {
184 write!(f, "Frames are identical")
185 } else {
186 writeln!(f, "Frame differences:")?;
187 for diff in &self.changed_lines {
188 writeln!(f, " Line {}: ", diff.line_number)?;
189 writeln!(f, " Expected: {:?}", diff.expected)?;
190 writeln!(f, " Actual: {:?}", diff.actual)?;
191 }
192 Ok(())
193 }
194 }
195}
196
197#[derive(Debug)]
202pub struct TuiTestBackend {
203 grid: TextGrid,
204 frames: Vec<TuiFrame>,
205 start_time: std::time::Instant,
206}
207
208impl TuiTestBackend {
209 #[must_use]
211 pub fn new(width: u16, height: u16) -> Self {
212 Self {
213 grid: TextGrid::new(width, height),
214 frames: Vec::new(),
215 start_time: std::time::Instant::now(),
216 }
217 }
218
219 #[must_use]
221 pub fn size(&self) -> (u16, u16) {
222 (self.grid.width(), self.grid.height())
223 }
224
225 #[must_use]
227 pub fn grid(&self) -> &TextGrid {
228 &self.grid
229 }
230
231 #[must_use]
233 pub fn grid_mut(&mut self) -> &mut TextGrid {
234 &mut self.grid
235 }
236
237 pub fn capture_frame(&mut self) -> TuiFrame {
239 let timestamp = self.start_time.elapsed().as_millis() as u64;
240 let frame = TuiFrame::from_grid(&self.grid, timestamp);
241 self.frames.push(frame.clone());
242 frame
243 }
244
245 #[must_use]
247 pub fn current_frame(&self) -> TuiFrame {
248 let timestamp = self.start_time.elapsed().as_millis() as u64;
249 TuiFrame::from_grid(&self.grid, timestamp)
250 }
251
252 #[must_use]
254 pub fn frames(&self) -> &[TuiFrame] {
255 &self.frames
256 }
257
258 #[must_use]
260 pub fn frame_count(&self) -> usize {
261 self.frames.len()
262 }
263
264 pub fn clear(&mut self) {
266 self.grid.clear();
267 }
268
269 pub fn reset(&mut self) {
271 self.grid.clear();
272 self.frames.clear();
273 self.start_time = std::time::Instant::now();
274 }
275
276 pub fn resize(&mut self, width: u16, height: u16) {
278 self.grid.resize(width, height);
279 }
280
281 pub fn write_text(&mut self, x: u16, y: u16, text: &str) {
283 self.grid.write_str(x, y, text);
284 }
285
286 pub fn write_lines(&mut self, x: u16, y: u16, lines: &[&str]) {
288 for (i, line) in lines.iter().enumerate() {
289 self.grid.write_str(x, y + i as u16, line);
290 }
291 }
292}
293
294impl Default for TuiTestBackend {
295 fn default() -> Self {
296 Self::new(80, 24) }
298}
299
300#[cfg(test)]
301#[allow(clippy::unwrap_used, clippy::expect_used)]
302mod tests {
303 use super::*;
304
305 mod tui_frame_tests {
306 use super::*;
307
308 #[test]
309 fn test_from_lines() {
310 let frame = TuiFrame::from_lines(&["Hello", "World"]);
311 assert_eq!(frame.width(), 5);
312 assert_eq!(frame.height(), 2);
313 assert_eq!(frame.lines(), &["Hello", "World"]);
314 }
315
316 #[test]
317 fn test_from_grid() {
318 let mut grid = TextGrid::new(10, 3);
319 grid.write_str(0, 0, "Hello");
320 grid.write_str(0, 1, "World");
321
322 let frame = TuiFrame::from_grid(&grid, 100);
323 assert_eq!(frame.width(), 10);
324 assert_eq!(frame.height(), 3);
325 assert_eq!(frame.timestamp_ms(), 100);
326 assert!(frame.contains("Hello"));
327 assert!(frame.contains("World"));
328 }
329
330 #[test]
331 fn test_as_text() {
332 let frame = TuiFrame::from_lines(&["Line 1", "Line 2"]);
333 assert_eq!(frame.as_text(), "Line 1\nLine 2");
334 }
335
336 #[test]
337 fn test_contains() {
338 let frame = TuiFrame::from_lines(&["Hello World", "Goodbye"]);
339 assert!(frame.contains("World"));
340 assert!(frame.contains("Goodbye"));
341 assert!(!frame.contains("Missing"));
342 }
343
344 #[test]
345 fn test_matches_regex() {
346 let frame = TuiFrame::from_lines(&["Score: 100", "Lives: 3"]);
347 assert!(frame.matches(r"Score: \d+").unwrap());
348 assert!(frame.matches(r"Lives: \d").unwrap());
349 assert!(!frame.matches(r"Health: \d+").unwrap());
350 }
351
352 #[test]
353 fn test_find_matches() {
354 let frame = TuiFrame::from_lines(&["Error: failed", "Warning: slow", "Info: ok"]);
355 let errors = frame.find_matches(r"Error:.*").unwrap();
356 assert_eq!(errors.len(), 1);
357 assert_eq!(errors[0], "Error: failed");
358 }
359
360 #[test]
361 fn test_line_access() {
362 let frame = TuiFrame::from_lines(&["First", "Second", "Third"]);
363 assert_eq!(frame.line(0), Some("First"));
364 assert_eq!(frame.line(1), Some("Second"));
365 assert_eq!(frame.line(2), Some("Third"));
366 assert_eq!(frame.line(3), None);
367 }
368
369 #[test]
370 fn test_is_identical() {
371 let frame1 = TuiFrame::from_lines(&["Same", "Content"]);
372 let frame2 = TuiFrame::from_lines(&["Same", "Content"]);
373 let frame3 = TuiFrame::from_lines(&["Different", "Content"]);
374
375 assert!(frame1.is_identical(&frame2));
376 assert!(!frame1.is_identical(&frame3));
377 }
378
379 #[test]
380 fn test_diff() {
381 let frame1 = TuiFrame::from_lines(&["Same", "Different1"]);
382 let frame2 = TuiFrame::from_lines(&["Same", "Different2"]);
383
384 let diff = frame1.diff(&frame2);
385 assert!(!diff.is_identical);
386 assert_eq!(diff.changed_lines.len(), 1);
387 assert_eq!(diff.changed_lines[0].line_number, 1);
388 assert_eq!(diff.changed_lines[0].expected, "Different1");
389 assert_eq!(diff.changed_lines[0].actual, "Different2");
390 }
391
392 #[test]
393 fn test_diff_identical() {
394 let frame1 = TuiFrame::from_lines(&["Same", "Same"]);
395 let frame2 = TuiFrame::from_lines(&["Same", "Same"]);
396
397 let diff = frame1.diff(&frame2);
398 assert!(diff.is_identical);
399 assert!(diff.changed_lines.is_empty());
400 }
401 }
402
403 mod tui_test_backend_tests {
404 use super::*;
405
406 #[test]
407 fn test_new() {
408 let backend = TuiTestBackend::new(80, 24);
409 assert_eq!(backend.size(), (80, 24));
410 assert_eq!(backend.frame_count(), 0);
411 }
412
413 #[test]
414 fn test_default() {
415 let backend = TuiTestBackend::default();
416 assert_eq!(backend.size(), (80, 24));
417 }
418
419 #[test]
420 fn test_write_text() {
421 let mut backend = TuiTestBackend::new(20, 5);
422 backend.write_text(0, 0, "Hello");
423
424 let frame = backend.current_frame();
425 assert!(frame.contains("Hello"));
426 }
427
428 #[test]
429 fn test_write_lines() {
430 let mut backend = TuiTestBackend::new(20, 5);
431 backend.write_lines(0, 0, &["Line 1", "Line 2"]);
432
433 let frame = backend.current_frame();
434 assert!(frame.contains("Line 1"));
435 assert!(frame.contains("Line 2"));
436 }
437
438 #[test]
439 fn test_capture_frame() {
440 let mut backend = TuiTestBackend::new(20, 5);
441 backend.write_text(0, 0, "Test");
442
443 let frame = backend.capture_frame();
444 assert!(frame.contains("Test"));
445 assert_eq!(backend.frame_count(), 1);
446
447 backend.write_text(0, 1, "More");
448 let _ = backend.capture_frame();
449 assert_eq!(backend.frame_count(), 2);
450 }
451
452 #[test]
453 fn test_frames() {
454 let mut backend = TuiTestBackend::new(20, 5);
455
456 backend.write_text(0, 0, "Frame1");
457 let _ = backend.capture_frame();
458
459 backend.write_text(0, 1, "Frame2");
460 let _ = backend.capture_frame();
461
462 let frames = backend.frames();
463 assert_eq!(frames.len(), 2);
464 assert!(frames[0].contains("Frame1"));
465 assert!(frames[1].contains("Frame2"));
466 }
467
468 #[test]
469 fn test_clear() {
470 let mut backend = TuiTestBackend::new(20, 5);
471 backend.write_text(0, 0, "Hello");
472 backend.clear();
473
474 let frame = backend.current_frame();
475 assert!(!frame.contains("Hello"));
476 }
477
478 #[test]
479 fn test_reset() {
480 let mut backend = TuiTestBackend::new(20, 5);
481 backend.write_text(0, 0, "Hello");
482 let _ = backend.capture_frame();
483
484 backend.reset();
485 assert_eq!(backend.frame_count(), 0);
486 assert!(!backend.current_frame().contains("Hello"));
487 }
488
489 #[test]
490 fn test_resize() {
491 let mut backend = TuiTestBackend::new(20, 5);
492 backend.resize(40, 10);
493 assert_eq!(backend.size(), (40, 10));
494 }
495
496 #[test]
497 fn test_grid_access() {
498 let mut backend = TuiTestBackend::new(20, 5);
499
500 assert_eq!(backend.grid().width(), 20);
502
503 backend.grid_mut().set(0, 0, 'X');
505 assert_eq!(backend.grid().get(0, 0), Some('X'));
506 }
507 }
508}