#![allow(deprecated)]
use assert_cmd::Command;
use predicates::prelude::*;
use serde_json::Value;
fn cmd() -> Command {
Command::cargo_bin("agent-image-diff").unwrap()
}
#[test]
fn identical_images_match() {
let output = cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/identical.png",
"-v",
])
.output()
.unwrap();
assert!(output.status.success());
let json: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["match"], true);
assert_eq!(json["regions"].as_array().unwrap().len(), 0);
assert_eq!(json["stats"]["changed_pixels"], 0);
}
#[test]
fn different_images_exit_0() {
cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/changed_small.png",
])
.assert()
.code(0);
}
#[test]
fn small_change_detected_with_label() {
let output = cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/changed_small.png",
"--dilate",
"0",
"--merge-distance",
"0",
"-v",
])
.output()
.unwrap();
let json: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["match"], false);
assert_eq!(json["stats"]["region_count"], 1);
assert_eq!(json["stats"]["changed_pixels"], 100);
let region = &json["regions"][0];
assert_eq!(region["bounding_box"]["x"], 20);
assert_eq!(region["bounding_box"]["y"], 30);
assert_eq!(region["bounding_box"]["width"], 10);
assert_eq!(region["bounding_box"]["height"], 10);
assert_eq!(region["pixel_count"], 100);
assert!(region["label"].is_string());
}
#[test]
fn two_regions_merge_with_default_settings() {
let output = cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/changed_large.png",
"-v",
])
.output()
.unwrap();
let json: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["stats"]["changed_pixels"], 550);
assert_eq!(json["stats"]["region_count"], 1);
}
#[test]
fn two_regions_stay_separate_without_merge() {
let output = cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/changed_large.png",
"--dilate",
"0",
"--merge-distance",
"0",
"-v",
])
.output()
.unwrap();
let json: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["stats"]["region_count"], 2);
assert_eq!(json["stats"]["changed_pixels"], 550);
let regions = json["regions"].as_array().unwrap();
assert_eq!(regions[0]["pixel_count"], 400);
assert_eq!(regions[1]["pixel_count"], 150);
}
#[test]
fn dimension_mismatch_reported() {
let output = cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/different_size.png",
])
.output()
.unwrap();
let json: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["match"], false);
assert!(json["dimension_mismatch"].is_object());
assert_eq!(json["dimension_mismatch"]["baseline"]["width"], 100);
assert_eq!(json["dimension_mismatch"]["candidate"]["width"], 120);
}
#[test]
fn summary_format_shows_labels() {
cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/changed_small.png",
"-f",
"summary",
])
.assert()
.stdout(predicate::str::contains("Images differ"))
.stdout(predicate::str::contains("Region #1"));
}
#[test]
fn output_flag_creates_diff_image() {
let dir = tempfile::tempdir().unwrap();
let diff_path = dir.path().join("diff.png");
cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/changed_small.png",
"-o",
diff_path.to_str().unwrap(),
])
.assert()
.code(0);
assert!(diff_path.exists());
}
#[test]
fn threshold_zero_is_exact_match() {
let output = cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/identical.png",
"-t",
"0.0",
])
.output()
.unwrap();
let json: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["match"], true);
}
#[test]
fn missing_file_gives_error() {
cmd()
.args(["nonexistent.png", "tests/fixtures/baseline.png"])
.assert()
.failure()
.stderr(predicate::str::contains("Failed to open baseline image"));
}
#[test]
fn dilate_zero_and_merge_zero_matches_v1_behavior() {
let output = cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/changed_large.png",
"--dilate",
"0",
"--merge-distance",
"0",
"-v",
])
.output()
.unwrap();
let json: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["stats"]["region_count"], 2);
let r0 = &json["regions"][0];
assert_eq!(r0["bounding_box"]["x"], 5);
assert_eq!(r0["bounding_box"]["y"], 5);
assert_eq!(r0["bounding_box"]["width"], 20);
assert_eq!(r0["bounding_box"]["height"], 20);
}
#[test]
fn regions_have_label_field() {
let output = cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/changed_small.png",
])
.output()
.unwrap();
let json: Value = serde_json::from_slice(&output.stdout).unwrap();
let regions = json["regions"].as_array().unwrap();
for region in regions {
let label = region["label"].as_str().unwrap();
assert!(
["added", "removed", "color-change", "content-change"].contains(&label),
"unexpected label: {label}"
);
}
}
#[test]
fn default_json_is_compact() {
let output = cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/changed_small.png",
])
.output()
.unwrap();
let stdout = String::from_utf8(output.stdout).unwrap();
assert_eq!(stdout.trim().lines().count(), 1);
assert!(!stdout.contains("\"dimensions\""));
assert!(!stdout.contains("\"stats\""));
assert!(!stdout.contains("\"pixel_count\""));
assert!(!stdout.contains("\"avg_delta\""));
assert!(!stdout.contains("\"max_delta\""));
assert!(stdout.contains("\"match\""));
assert!(stdout.contains("\"diff_percentage\""));
assert!(stdout.contains("\"regions\""));
assert!(stdout.contains("\"bounding_box\""));
assert!(stdout.contains("\"label\""));
}
#[test]
fn verbose_json_has_all_fields() {
let output = cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/changed_small.png",
"-v",
])
.output()
.unwrap();
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("\"dimensions\""));
assert!(stdout.contains("\"stats\""));
assert!(stdout.contains("\"pixel_count\""));
assert!(stdout.contains("\"avg_delta\""));
assert!(stdout.contains("\"max_delta\""));
}
#[test]
fn pretty_flag_adds_whitespace() {
let output = cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/changed_small.png",
"--pretty",
])
.output()
.unwrap();
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.trim().lines().count() > 1);
}
#[test]
fn quiet_mode_no_stdout() {
let output = cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/changed_small.png",
"-q",
])
.output()
.unwrap();
assert!(output.status.success());
assert!(output.stdout.is_empty());
}
#[test]
fn quiet_mode_still_writes_diff_image() {
let dir = tempfile::tempdir().unwrap();
let diff_path = dir.path().join("diff.png");
let output = cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/changed_small.png",
"-q",
"-o",
diff_path.to_str().unwrap(),
])
.output()
.unwrap();
assert!(output.stdout.is_empty());
assert!(diff_path.exists());
}
#[test]
fn diff_percentage_is_rounded() {
let output = cmd()
.args([
"tests/fixtures/baseline.png",
"tests/fixtures/changed_small.png",
])
.output()
.unwrap();
let json: Value = serde_json::from_slice(&output.stdout).unwrap();
let pct = json["diff_percentage"].as_f64().unwrap();
assert_eq!(pct, 1.0);
}
#[test]
fn crop_extracts_correct_region() {
let dir = tempfile::tempdir().unwrap();
let out_path = dir.path().join("cropped.png");
cmd()
.args([
"tests/fixtures/baseline.png",
"--crop",
"--x",
"5",
"--y",
"5",
"--crop-width",
"20",
"--crop-height",
"20",
"-o",
out_path.to_str().unwrap(),
])
.assert()
.code(0);
assert!(out_path.exists());
let img = image::open(&out_path).unwrap();
assert_eq!(img.width(), 20);
assert_eq!(img.height(), 20);
}
#[test]
fn crop_out_of_bounds_errors() {
let dir = tempfile::tempdir().unwrap();
let out_path = dir.path().join("cropped.png");
cmd()
.args([
"tests/fixtures/baseline.png",
"--crop",
"--x",
"90",
"--y",
"90",
"--crop-width",
"20",
"--crop-height",
"20",
"-o",
out_path.to_str().unwrap(),
])
.assert()
.failure()
.stderr(predicate::str::contains("exceeds"));
}
#[test]
fn diff_without_candidate_errors() {
cmd()
.args(["tests/fixtures/baseline.png"])
.assert()
.failure()
.stderr(predicate::str::contains("candidate"));
}