use crate::render::{Buffer, Modifier};
use crate::style::Color;
use crate::testing::visual::types::{CapturedCell, VisualCapture};
use crate::testing::visual::{comparison::color_to_rgb, helpers::parse_hex_color};
use std::collections::HashMap;
use std::path::Path;
impl VisualCapture {
pub fn from_buffer(buffer: &Buffer, include_styles: bool, include_colors: bool) -> Self {
let width = buffer.width();
let height = buffer.height();
let mut cells = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
let cell = if let Some(buf_cell) = buffer.get(x, y) {
CapturedCell {
symbol: buf_cell.symbol,
fg: if include_colors { buf_cell.fg } else { None },
bg: if include_colors { buf_cell.bg } else { None },
bold: include_styles && buf_cell.modifier.contains(Modifier::BOLD),
italic: include_styles && buf_cell.modifier.contains(Modifier::ITALIC),
underline: include_styles
&& buf_cell.modifier.contains(Modifier::UNDERLINE),
dim: include_styles && buf_cell.modifier.contains(Modifier::DIM),
}
} else {
CapturedCell::default()
};
cells.push(cell);
}
}
Self {
width,
height,
cells,
include_styles,
include_colors,
}
}
pub fn get(&self, x: u16, y: u16) -> Option<&CapturedCell> {
if x < self.width && y < self.height {
let idx = (y * self.width + x) as usize;
self.cells.get(idx)
} else {
None
}
}
pub fn diff(&self, other: &Self, tolerance: u8) -> crate::testing::visual::types::VisualDiff {
let mut differences = Vec::new();
if self.width != other.width || self.height != other.height {
return crate::testing::visual::types::VisualDiff {
size_mismatch: Some(((self.width, self.height), (other.width, other.height))),
differences,
actual_width: self.width,
actual_height: self.height,
expected_width: other.width,
expected_height: other.height,
};
}
for y in 0..self.height {
for x in 0..self.width {
let Some(actual) = self.get(x, y) else {
continue;
};
let Some(expected) = other.get(x, y) else {
continue;
};
if !actual.matches(
expected,
tolerance,
self.include_styles,
self.include_colors,
) {
differences.push(crate::testing::visual::types::CellDiff {
x,
y,
actual: actual.clone(),
expected: expected.clone(),
});
}
}
}
crate::testing::visual::types::VisualDiff {
size_mismatch: None,
differences,
actual_width: self.width,
actual_height: self.height,
expected_width: other.width,
expected_height: other.height,
}
}
pub fn save(&self, path: &Path) -> std::io::Result<()> {
let content = self.serialize();
std::fs::write(path, content)
}
pub fn load(path: &Path) -> std::io::Result<Self> {
let content = std::fs::read_to_string(path)?;
Self::deserialize(&content)
}
pub fn serialize(&self) -> String {
let mut output = String::new();
output.push_str(&format!(
"# Visual Golden File\n# Size: {}x{}\n# Styles: {}\n# Colors: {}\n\n",
self.width, self.height, self.include_styles, self.include_colors
));
output.push_str("## Text\n");
for y in 0..self.height {
for x in 0..self.width {
if let Some(cell) = self.get(x, y) {
output.push(cell.symbol);
} else {
output.push(' ');
}
}
output.push('\n');
}
if self.include_colors {
output.push_str("\n## Colors\n");
for y in 0..self.height {
for x in 0..self.width {
if let Some(cell) = self.get(x, y) {
if let Some(fg) = &cell.fg {
let (r, g, b) = color_to_rgb(fg);
output.push_str(&format!(
"{}:{},{}:#{:02x}{:02x}{:02x} ",
x, y, "fg", r, g, b
));
}
if let Some(bg) = &cell.bg {
let (r, g, b) = color_to_rgb(bg);
output.push_str(&format!(
"{}:{},{}:#{:02x}{:02x}{:02x} ",
x, y, "bg", r, g, b
));
}
}
}
output.push('\n');
}
}
if self.include_styles {
output.push_str("\n## Styles\n");
for y in 0..self.height {
for x in 0..self.width {
if let Some(cell) = self.get(x, y) {
let mut styles = Vec::new();
if cell.bold {
styles.push("B");
}
if cell.italic {
styles.push("I");
}
if cell.underline {
styles.push("U");
}
if cell.dim {
styles.push("D");
}
if !styles.is_empty() {
output.push_str(&format!("{}:{}:{} ", x, y, styles.join("")));
}
}
}
output.push('\n');
}
}
output
}
pub fn deserialize(content: &str) -> std::io::Result<Self> {
let mut width = 0u16;
let mut height = 0u16;
let mut include_styles = true;
let mut include_colors = true;
let mut cells = Vec::new();
let mut in_text = false;
let mut in_colors = false;
let mut in_styles = false;
let mut text_lines: Vec<String> = Vec::new();
let mut color_data: HashMap<(u16, u16, String), (u8, u8, u8)> = HashMap::new();
let mut style_data: HashMap<(u16, u16), (bool, bool, bool, bool)> = HashMap::new();
for line in content.lines() {
let line = line.trim_end();
if line.starts_with("# Size:") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let size_parts: Vec<&str> = parts[2].split('x').collect();
if size_parts.len() == 2 {
width = size_parts[0].parse().unwrap_or(0);
height = size_parts[1].parse().unwrap_or(0);
}
}
continue;
}
if line.starts_with("# Styles:") {
include_styles = line.contains("true");
continue;
}
if line.starts_with("# Colors:") {
include_colors = line.contains("true");
continue;
}
if line == "## Text" {
in_text = true;
in_colors = false;
in_styles = false;
continue;
}
if line == "## Colors" {
in_text = false;
in_colors = true;
in_styles = false;
continue;
}
if line == "## Styles" {
in_text = false;
in_colors = false;
in_styles = true;
continue;
}
if line.starts_with('#') || line.is_empty() {
continue;
}
if in_text {
text_lines.push(line.to_string());
} else if in_colors {
for part in line.split_whitespace() {
if let Some((coord, color)) = part.split_once(',') {
if let Some((pos, hex)) = coord.split_once(':') {
if let Some((kind, hex_val)) = color.split_once(':') {
let x: u16 = pos.parse().unwrap_or(0);
let y: u16 = hex.parse().unwrap_or(0);
if let Some(rgb) = parse_hex_color(hex_val) {
color_data.insert((x, y, kind.to_string()), rgb);
}
}
}
}
}
} else if in_styles {
for part in line.split_whitespace() {
let parts: Vec<&str> = part.split(':').collect();
if parts.len() >= 3 {
let x: u16 = parts[0].parse().unwrap_or(0);
let y: u16 = parts[1].parse().unwrap_or(0);
let flags = parts[2];
style_data.insert(
(x, y),
(
flags.contains('B'),
flags.contains('I'),
flags.contains('U'),
flags.contains('D'),
),
);
}
}
}
}
if height == 0 {
height = text_lines.len() as u16;
}
if width == 0 && !text_lines.is_empty() {
width = text_lines
.iter()
.map(|l| l.chars().count())
.max()
.unwrap_or(0) as u16;
}
for y in 0..height {
let line = text_lines.get(y as usize).map(|s| s.as_str()).unwrap_or("");
let chars: Vec<char> = line.chars().collect();
for x in 0..width {
let symbol = chars.get(x as usize).copied().unwrap_or(' ');
let fg = color_data
.get(&(x, y, "fg".to_string()))
.map(|(r, g, b)| Color::rgb(*r, *g, *b));
let bg = color_data
.get(&(x, y, "bg".to_string()))
.map(|(r, g, b)| Color::rgb(*r, *g, *b));
let (bold, italic, underline, dim) = style_data
.get(&(x, y))
.copied()
.unwrap_or((false, false, false, false));
cells.push(CapturedCell {
symbol,
fg,
bg,
bold,
italic,
underline,
dim,
});
}
}
Ok(Self {
width,
height,
cells,
include_styles,
include_colors,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::Cell;
fn create_test_buffer(width: u16, height: u16) -> Buffer {
let mut buffer = Buffer::new(width, height);
for y in 0..height {
for x in 0..width {
let digit = ((x + y) % 10) as u8 + b'0';
let mut cell = Cell::new(digit as char);
if x == 0 && y == 0 {
cell.fg = Some(Color::RED);
cell.modifier |= Modifier::BOLD;
}
if x == 1 && y == 0 {
cell.bg = Some(Color::BLUE);
cell.modifier |= Modifier::ITALIC;
}
buffer.set(x, y, cell);
}
}
buffer
}
#[test]
fn test_from_buffer_basic() {
let buffer = create_test_buffer(5, 3);
let capture = VisualCapture::from_buffer(&buffer, false, false);
assert_eq!(capture.width, 5);
assert_eq!(capture.height, 3);
assert_eq!(capture.cells.len(), 15);
assert!(!capture.include_styles);
assert!(!capture.include_colors);
}
#[test]
fn test_from_buffer_with_styles() {
let buffer = create_test_buffer(5, 3);
let capture = VisualCapture::from_buffer(&buffer, true, false);
assert!(capture.include_styles);
assert!(capture.cells[0].bold);
assert!(capture.cells[1].italic);
}
#[test]
fn test_from_buffer_with_colors() {
let buffer = create_test_buffer(5, 3);
let capture = VisualCapture::from_buffer(&buffer, false, true);
assert!(capture.include_colors);
assert_eq!(capture.cells[0].fg, Some(Color::RED));
assert_eq!(capture.cells[1].bg, Some(Color::BLUE));
}
#[test]
fn test_from_buffer_empty_cell() {
let buffer = Buffer::new(3, 3);
let capture = VisualCapture::from_buffer(&buffer, false, false);
assert_eq!(capture.cells.len(), 9);
}
#[test]
fn test_get_valid_position() {
let buffer = create_test_buffer(5, 3);
let capture = VisualCapture::from_buffer(&buffer, false, false);
assert!(capture.get(0, 0).is_some());
assert!(capture.get(4, 2).is_some());
assert!(capture.get(2, 1).is_some());
}
#[test]
fn test_get_out_of_bounds() {
let buffer = create_test_buffer(5, 3);
let capture = VisualCapture::from_buffer(&buffer, false, false);
assert!(capture.get(5, 0).is_none()); assert!(capture.get(0, 3).is_none()); assert!(capture.get(10, 10).is_none()); }
#[test]
fn test_get_index_calculation() {
let buffer = create_test_buffer(5, 3);
let capture = VisualCapture::from_buffer(&buffer, false, false);
let cell = capture.get(2, 1).unwrap();
assert_eq!(capture.cells[7].symbol, cell.symbol);
}
#[test]
fn test_diff_identical() {
let buffer = create_test_buffer(5, 3);
let capture1 = VisualCapture::from_buffer(&buffer, false, false);
let capture2 = VisualCapture::from_buffer(&buffer, false, false);
let diff = capture1.diff(&capture2, 0);
assert!(diff.size_mismatch.is_none());
assert!(diff.differences.is_empty());
}
#[test]
fn test_diff_size_mismatch() {
let buffer1 = create_test_buffer(5, 3);
let buffer2 = create_test_buffer(4, 2);
let capture1 = VisualCapture::from_buffer(&buffer1, false, false);
let capture2 = VisualCapture::from_buffer(&buffer2, false, false);
let diff = capture1.diff(&capture2, 0);
assert!(diff.size_mismatch.is_some());
assert_eq!(diff.actual_width, 5);
assert_eq!(diff.actual_height, 3);
assert_eq!(diff.expected_width, 4);
assert_eq!(diff.expected_height, 2);
}
#[test]
fn test_diff_cell_difference() {
let mut buffer1 = Buffer::new(3, 3);
let cell1 = Cell::new('A');
buffer1.set(0, 0, cell1);
let mut buffer2 = Buffer::new(3, 3);
let cell2 = Cell::new('B');
buffer2.set(0, 0, cell2);
let capture1 = VisualCapture::from_buffer(&buffer1, false, false);
let capture2 = VisualCapture::from_buffer(&buffer2, false, false);
let diff = capture1.diff(&capture2, 0);
assert!(diff.size_mismatch.is_none());
assert_eq!(diff.differences.len(), 1);
assert_eq!(diff.differences[0].x, 0);
assert_eq!(diff.differences[0].y, 0);
}
#[test]
fn test_serialize_text_section() {
let buffer = create_test_buffer(3, 2);
let capture = VisualCapture::from_buffer(&buffer, false, false);
let serialized = capture.serialize();
assert!(serialized.contains("# Visual Golden File"));
assert!(serialized.contains("# Size: 3x2"));
assert!(serialized.contains("## Text"));
}
#[test]
fn test_serialize_with_styles() {
let buffer = create_test_buffer(3, 2);
let capture = VisualCapture::from_buffer(&buffer, true, false);
let serialized = capture.serialize();
assert!(serialized.contains("# Styles: true"));
assert!(serialized.contains("## Styles"));
}
#[test]
fn test_serialize_with_colors() {
let buffer = create_test_buffer(3, 2);
let capture = VisualCapture::from_buffer(&buffer, false, true);
let serialized = capture.serialize();
assert!(serialized.contains("# Colors: true"));
assert!(serialized.contains("## Colors"));
}
#[test]
fn test_serialize_color_format() {
let buffer = create_test_buffer(3, 2);
let capture = VisualCapture::from_buffer(&buffer, false, true);
let serialized = capture.serialize();
assert!(serialized.contains("#ff0000") || serialized.contains("#FF0000"));
}
#[test]
fn test_serialize_style_format() {
let buffer = create_test_buffer(3, 2);
let capture = VisualCapture::from_buffer(&buffer, true, false);
let serialized = capture.serialize();
assert!(serialized.contains('B') || serialized.contains('I'));
}
#[test]
fn test_deserialize_basic() {
let content = "# Visual Golden File
# Size: 2x2
# Styles: false
# Colors: false
## Text
AB
CD";
let capture = VisualCapture::deserialize(content).unwrap();
assert_eq!(capture.width, 2);
assert_eq!(capture.height, 2);
assert_eq!(capture.cells[0].symbol, 'A');
assert_eq!(capture.cells[1].symbol, 'B');
assert_eq!(capture.cells[2].symbol, 'C');
assert_eq!(capture.cells[3].symbol, 'D');
}
#[test]
fn test_deserialize_with_styles() {
let content = "# Visual Golden File
# Size: 2x1
# Styles: true
# Colors: false
## Text
AB
## Styles
0:0:B
1:0:I";
let capture = VisualCapture::deserialize(content).unwrap();
assert!(capture.include_styles);
assert!(capture.cells[0].bold);
assert!(capture.cells[1].italic);
}
#[test]
fn test_deserialize_with_colors() {
let content = "# Visual Golden File
# Size: 2x1
# Styles: false
# Colors: true
## Text
AB
## Colors
0:0,fg:#ff0000
1:0,bg:#0000ff";
let capture = VisualCapture::deserialize(content).unwrap();
assert!(capture.include_colors);
assert_eq!(capture.cells[0].fg, Some(Color::rgb(255, 0, 0)));
assert_eq!(capture.cells[1].bg, Some(Color::rgb(0, 0, 255)));
}
#[test]
fn test_deserialize_infer_size() {
let content = "# Visual Golden File
# Size: 0x0
# Styles: false
# Colors: false
## Text
ABC
DE";
let capture = VisualCapture::deserialize(content).unwrap();
assert_eq!(capture.width, 3); assert_eq!(capture.height, 2); }
#[test]
fn test_deserialize_partial_colors() {
let content = "# Visual Golden File
# Size: 2x2
# Styles: false
# Colors: true
## Text
AB
CD
## Colors
0:0,fg:#ff0000
1:1,bg:#00ff00";
let capture = VisualCapture::deserialize(content).unwrap();
assert_eq!(capture.cells[0].fg, Some(Color::rgb(255, 0, 0)));
assert_eq!(capture.cells[3].bg, Some(Color::rgb(0, 255, 0)));
assert!(capture.cells[1].fg.is_none());
assert!(capture.cells[2].bg.is_none());
}
#[test]
fn test_deserialize_multiple_styles_per_cell() {
let content = "# Visual Golden File
# Size: 1x1
# Styles: true
# Colors: false
## Text
A
## Styles
0:0:BIUD";
let capture = VisualCapture::deserialize(content).unwrap();
assert!(capture.cells[0].bold);
assert!(capture.cells[0].italic);
assert!(capture.cells[0].underline);
assert!(capture.cells[0].dim);
}
#[test]
fn test_deserialize_with_comments() {
let content = "# Visual Golden File
# Size: 0x0
# Styles: false
# Colors: false
## Text
A
# This is a comment in the text section (should be skipped)
B";
let capture = VisualCapture::deserialize(content).unwrap();
assert_eq!(capture.height, 2);
assert_eq!(capture.width, 1);
assert_eq!(capture.cells[0].symbol, 'A');
assert_eq!(capture.cells[1].symbol, 'B');
}
#[test]
fn test_deserialize_empty_content() {
let content = "";
let result = VisualCapture::deserialize(content);
assert!(result.is_ok());
let capture = result.unwrap();
assert_eq!(capture.width, 0);
assert_eq!(capture.height, 0);
}
#[test]
fn test_serialize_roundtrip() {
let buffer = create_test_buffer(3, 2);
let original = VisualCapture::from_buffer(&buffer, true, true);
let serialized = original.serialize();
let deserialized = VisualCapture::deserialize(&serialized).unwrap();
assert_eq!(original.width, deserialized.width);
assert_eq!(original.height, deserialized.height);
assert_eq!(original.cells.len(), deserialized.cells.len());
}
#[test]
fn test_diff_with_tolerance() {
let mut buffer1 = Buffer::new(2, 2);
let mut cell = Cell::new('A');
cell.fg = Some(Color::rgb(255, 0, 0));
buffer1.set(0, 0, cell);
let mut buffer2 = Buffer::new(2, 2);
let mut cell2 = Cell::new('A');
cell2.fg = Some(Color::rgb(254, 0, 0)); buffer2.set(0, 0, cell2);
let capture1 = VisualCapture::from_buffer(&buffer1, false, true);
let capture2 = VisualCapture::from_buffer(&buffer2, false, true);
let diff = capture1.diff(&capture2, 0);
assert!(!diff.differences.is_empty());
let diff = capture1.diff(&capture2, 10);
assert!(diff.differences.is_empty());
}
#[test]
fn test_serialize_includes_all_flags() {
let content = "# Visual Golden File
# Size: 1x1
# Styles: true
# Colors: true
## Text
A
## Colors
0:0,fg:#ff0000 0:0,bg:#00ff00
## Styles
0:0:BIUD";
let capture = VisualCapture::deserialize(content).unwrap();
let serialized = capture.serialize();
assert!(serialized.contains("# Styles: true"));
assert!(serialized.contains("# Colors: true"));
assert!(serialized.contains("## Text"));
assert!(serialized.contains("## Colors"));
assert!(serialized.contains("## Styles"));
}
}