use super::types::CiEnvironment;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct TestResult {
pub name: String,
pub passed: bool,
pub message: Option<String>,
pub diff_path: Option<PathBuf>,
pub duration_ms: u64,
}
#[derive(Debug, Default)]
pub struct TestReport {
results: Vec<TestResult>,
start_time: Option<std::time::Instant>,
metadata: HashMap<String, String>,
}
impl TestReport {
pub fn new() -> Self {
Self {
results: Vec::new(),
start_time: Some(std::time::Instant::now()),
metadata: HashMap::new(),
}
}
pub fn add_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.metadata.insert(key.into(), value.into());
}
pub fn add_passed(&mut self, name: impl Into<String>) {
self.results.push(TestResult {
name: name.into(),
passed: true,
message: None,
diff_path: None,
duration_ms: 0,
});
}
pub fn add_passed_with_duration(&mut self, name: impl Into<String>, duration_ms: u64) {
self.results.push(TestResult {
name: name.into(),
passed: true,
message: None,
diff_path: None,
duration_ms,
});
}
pub fn add_failed(&mut self, name: impl Into<String>, message: impl Into<String>) {
self.results.push(TestResult {
name: name.into(),
passed: false,
message: Some(message.into()),
diff_path: None,
duration_ms: 0,
});
}
pub fn add_failed_with_diff(
&mut self,
name: impl Into<String>,
message: impl Into<String>,
diff_path: impl Into<PathBuf>,
) {
self.results.push(TestResult {
name: name.into(),
passed: false,
message: Some(message.into()),
diff_path: Some(diff_path.into()),
duration_ms: 0,
});
}
pub fn total(&self) -> usize {
self.results.len()
}
pub fn passed(&self) -> usize {
self.results.iter().filter(|r| r.passed).count()
}
pub fn failed(&self) -> usize {
self.results.iter().filter(|r| !r.passed).count()
}
pub fn all_passed(&self) -> bool {
self.results.iter().all(|r| r.passed)
}
pub fn failures(&self) -> impl Iterator<Item = &TestResult> {
self.results.iter().filter(|r| !r.passed)
}
pub fn duration(&self) -> std::time::Duration {
self.start_time
.map(|s| s.elapsed())
.unwrap_or(std::time::Duration::ZERO)
}
pub fn summary(&self) -> String {
let mut output = String::new();
output.push_str(&format!(
"Visual Tests: {} passed, {} failed, {} total\n",
self.passed(),
self.failed(),
self.total()
));
if !self.all_passed() {
output.push_str("\nFailed tests:\n");
for result in self.failures() {
output.push_str(&format!(" - {}", result.name));
if let Some(ref msg) = result.message {
output.push_str(&format!(": {}", msg));
}
output.push('\n');
}
}
output.push_str(&format!("\nDuration: {:?}\n", self.duration()));
output
}
pub fn write_summary(&self, ci: &CiEnvironment) {
println!("\n{}", self.summary());
if ci.provider == super::types::CiProvider::GitHubActions {
if let Ok(summary_file) = std::env::var("GITHUB_STEP_SUMMARY") {
let markdown = self.to_markdown();
let _ = fs::write(summary_file, markdown);
}
ci.set_output("passed", &self.passed().to_string());
ci.set_output("failed", &self.failed().to_string());
ci.set_output("total", &self.total().to_string());
for result in self.failures() {
if let Some(ref msg) = result.message {
println!("::error title=Visual Test Failed: {}::{}", result.name, msg);
}
}
}
}
pub fn to_markdown(&self) -> String {
let mut output = String::new();
output.push_str("# Visual Regression Test Results\n\n");
let status = if self.all_passed() {
"✅ Passed"
} else {
"❌ Failed"
};
output.push_str(&format!("**Status:** {}\n\n", status));
output.push_str("| Metric | Value |\n");
output.push_str("|--------|-------|\n");
output.push_str(&format!("| Total | {} |\n", self.total()));
output.push_str(&format!("| Passed | {} |\n", self.passed()));
output.push_str(&format!("| Failed | {} |\n", self.failed()));
output.push_str(&format!("| Duration | {:?} |\n", self.duration()));
output.push('\n');
if !self.all_passed() {
output.push_str("## Failed Tests\n\n");
for result in self.failures() {
output.push_str(&format!("### {}\n\n", result.name));
if let Some(ref msg) = result.message {
output.push_str(&format!("**Error:** {}\n\n", msg));
}
if let Some(ref diff) = result.diff_path {
output.push_str(&format!("**Diff:** `{}`\n\n", diff.display()));
}
}
}
if !self.metadata.is_empty() {
output.push_str("## Metadata\n\n");
for (key, value) in &self.metadata {
output.push_str(&format!("- **{}:** {}\n", key, value));
}
}
output
}
pub fn save(&self, path: &Path) -> std::io::Result<()> {
let content = self.to_markdown();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, content)
}
pub fn save_artifacts(&self, ci: &CiEnvironment) -> std::io::Result<()> {
if self.all_passed() {
return Ok(());
}
fs::create_dir_all(&ci.artifacts_dir)?;
let report_path = ci.artifacts_dir.join("visual-test-report.md");
self.save(&report_path)?;
for result in self.failures() {
if let Some(ref diff_path) = result.diff_path {
if diff_path.exists() {
let dest = ci.artifacts_dir.join(diff_path.file_name().unwrap());
fs::copy(diff_path, dest)?;
}
}
}
Ok(())
}
}