diff (#510)
* diff * fixes * fixes * fixes * fixes * fixes * better docs
This commit is contained in:
@@ -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 { .. }
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user