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