* 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 { .. }
));
}
}