* diff

* fixes

* fixes

* fixes

* fixes

* fixes

* better docs
This commit is contained in:
Chris Tate
2026-02-19 23:51:09 -06:00
committed by GitHub
parent 9732031087
commit d5a667ea2d
21 changed files with 2446 additions and 88 deletions
+688
View File
@@ -1037,12 +1037,284 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
}
}
"diff" => parse_diff(&rest, &id, flags),
_ => Err(ParseError::UnknownCommand {
command: cmd.to_string(),
}),
}
}
fn parse_diff(rest: &[&str], id: &str, flags: &Flags) -> Result<Value, ParseError> {
const VALID: &[&str] = &["snapshot", "screenshot", "url"];
match rest.first().copied() {
Some("snapshot") => {
let mut cmd = json!({ "id": id, "action": "diff_snapshot" });
let obj = cmd.as_object_mut().unwrap();
let mut i = 1;
while i < rest.len() {
match rest[i] {
"-b" | "--baseline" => {
if let Some(path) = rest.get(i + 1) {
obj.insert("baseline".to_string(), json!(path));
i += 1;
} else {
return Err(ParseError::MissingArguments {
context: "diff snapshot --baseline".to_string(),
usage: "diff snapshot --baseline <file>",
});
}
}
"-s" | "--selector" => {
if let Some(s) = rest.get(i + 1) {
obj.insert("selector".to_string(), json!(s));
i += 1;
} else {
return Err(ParseError::MissingArguments {
context: "diff snapshot --selector".to_string(),
usage: "diff snapshot --selector <sel>",
});
}
}
"-c" | "--compact" => {
obj.insert("compact".to_string(), json!(true));
}
"-d" | "--depth" => {
if let Some(d) = rest.get(i + 1) {
match d.parse::<u32>() {
Ok(n) => {
obj.insert("maxDepth".to_string(), json!(n));
i += 1;
}
Err(_) => {
return Err(ParseError::InvalidValue {
message: format!("Depth must be a non-negative integer, got: {}", d),
usage: "diff snapshot --depth <n>",
});
}
}
} else {
return Err(ParseError::MissingArguments {
context: "diff snapshot --depth".to_string(),
usage: "diff snapshot --depth <n>",
});
}
}
other if other.starts_with('-') => {
return Err(ParseError::InvalidValue {
message: format!("Unknown flag: {}", other),
usage: "diff snapshot [--baseline <file>] [--selector <sel>] [--compact] [--depth <n>]",
});
}
other => {
return Err(ParseError::InvalidValue {
message: format!("Unexpected argument: {}", other),
usage: "diff snapshot [--baseline <file>] [--selector <sel>] [--compact] [--depth <n>]",
});
}
}
i += 1;
}
Ok(cmd)
}
Some("screenshot") => {
let mut cmd = json!({ "id": id, "action": "diff_screenshot" });
let obj = cmd.as_object_mut().unwrap();
let mut i = 1;
while i < rest.len() {
match rest[i] {
"-b" | "--baseline" => {
if let Some(path) = rest.get(i + 1) {
obj.insert("baseline".to_string(), json!(path));
i += 1;
} else {
return Err(ParseError::MissingArguments {
context: "diff screenshot --baseline".to_string(),
usage: "diff screenshot --baseline <file>",
});
}
}
"-o" | "--output" => {
if let Some(path) = rest.get(i + 1) {
obj.insert("output".to_string(), json!(path));
i += 1;
} else {
return Err(ParseError::MissingArguments {
context: "diff screenshot --output".to_string(),
usage: "diff screenshot --output <file>",
});
}
}
"-t" | "--threshold" => {
if let Some(t) = rest.get(i + 1) {
match t.parse::<f64>() {
Ok(n) if (0.0..=1.0).contains(&n) => {
obj.insert("threshold".to_string(), json!(n));
i += 1;
}
Ok(n) => {
return Err(ParseError::InvalidValue {
message: format!("Threshold must be between 0 and 1, got {}", n),
usage: "diff screenshot --threshold <0-1>",
});
}
Err(_) => {
return Err(ParseError::InvalidValue {
message: format!("Invalid threshold value: {}", t),
usage: "diff screenshot --threshold <0-1>",
});
}
}
} else {
return Err(ParseError::MissingArguments {
context: "diff screenshot --threshold".to_string(),
usage: "diff screenshot --threshold <0-1>",
});
}
}
"-s" | "--selector" => {
if let Some(s) = rest.get(i + 1) {
obj.insert("selector".to_string(), json!(s));
i += 1;
} else {
return Err(ParseError::MissingArguments {
context: "diff screenshot --selector".to_string(),
usage: "diff screenshot --selector <sel>",
});
}
}
"--full" => {
obj.insert("fullPage".to_string(), json!(true));
}
other if other.starts_with('-') => {
return Err(ParseError::InvalidValue {
message: format!("Unknown flag: {}", other),
usage: "diff screenshot --baseline <file> [--output <file>] [--threshold <0-1>] [--selector <sel>] [--full]",
});
}
other => {
return Err(ParseError::InvalidValue {
message: format!("Unexpected argument: {}", other),
usage: "diff screenshot --baseline <file> [--output <file>] [--threshold <0-1>] [--selector <sel>] [--full]",
});
}
}
i += 1;
}
if flags.full {
obj.insert("fullPage".to_string(), json!(true));
}
if !obj.contains_key("baseline") {
return Err(ParseError::MissingArguments {
context: "diff screenshot".to_string(),
usage: "diff screenshot --baseline <file>",
});
}
Ok(cmd)
}
Some("url") => {
let url1 = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
context: "diff url".to_string(),
usage: "diff url <url1> <url2>",
})?;
let url2 = rest.get(2).ok_or_else(|| ParseError::MissingArguments {
context: "diff url".to_string(),
usage: "diff url <url1> <url2>",
})?;
let mut cmd = json!({
"id": id,
"action": "diff_url",
"url1": url1,
"url2": url2,
});
let obj = cmd.as_object_mut().unwrap();
let mut i = 3;
while i < rest.len() {
match rest[i] {
"--screenshot" => {
obj.insert("screenshot".to_string(), json!(true));
}
"--full" => {
obj.insert("fullPage".to_string(), json!(true));
}
"--wait-until" => {
if let Some(val) = rest.get(i + 1) {
obj.insert("waitUntil".to_string(), json!(val));
i += 1;
} else {
return Err(ParseError::MissingArguments {
context: "diff url --wait-until".to_string(),
usage: "diff url <url1> <url2> --wait-until <load|domcontentloaded|networkidle>",
});
}
}
"-s" | "--selector" => {
if let Some(s) = rest.get(i + 1) {
obj.insert("selector".to_string(), json!(s));
i += 1;
} else {
return Err(ParseError::MissingArguments {
context: "diff url --selector".to_string(),
usage: "diff url <url1> <url2> --selector <sel>",
});
}
}
"-c" | "--compact" => {
obj.insert("compact".to_string(), json!(true));
}
"-d" | "--depth" => {
if let Some(d) = rest.get(i + 1) {
match d.parse::<u32>() {
Ok(n) => {
obj.insert("maxDepth".to_string(), json!(n));
i += 1;
}
Err(_) => {
return Err(ParseError::InvalidValue {
message: format!("Depth must be a non-negative integer, got: {}", d),
usage: "diff url <url1> <url2> --depth <n>",
});
}
}
} else {
return Err(ParseError::MissingArguments {
context: "diff url --depth".to_string(),
usage: "diff url <url1> <url2> --depth <n>",
});
}
}
other if other.starts_with('-') => {
return Err(ParseError::InvalidValue {
message: format!("Unknown flag: {}", other),
usage: "diff url <url1> <url2> [--screenshot] [--full] [--wait-until <strategy>] [--selector <sel>] [--compact] [--depth <n>]",
});
}
other => {
return Err(ParseError::InvalidValue {
message: format!("Unexpected argument: {}", other),
usage: "diff url <url1> <url2> [--screenshot] [--full] [--wait-until <strategy>] [--selector <sel>] [--compact] [--depth <n>]",
});
}
}
i += 1;
}
if flags.full {
obj.insert("fullPage".to_string(), json!(true));
}
Ok(cmd)
}
Some(sub) => Err(ParseError::UnknownSubcommand {
subcommand: sub.to_string(),
valid_options: VALID,
}),
None => Err(ParseError::MissingArguments {
context: "diff".to_string(),
usage: "diff <snapshot|screenshot|url>",
}),
}
}
fn parse_get(rest: &[&str], id: &str) -> Result<Value, ParseError> {
const VALID: &[&str] = &[
"text", "html", "value", "attr", "url", "title", "count", "box", "styles",
@@ -2712,4 +2984,420 @@ mod tests {
assert_eq!(cmd["action"], "trace_stop");
assert!(cmd.get("path").is_none() || cmd["path"].is_null());
}
// === Diff Tests ===
#[test]
fn test_diff_snapshot_basic() {
let cmd = parse_command(&args("diff snapshot"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "diff_snapshot");
}
#[test]
fn test_diff_snapshot_baseline() {
let cmd =
parse_command(&args("diff snapshot --baseline before.txt"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "diff_snapshot");
assert_eq!(cmd["baseline"], "before.txt");
}
#[test]
fn test_diff_snapshot_selector_compact_depth() {
let cmd = parse_command(
&args("diff snapshot --selector #main --compact --depth 3"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "diff_snapshot");
assert_eq!(cmd["selector"], "#main");
assert_eq!(cmd["compact"], true);
assert_eq!(cmd["maxDepth"], 3);
}
#[test]
fn test_diff_snapshot_short_flags() {
let cmd =
parse_command(&args("diff snapshot -b snap.txt -s .content -c -d 2"), &default_flags())
.unwrap();
assert_eq!(cmd["action"], "diff_snapshot");
assert_eq!(cmd["baseline"], "snap.txt");
assert_eq!(cmd["selector"], ".content");
assert_eq!(cmd["compact"], true);
assert_eq!(cmd["maxDepth"], 2);
}
#[test]
fn test_diff_screenshot_baseline() {
let cmd = parse_command(
&args("diff screenshot --baseline before.png"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "diff_screenshot");
assert_eq!(cmd["baseline"], "before.png");
}
#[test]
fn test_diff_screenshot_all_options() {
let cmd = parse_command(
&args("diff screenshot --baseline b.png --output d.png --threshold 0.2 --selector #hero --full"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "diff_screenshot");
assert_eq!(cmd["baseline"], "b.png");
assert_eq!(cmd["output"], "d.png");
assert_eq!(cmd["threshold"], 0.2);
assert_eq!(cmd["selector"], "#hero");
assert_eq!(cmd["fullPage"], true);
}
#[test]
fn test_diff_screenshot_missing_baseline() {
let result = parse_command(&args("diff screenshot"), &default_flags());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
}
#[test]
fn test_diff_screenshot_global_full_flag() {
let mut flags = default_flags();
flags.full = true;
let cmd =
parse_command(&args("diff screenshot --baseline b.png"), &flags).unwrap();
assert_eq!(cmd["action"], "diff_screenshot");
assert_eq!(cmd["fullPage"], true);
}
#[test]
fn test_diff_url_basic() {
let cmd = parse_command(
&args("diff url https://a.com https://b.com"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "diff_url");
assert_eq!(cmd["url1"], "https://a.com");
assert_eq!(cmd["url2"], "https://b.com");
}
#[test]
fn test_diff_url_with_screenshot_full() {
let cmd = parse_command(
&args("diff url https://a.com https://b.com --screenshot --full"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "diff_url");
assert_eq!(cmd["screenshot"], true);
assert_eq!(cmd["fullPage"], true);
}
#[test]
fn test_diff_url_with_wait_until() {
let cmd = parse_command(
&args("diff url https://a.com https://b.com --wait-until networkidle"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "diff_url");
assert_eq!(cmd["waitUntil"], "networkidle");
}
#[test]
fn test_diff_url_global_full_flag() {
let mut flags = default_flags();
flags.full = true;
let cmd =
parse_command(&args("diff url https://a.com https://b.com"), &flags).unwrap();
assert_eq!(cmd["fullPage"], true);
}
#[test]
fn test_diff_missing_subcommand() {
let result = parse_command(&args("diff"), &default_flags());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
}
#[test]
fn test_diff_unknown_subcommand() {
let result = parse_command(&args("diff invalid"), &default_flags());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::UnknownSubcommand { .. }
));
}
#[test]
fn test_diff_snapshot_baseline_missing_value() {
let result = parse_command(&args("diff snapshot --baseline"), &default_flags());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
}
#[test]
fn test_diff_snapshot_selector_missing_value() {
let result = parse_command(&args("diff snapshot --selector"), &default_flags());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
}
#[test]
fn test_diff_snapshot_depth_missing_value() {
let result = parse_command(&args("diff snapshot --depth"), &default_flags());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
}
#[test]
fn test_diff_screenshot_threshold_missing_value() {
let result = parse_command(
&args("diff screenshot --baseline b.png --threshold"),
&default_flags(),
);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
}
#[test]
fn test_diff_screenshot_output_missing_value() {
let result = parse_command(
&args("diff screenshot --baseline b.png --output"),
&default_flags(),
);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
}
#[test]
fn test_diff_url_wait_until_missing_value() {
let result = parse_command(
&args("diff url https://a.com https://b.com --wait-until"),
&default_flags(),
);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
}
#[test]
fn test_diff_snapshot_unexpected_arg() {
let result = parse_command(&args("diff snapshot foo"), &default_flags());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidValue { .. }
));
}
#[test]
fn test_diff_screenshot_unexpected_arg() {
let result = parse_command(
&args("diff screenshot --baseline b.png unexpected"),
&default_flags(),
);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidValue { .. }
));
}
#[test]
fn test_diff_url_unexpected_arg() {
let result = parse_command(
&args("diff url https://a.com https://b.com extra"),
&default_flags(),
);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidValue { .. }
));
}
#[test]
fn test_diff_snapshot_unknown_flag() {
let result = parse_command(&args("diff snapshot --invalid"), &default_flags());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidValue { .. }
));
}
#[test]
fn test_diff_url_missing_urls() {
let result = parse_command(&args("diff url"), &default_flags());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
}
#[test]
fn test_diff_url_missing_second_url() {
let result = parse_command(&args("diff url https://a.com"), &default_flags());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
}
#[test]
fn test_diff_snapshot_depth_invalid_value() {
let result = parse_command(&args("diff snapshot --depth abc"), &default_flags());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidValue { .. }
));
}
#[test]
fn test_diff_screenshot_threshold_invalid_value() {
let result = parse_command(
&args("diff screenshot --baseline b.png --threshold abc"),
&default_flags(),
);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidValue { .. }
));
}
#[test]
fn test_diff_screenshot_threshold_out_of_range() {
let result = parse_command(
&args("diff screenshot --baseline b.png --threshold 1.5"),
&default_flags(),
);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidValue { .. }
));
}
#[test]
fn test_diff_screenshot_threshold_negative() {
let result = parse_command(
&args("diff screenshot --baseline b.png --threshold -0.5"),
&default_flags(),
);
assert!(result.is_err());
}
#[test]
fn test_diff_url_with_selector() {
let cmd = parse_command(
&args("diff url https://a.com https://b.com --selector #main"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "diff_url");
assert_eq!(cmd["selector"], "#main");
}
#[test]
fn test_diff_url_with_compact_depth() {
let cmd = parse_command(
&args("diff url https://a.com https://b.com --compact --depth 3"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "diff_url");
assert_eq!(cmd["compact"], true);
assert_eq!(cmd["maxDepth"], 3);
}
#[test]
fn test_diff_url_with_short_snapshot_flags() {
let cmd = parse_command(
&args("diff url https://a.com https://b.com -s .content -c -d 2"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "diff_url");
assert_eq!(cmd["selector"], ".content");
assert_eq!(cmd["compact"], true);
assert_eq!(cmd["maxDepth"], 2);
}
#[test]
fn test_diff_url_depth_invalid_value() {
let result = parse_command(
&args("diff url https://a.com https://b.com --depth abc"),
&default_flags(),
);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidValue { .. }
));
}
#[test]
fn test_diff_snapshot_depth_negative_value() {
let result = parse_command(&args("diff snapshot --depth -1"), &default_flags());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidValue { .. }
));
}
#[test]
fn test_diff_url_depth_negative_value() {
let result = parse_command(
&args("diff url https://a.com https://b.com --depth -1"),
&default_flags(),
);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::InvalidValue { .. }
));
}
#[test]
fn test_diff_url_selector_missing_value() {
let result = parse_command(
&args("diff url https://a.com https://b.com --selector"),
&default_flags(),
);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
}
}
+166
View File
@@ -27,6 +27,35 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
println!("{}", url);
return;
}
// Diff responses -- route by action to avoid fragile shape probing
if let Some(obj) = data.as_object() {
match action {
Some("diff_snapshot") => {
print_snapshot_diff(obj);
return;
}
Some("diff_screenshot") => {
print_screenshot_diff(obj);
return;
}
Some("diff_url") => {
if let Some(snap_data) =
obj.get("snapshot").and_then(|v| v.as_object())
{
println!("{}", color::bold("Snapshot diff:"));
print_snapshot_diff(snap_data);
}
if let Some(ss_data) =
obj.get("screenshot").and_then(|v| v.as_object())
{
println!("\n{}", color::bold("Screenshot diff:"));
print_screenshot_diff(ss_data);
}
return;
}
_ => {}
}
}
// Snapshot
if let Some(snapshot) = data.get("snapshot").and_then(|v| v.as_str()) {
println!("{}", snapshot);
@@ -1850,6 +1879,65 @@ Examples:
"##
}
"diff" => {
r##"
agent-browser diff - Compare page states
Subcommands:
diff snapshot Compare current snapshot to last snapshot in session
diff screenshot --baseline <f> Visual pixel diff against a baseline image
diff url <url1> <url2> Compare two pages
Snapshot Diff:
Usage: agent-browser diff snapshot [options]
Options:
-b, --baseline <file> Compare against a saved snapshot file
-s, --selector <sel> Scope snapshot to a CSS selector or @ref
-c, --compact Use compact snapshot format
-d, --depth <n> Limit snapshot tree depth
Without --baseline, compares against the last snapshot taken in this session.
Screenshot Diff:
Usage: agent-browser diff screenshot --baseline <file> [options]
Options:
-b, --baseline <file> Baseline image to compare against (required)
-o, --output <file> Path for the diff image (default: temp dir)
-t, --threshold <0-1> Color distance threshold (default: 0.1)
-s, --selector <sel> Scope screenshot to element
--full Full page screenshot
URL Diff:
Usage: agent-browser diff url <url1> <url2> [options]
Options:
--screenshot Also compare screenshots (default: snapshot only)
--full Full page screenshots
--wait-until <strategy> Navigation wait strategy: load, domcontentloaded, networkidle (default: load)
-s, --selector <sel> Scope snapshots to a CSS selector or @ref
-c, --compact Use compact snapshot format
-d, --depth <n> Limit snapshot tree depth
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser diff snapshot
agent-browser diff snapshot --baseline before.txt
agent-browser diff screenshot --baseline before.png
agent-browser diff screenshot --baseline before.png --output diff.png --threshold 0.2
agent-browser diff url https://staging.example.com https://prod.example.com
agent-browser diff url https://v1.example.com https://v2.example.com --screenshot
"##
}
_ => return false,
};
println!("{}", help.trim());
@@ -1922,6 +2010,11 @@ Storage:
Tabs:
tab [new|list|close|<n>] Manage tabs
Diff:
diff snapshot Compare current vs last snapshot
diff screenshot --baseline Compare current vs baseline image
diff url <u1> <u2> Compare two pages
Debug:
trace start|stop [path] Record Playwright trace
profiler start|stop [path] Record Chrome DevTools profile
@@ -2053,6 +2146,79 @@ iOS Simulator (requires Xcode and Appium):
);
}
fn print_snapshot_diff(data: &serde_json::Map<String, serde_json::Value>) {
let changed = data
.get("changed")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !changed {
println!("{} No changes detected", color::success_indicator());
return;
}
if let Some(diff) = data.get("diff").and_then(|v| v.as_str()) {
for line in diff.lines() {
if line.starts_with("+ ") {
println!("{}", color::green(line));
} else if line.starts_with("- ") {
println!("{}", color::red(line));
} else {
println!("{}", color::dim(line));
}
}
let additions = data.get("additions").and_then(|v| v.as_i64()).unwrap_or(0);
let removals = data.get("removals").and_then(|v| v.as_i64()).unwrap_or(0);
let unchanged = data.get("unchanged").and_then(|v| v.as_i64()).unwrap_or(0);
println!(
"\n{} additions, {} removals, {} unchanged",
color::green(&additions.to_string()),
color::red(&removals.to_string()),
unchanged
);
}
}
fn print_screenshot_diff(data: &serde_json::Map<String, serde_json::Value>) {
let mismatch = data
.get("mismatchPercentage")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let is_match = data
.get("match")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let dim_mismatch = data
.get("dimensionMismatch")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if dim_mismatch {
println!(
"{} Images have different dimensions",
color::error_indicator()
);
} else if is_match {
println!("{} Images match (0% difference)", color::success_indicator());
} else {
println!(
"{} {:.2}% pixels differ",
color::error_indicator(),
mismatch
);
}
if let Some(diff_path) = data.get("diffPath").and_then(|v| v.as_str()) {
println!(" Diff image: {}", color::green(diff_path));
}
let total = data.get("totalPixels").and_then(|v| v.as_i64()).unwrap_or(0);
let different = data
.get("differentPixels")
.and_then(|v| v.as_i64())
.unwrap_or(0);
println!(
" {} different / {} total pixels",
color::red(&different.to_string()),
total
);
}
pub fn print_version() {
println!("agent-browser {}", env!("CARGO_PKG_VERSION"));
}