use assert_cmd::Command;
use predicates::prelude::*;
use std::{fs, path::Path};
use tempfile::TempDir;
const TEST_URL: &str = "https://httpbin.org/html";
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_basic_screenshot() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("test.png");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg(TEST_URL)
.arg("-o")
.arg(&output_path)
.arg("-w")
.arg("800")
.arg("-H")
.arg("600");
cmd.assert().success();
assert!(output_path.exists());
let metadata = fs::metadata(&output_path).unwrap();
assert!(metadata.len() > 0);
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_pdf_generation() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("test.pdf");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("pdf").arg(TEST_URL).arg("-o").arg(&output_path);
cmd.assert().success();
assert!(output_path.exists());
let content = fs::read(&output_path).unwrap();
assert!(!content.is_empty());
assert!(content.starts_with(b"%PDF"));
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_element_screenshot() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("element.png");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg(TEST_URL)
.arg("-o")
.arg(&output_path)
.arg("-s")
.arg("h1");
cmd.assert().success();
assert!(output_path.exists());
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_javascript_execution() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("js-test.png");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg(TEST_URL)
.arg("-o")
.arg(&output_path)
.arg("-j")
.arg("document.body.style.backgroundColor = 'red'");
cmd.assert().success();
assert!(output_path.exists());
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_text_extraction() {
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("text").arg(TEST_URL).arg("-s").arg("h1");
cmd.assert()
.success()
.stdout(predicate::str::contains("Herman Melville"));
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_config_processing() {
let temp_dir = TempDir::new().unwrap();
let config_content = format!(
r#"
screenshots:
- url: "{}"
output: "test1.png"
width: 800
height: 600
- url: "{}"
output: "test2.png"
width: 1200
height: 800
"#,
TEST_URL, TEST_URL
);
let config_path = temp_dir.path().join("config.yaml");
fs::write(&config_path, config_content).unwrap();
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("multi")
.arg(&config_path)
.arg("-o")
.arg(temp_dir.path())
.arg("-p")
.arg("2");
cmd.assert().success();
assert!(temp_dir.path().join("test1.png").exists());
assert!(temp_dir.path().join("test2.png").exists());
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_multi_output_dir_prepends_defaults_output_dir_and_overwrites() {
let temp_dir = TempDir::new().unwrap();
let config_content = format!(
r#"
defaults:
output_dir: "defaults/nested"
screenshots:
- url: "{}"
output: "page.png"
width: 800
height: 600
"#,
TEST_URL
);
let config_path = temp_dir.path().join("config.yaml");
let artifacts_dir = temp_dir.path().join("artifacts");
let expected_output = artifacts_dir
.join("defaults")
.join("nested")
.join("page.png");
fs::write(&config_path, config_content).unwrap();
run_multi_output_dir_command(&config_path, &artifacts_dir);
assert_png_file(&expected_output);
assert_no_alternate_outputs(temp_dir.path(), &artifacts_dir);
fs::write(&expected_output, b"stale output").unwrap();
run_multi_output_dir_command(&config_path, &artifacts_dir);
let second_output = assert_png_file(&expected_output);
assert_ne!(second_output, b"stale output");
assert_no_alternate_outputs(temp_dir.path(), &artifacts_dir);
}
fn run_multi_output_dir_command(config_path: &Path, artifacts_dir: &Path) {
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.current_dir(config_path.parent().unwrap())
.arg("multi")
.arg(config_path)
.arg("-o")
.arg(artifacts_dir)
.arg("-p")
.arg("1");
cmd.assert().success();
}
fn assert_no_alternate_outputs(temp_dir: &Path, artifacts_dir: &Path) {
assert!(!temp_dir
.join("defaults")
.join("nested")
.join("page.png")
.exists());
assert!(!artifacts_dir.join("page.png").exists());
}
fn assert_png_file(path: &Path) -> Vec<u8> {
let content = fs::read(path).unwrap();
assert!(content.starts_with(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]));
assert!(content.len() > 8);
content
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_jpeg_quality() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("quality.jpg");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg(TEST_URL)
.arg("-o")
.arg(&output_path)
.arg("-q")
.arg("50");
cmd.assert().success();
assert!(output_path.exists());
let content = fs::read(&output_path).unwrap();
assert!(content.starts_with(&[0xFF, 0xD8, 0xFF])); }
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_retina_mode() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("retina.png");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg(TEST_URL)
.arg("-o")
.arg(&output_path)
.arg("--retina");
cmd.assert().success();
assert!(output_path.exists());
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_wait_for_element() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("wait.png");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg(TEST_URL)
.arg("-o")
.arg(&output_path)
.arg("--wait-for")
.arg("h1");
cmd.assert().success();
assert!(output_path.exists());
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_custom_user_agent() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("user-agent.png");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("https://httpbin.org/user-agent")
.arg("-o")
.arg(&output_path)
.arg("--user-agent")
.arg("WebshotBot/1.0");
cmd.assert().success();
assert!(output_path.exists());
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium startup before validation completes"]
async fn test_error_handling_invalid_url() {
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("not-a-valid-url");
cmd.assert().failure();
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_error_handling_invalid_selector() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("invalid.png");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg(TEST_URL)
.arg("-o")
.arg(&output_path)
.arg("-s")
.arg("invalid-selector-that-does-not-exist");
cmd.assert().failure();
}
#[tokio::test]
async fn test_help_output() {
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("webshot"))
.stdout(predicate::str::contains("screenshot"))
.stdout(predicate::str::contains("-H, --height"))
.stdout(predicate::str::contains("-h, --help"));
}
#[tokio::test]
async fn test_screenshot_help_height_short_flag() {
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.args(["screenshot", "--help"]);
cmd.assert()
.success()
.stdout(predicate::str::contains("-H, --height"))
.stdout(predicate::str::contains("-h, --help"));
}
#[tokio::test]
async fn test_cli_rejects_non_web_url_before_browser_startup() {
for args in [
vec!["screenshot", "file:///etc/passwd"],
vec!["pdf", "file:///etc/passwd"],
vec!["text", "data:text/html,<h1>Test</h1>"],
] {
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.args(args);
cmd.assert()
.failure()
.stderr(predicate::str::contains("Unsupported URL scheme"));
}
}
#[test]
fn test_readme_uses_actual_height_short_flag() {
let readme_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("README.md");
let readme = fs::read_to_string(readme_path).unwrap();
assert!(
!readme.contains("-h, --height"),
"README should document -H for --height because -h is clap help"
);
assert!(
readme.contains("-H, --height"),
"README should document the actual short flag for --height"
);
assert!(
readme.contains("webshot https://example.com -o screenshot.png -w 1920 -H 1080"),
"README quick-start example should use -H for viewport height"
);
assert!(
readme.contains("webshot screenshot https://example.com -o test.png -w 1920 -H 1080"),
"README screenshot subcommand example should use -H for viewport height"
);
}
#[test]
fn test_readme_documents_batch_output_behavior() {
let readme_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("README.md");
let readme = fs::read_to_string(readme_path).unwrap();
for detail in [
"#### Output Behavior",
"Target HTTP(S) URL (required)",
"Supported output extensions are `.png`, `.jpg`, `.jpeg`, `.webp`, and `.pdf`.",
"Webshot chooses the runtime output format from the `output` filename extension.",
"Relative screenshot `output` paths are resolved under `defaults.output_dir` when it is set.",
"The `multi` command's `-o, --output-dir` option is prepended at runtime to each loaded output path",
"artifacts/screenshots/home.png",
"Parent directories for screenshot, PDF, text, diff-image, and JSON comparison outputs are created automatically.",
"Existing output files are replaced when a command writes the same path.",
] {
assert!(
readme.contains(detail),
"README should document batch/output behavior detail: {detail}"
);
}
}
#[test]
fn test_readme_release_flow_matches_github_actions() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let readme = fs::read_to_string(manifest_dir.join("README.md"))
.expect("README.md must exist for release-flow doc test");
let workflow = fs::read_to_string(manifest_dir.join(".github/workflows/rust.yml"))
.expect(".github/workflows/rust.yml must exist for release-flow doc test");
let workflow_yaml: serde_yaml::Value =
serde_yaml::from_str(&workflow).expect("rust.yml must be valid YAML");
let workflow_name = workflow_yaml["name"]
.as_str()
.expect("workflow should have a name");
let triggers = &workflow_yaml["on"];
let jobs = &workflow_yaml["jobs"];
let test_job = &jobs["test"];
let release_job = &jobs["release"];
assert_eq!(workflow_name, "Rust CI");
assert_eq!(test_job["name"].as_str(), Some("Test"));
assert_eq!(release_job["name"].as_str(), Some("Create Release"));
assert_eq!(release_job["needs"].as_str(), Some("test"));
assert_eq!(
release_job["if"].as_str(),
Some("startsWith(github.ref, 'refs/tags/')")
);
for branch in ["master", "main"] {
assert!(
yaml_sequence_contains(&triggers["push"]["branches"], branch),
"push trigger should include {branch}"
);
assert!(
yaml_sequence_contains(&triggers["pull_request"]["branches"], branch),
"pull_request trigger should include {branch}"
);
}
assert!(
yaml_sequence_contains(&triggers["push"]["tags"], "v*"),
"push trigger should include v* tags"
);
assert!(
job_has_run_step(test_job, "cargo test --verbose --all-features"),
"Test job should run the documented test command"
);
assert!(
job_has_run_step(test_job, "cargo build --release --verbose"),
"Test job should build the release binary before artifact upload"
);
assert!(
job_uses_action(test_job, "actions/upload-artifact@v4"),
"Test job should upload the release binary artifact"
);
assert_eq!(
find_action_step(test_job, "actions/upload-artifact@v4")["with"]["name"].as_str(),
Some("webshot-binary")
);
assert_eq!(
find_action_step(test_job, "actions/upload-artifact@v4")["with"]["path"].as_str(),
Some("target/release/webshot")
);
assert_eq!(
find_action_step(test_job, "actions/upload-artifact@v4")["with"]["retention-days"].as_i64(),
Some(7)
);
assert!(
job_has_run_step(release_job, "cargo build --release --verbose"),
"Create Release job should rebuild the release binary after Test succeeds"
);
assert!(
job_uses_action(release_job, "softprops/action-gh-release@v1"),
"Create Release job should publish through the documented release action"
);
assert_eq!(
find_action_step(release_job, "softprops/action-gh-release@v1")["with"]["files"].as_str(),
Some("target/release/webshot")
);
assert_eq!(
find_action_step(release_job, "softprops/action-gh-release@v1")["with"]
["generate_release_notes"]
.as_bool(),
Some(true)
);
let documented_details = [
("workflow name", "`Rust CI` workflow"),
("push branches", "pushes to `master` or `main`"),
(
"pull request target branches",
"`pull_request` events targeting `master` or `main`",
),
("tag release trigger", "pushed tags matching `v*`"),
(
"pull request job scope",
"Pull requests run only the `Test` job",
),
(
"release job tag scope",
"releases are triggered only by `v*` tags",
),
("test command", "`cargo test --verbose --all-features`"),
("release build command", "`cargo build --release --verbose`"),
("release binary path", "`target/release/webshot`"),
("artifact name", "`webshot-binary`"),
("artifact retention", "for seven days"),
(
"test dependency",
"If it succeeds, the `Create Release` job",
),
("release action", "`softprops/action-gh-release@v1`"),
("generated release notes", "`generate_release_notes: true`"),
];
for (description, detail) in documented_details {
assert!(
readme.contains(detail),
"README release flow should document {description}: {detail}"
);
}
}
fn yaml_sequence_contains(value: &serde_yaml::Value, expected: &str) -> bool {
value
.as_sequence()
.map(|items| items.iter().any(|item| item.as_str() == Some(expected)))
.unwrap_or(false)
}
fn job_steps(job: &serde_yaml::Value) -> &[serde_yaml::Value] {
job["steps"]
.as_sequence()
.expect("workflow job should define steps")
}
fn job_has_run_step(job: &serde_yaml::Value, expected: &str) -> bool {
job_steps(job)
.iter()
.any(|step| step["run"].as_str() == Some(expected))
}
fn job_uses_action(job: &serde_yaml::Value, action: &str) -> bool {
job_steps(job)
.iter()
.any(|step| step["uses"].as_str() == Some(action))
}
fn find_action_step<'a>(job: &'a serde_yaml::Value, action: &str) -> &'a serde_yaml::Value {
job_steps(job)
.iter()
.find(|step| step["uses"].as_str() == Some(action))
.expect("workflow job should include documented action step")
}
#[tokio::test]
async fn test_version_output() {
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("--version");
cmd.assert()
.success()
.stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")));
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_verbose_logging() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("verbose.png");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg(TEST_URL).arg("-o").arg(&output_path).arg("-v");
cmd.assert().success();
assert!(output_path.exists());
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_timeout_handling() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("timeout.png");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("https://httpbin.org/html")
.arg("-o")
.arg(&output_path)
.arg("--wait-for")
.arg(".non-existent-element-that-will-never-appear")
.arg("-t")
.arg("2");
cmd.timeout(std::time::Duration::from_secs(8))
.assert()
.failure();
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium startup before validation completes"]
async fn test_config_validation() {
let temp_dir = TempDir::new().unwrap();
let config_content = r#"
screenshots:
- url: "not-a-valid-url"
output: "test.png"
"#;
let config_path = temp_dir.path().join("invalid-config.yaml");
fs::write(&config_path, config_content).unwrap();
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("multi").arg(&config_path);
cmd.assert().failure();
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_subcommand_screenshot() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("subcommand.png");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("screenshot")
.arg(TEST_URL)
.arg("-o")
.arg(&output_path);
cmd.assert().success();
assert!(output_path.exists());
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_subcommand_pdf() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("subcommand.pdf");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("pdf")
.arg(TEST_URL)
.arg("-o")
.arg(&output_path)
.arg("--landscape");
cmd.assert().success();
assert!(output_path.exists());
let content = fs::read(&output_path).unwrap();
assert!(content.starts_with(b"%PDF"));
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_parallel_processing() {
let temp_dir = TempDir::new().unwrap();
let config_content = format!(
r#"
screenshots:
- url: "{}"
output: "parallel1.png"
- url: "{}"
output: "parallel2.png"
- url: "{}"
output: "parallel3.png"
- url: "{}"
output: "parallel4.png"
"#,
TEST_URL, TEST_URL, TEST_URL, TEST_URL
);
let config_path = temp_dir.path().join("parallel-config.yaml");
fs::write(&config_path, config_content).unwrap();
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("multi")
.arg(&config_path)
.arg("-o")
.arg(temp_dir.path())
.arg("-p")
.arg("4");
cmd.assert().success();
for i in 1..=4 {
assert!(temp_dir.path().join(format!("parallel{}.png", i)).exists());
}
}
fn create_test_image(width: u32, height: u32, color: [u8; 3], path: &std::path::Path) {
let img: image::RgbImage = image::ImageBuffer::from_fn(width, height, |_, _| image::Rgb(color));
img.save(path).unwrap();
}
#[tokio::test]
async fn test_compare_identical_images() {
let temp_dir = TempDir::new().unwrap();
let img1_path = temp_dir.path().join("img1.png");
let img2_path = temp_dir.path().join("img2.png");
create_test_image(100, 100, [255, 0, 0], &img1_path);
create_test_image(100, 100, [255, 0, 0], &img2_path);
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare").arg(&img1_path).arg(&img2_path);
cmd.assert().code(0);
}
#[tokio::test]
async fn test_compare_different_images() {
let temp_dir = TempDir::new().unwrap();
let img1_path = temp_dir.path().join("img1.png");
let img2_path = temp_dir.path().join("img2.png");
create_test_image(100, 100, [255, 0, 0], &img1_path); create_test_image(100, 100, [0, 255, 0], &img2_path);
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare").arg(&img1_path).arg(&img2_path);
cmd.assert().code(1);
}
#[tokio::test]
async fn test_compare_with_diff_image() {
let temp_dir = TempDir::new().unwrap();
let img1_path = temp_dir.path().join("img1.png");
let img2_path = temp_dir.path().join("img2.png");
let diff_path = temp_dir.path().join("diff.png");
create_test_image(100, 100, [255, 0, 0], &img1_path);
create_test_image(100, 100, [0, 255, 0], &img2_path);
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare")
.arg(&img1_path)
.arg(&img2_path)
.arg("--diff-image")
.arg("--diff-path")
.arg(&diff_path);
cmd.assert().code(1);
assert!(diff_path.exists());
let metadata = fs::metadata(&diff_path).unwrap();
assert!(metadata.len() > 0);
}
#[tokio::test]
async fn test_compare_json_output_creates_parent_dirs_and_overwrites() {
let temp_dir = TempDir::new().unwrap();
let img1_path = temp_dir.path().join("img1.png");
let img2_path = temp_dir.path().join("img2.png");
let output_path = temp_dir
.path()
.join("reports")
.join("nested")
.join("results.json");
create_test_image(50, 50, [255, 0, 0], &img1_path);
create_test_image(50, 50, [0, 255, 0], &img2_path);
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare")
.arg(&img1_path)
.arg(&img2_path)
.arg("--format")
.arg("json")
.arg("-o")
.arg(&output_path);
cmd.assert().code(1);
assert_json_comparison_output(&output_path);
fs::write(&output_path, "stale output").unwrap();
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare")
.arg(&img1_path)
.arg(&img2_path)
.arg("--format")
.arg("json")
.arg("-o")
.arg(&output_path);
cmd.assert().code(1);
let content = fs::read_to_string(&output_path).unwrap();
assert!(!content.contains("stale output"));
assert_json_comparison_output(&output_path);
}
fn assert_json_comparison_output(output_path: &Path) {
assert!(output_path.exists());
let content = fs::read_to_string(output_path).unwrap();
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
assert!(json["similar"].is_boolean());
assert!(json["similarity"].is_number());
assert!(json["algorithm"].is_string());
assert!(json["threshold"].is_number());
assert!(json["total_pixels"].is_number());
}
#[tokio::test]
async fn test_compare_text_output_creates_parent_dirs_and_overwrites() {
let temp_dir = TempDir::new().unwrap();
let img1_path = temp_dir.path().join("img1.png");
let img2_path = temp_dir.path().join("img2.png");
let output_path = temp_dir
.path()
.join("reports")
.join("nested")
.join("results.txt");
create_test_image(50, 50, [255, 0, 0], &img1_path);
create_test_image(50, 50, [255, 0, 0], &img2_path);
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare")
.arg(&img1_path)
.arg(&img2_path)
.arg("-o")
.arg(&output_path);
cmd.assert().code(0);
assert!(output_path.exists());
assert!(fs::read_to_string(&output_path)
.unwrap()
.contains("Similar: YES"));
fs::write(&output_path, "stale output").unwrap();
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare")
.arg(&img1_path)
.arg(&img2_path)
.arg("-o")
.arg(&output_path);
cmd.assert().code(0);
let content = fs::read_to_string(&output_path).unwrap();
assert!(content.contains("Image Comparison Results"));
assert!(!content.contains("stale output"));
}
#[tokio::test]
async fn test_compare_diff_image_creates_parent_dirs_and_overwrites() {
let temp_dir = TempDir::new().unwrap();
let img1_path = temp_dir.path().join("img1.png");
let img2_path = temp_dir.path().join("img2.png");
let diff_path = temp_dir
.path()
.join("diffs")
.join("nested")
.join("diff.png");
create_test_image(50, 50, [255, 0, 0], &img1_path);
create_test_image(50, 50, [0, 255, 0], &img2_path);
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare")
.arg(&img1_path)
.arg(&img2_path)
.arg("--diff-image")
.arg("--diff-path")
.arg(&diff_path);
cmd.assert().code(1);
assert!(diff_path.exists());
assert!(image::open(&diff_path).is_ok());
fs::write(&diff_path, "stale output").unwrap();
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare")
.arg(&img1_path)
.arg(&img2_path)
.arg("--diff-image")
.arg("--diff-path")
.arg(&diff_path);
cmd.assert().code(1);
let diff_image = image::open(&diff_path).unwrap();
assert_eq!(diff_image.width(), 50);
assert_eq!(diff_image.height(), 50);
assert_ne!(fs::read(&diff_path).unwrap(), b"stale output");
}
#[tokio::test]
async fn test_compare_different_algorithms() {
let temp_dir = TempDir::new().unwrap();
let img1_path = temp_dir.path().join("img1.png");
let img2_path = temp_dir.path().join("img2.png");
create_test_image(50, 50, [255, 0, 0], &img1_path);
create_test_image(50, 50, [250, 5, 5], &img2_path);
let algorithms = ["pixel-diff", "ssim", "mse", "psnr"];
for algorithm in &algorithms {
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare")
.arg(&img1_path)
.arg(&img2_path)
.arg("-a")
.arg(algorithm)
.arg("--format")
.arg("json");
let assertion = cmd.assert();
let output = assertion.get_output();
let stdout = String::from_utf8(output.stdout.clone()).unwrap();
if !stdout.is_empty() {
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let expected_algorithm = match *algorithm {
"pixel-diff" => "PixelDiff",
"ssim" => "SSIM",
"mse" => "MSE",
"psnr" => "PSNR",
_ => *algorithm,
};
assert_eq!(json["algorithm"].as_str().unwrap(), expected_algorithm);
}
}
}
#[tokio::test]
async fn test_compare_with_threshold() {
let temp_dir = TempDir::new().unwrap();
let img1_path = temp_dir.path().join("img1.png");
let img2_path = temp_dir.path().join("img2.png");
create_test_image(50, 50, [255, 0, 0], &img1_path);
create_test_image(50, 50, [200, 50, 50], &img2_path);
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare")
.arg(&img1_path)
.arg(&img2_path)
.arg("-t")
.arg("0.01");
cmd.assert().code(1);
let similar_img1_path = temp_dir.path().join("similar1.png");
let similar_img2_path = temp_dir.path().join("similar2.png");
create_test_image(50, 50, [255, 0, 0], &similar_img1_path);
create_test_image(50, 50, [253, 2, 2], &similar_img2_path);
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare")
.arg(&similar_img1_path)
.arg(&similar_img2_path)
.arg("-t")
.arg("0.5") .arg("--ignore-antialiasing");
cmd.assert().code(0); }
#[tokio::test]
async fn test_compare_custom_diff_color() {
let temp_dir = TempDir::new().unwrap();
let img1_path = temp_dir.path().join("img1.png");
let img2_path = temp_dir.path().join("img2.png");
let diff_path = temp_dir.path().join("diff.png");
create_test_image(50, 50, [255, 0, 0], &img1_path);
create_test_image(50, 50, [0, 255, 0], &img2_path);
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare")
.arg(&img1_path)
.arg(&img2_path)
.arg("--diff-image")
.arg("--diff-path")
.arg(&diff_path)
.arg("--diff-color")
.arg("0,0,255");
cmd.assert().code(1);
assert!(diff_path.exists());
}
#[tokio::test]
async fn test_compare_dimension_mismatch() {
let temp_dir = TempDir::new().unwrap();
let img1_path = temp_dir.path().join("img1.png");
let img2_path = temp_dir.path().join("img2.png");
create_test_image(100, 100, [255, 0, 0], &img1_path);
create_test_image(200, 100, [255, 0, 0], &img2_path);
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare").arg(&img1_path).arg(&img2_path);
cmd.assert()
.failure()
.stderr(predicate::str::contains("dimensions don't match"));
}
#[tokio::test]
async fn test_compare_invalid_files() {
let temp_dir = TempDir::new().unwrap();
let nonexistent_path = temp_dir.path().join("nonexistent.png");
let valid_path = temp_dir.path().join("valid.png");
create_test_image(50, 50, [255, 0, 0], &valid_path);
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare").arg(&nonexistent_path).arg(&valid_path);
cmd.assert()
.failure()
.stderr(predicate::str::contains("Failed to load first image"));
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare").arg(&valid_path).arg(&nonexistent_path);
cmd.assert()
.failure()
.stderr(predicate::str::contains("Failed to load second image"));
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_full_page_screenshot() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("fullpage.png");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg(TEST_URL)
.arg("-o")
.arg(&output_path)
.arg("--full-page")
.arg("--max-height")
.arg("5000");
cmd.assert().success();
assert!(output_path.exists());
let metadata = fs::metadata(&output_path).unwrap();
assert!(metadata.len() > 0);
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_full_page_with_config() {
let temp_dir = TempDir::new().unwrap();
let config_content = format!(
r#"
defaults:
scroll_mode: "FullPage"
max_height: 3000
scroll_delay: 50
screenshots:
- url: "{}"
output: "fullpage-config.png"
width: 800
height: 600
"#,
TEST_URL
);
let config_path = temp_dir.path().join("scroll-config.yaml");
fs::write(&config_path, config_content).unwrap();
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("multi")
.arg(&config_path)
.arg("-o")
.arg(temp_dir.path());
cmd.assert().success();
assert!(temp_dir.path().join("fullpage-config.png").exists());
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_full_element_screenshot() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("element-full.png");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg(TEST_URL)
.arg("-o")
.arg(&output_path)
.arg("-s")
.arg("body") .arg("--full-page") .arg("--max-height")
.arg("3000");
cmd.assert().success();
assert!(output_path.exists());
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_scroll_delay_option() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("scroll-delay.png");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg(TEST_URL)
.arg("-o")
.arg(&output_path)
.arg("--full-page")
.arg("--scroll-delay")
.arg("200")
.arg("--max-height")
.arg("2000");
cmd.assert().success();
assert!(output_path.exists());
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_viewport_vs_fullpage_difference() {
let temp_dir = TempDir::new().unwrap();
let viewport_path = temp_dir.path().join("viewport.png");
let fullpage_path = temp_dir.path().join("fullpage.png");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg(TEST_URL)
.arg("-o")
.arg(&viewport_path)
.arg("-w")
.arg("800")
.arg("-H")
.arg("600");
cmd.assert().success();
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg(TEST_URL)
.arg("-o")
.arg(&fullpage_path)
.arg("-w")
.arg("800")
.arg("-H")
.arg("600")
.arg("--full-page")
.arg("--max-height")
.arg("3000");
cmd.assert().success();
assert!(viewport_path.exists());
assert!(fullpage_path.exists());
let viewport_size = fs::metadata(&viewport_path).unwrap().len();
let fullpage_size = fs::metadata(&fullpage_path).unwrap().len();
assert!(viewport_size > 0);
assert!(fullpage_size > 0);
}
#[tokio::test]
#[ignore = "requires Chrome/Chromium and network access"]
async fn test_max_height_limit() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("height-limited.png");
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg(TEST_URL)
.arg("-o")
.arg(&output_path)
.arg("--full-page")
.arg("--max-height")
.arg("1000");
cmd.assert().success();
assert!(output_path.exists());
}
#[tokio::test]
async fn test_scroll_mode_config_validation() {
let temp_dir = TempDir::new().unwrap();
let invalid_config = r#"
screenshots:
- url: "https://httpbin.org/html"
output: "invalid.png"
scroll_mode: "FullElement"
"#;
let config_path = temp_dir.path().join("invalid-scroll-config.yaml");
fs::write(&config_path, invalid_config).unwrap();
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("multi").arg(&config_path);
cmd.assert().failure().stderr(predicate::str::contains(
"FullElement scroll mode requires a selector",
));
}
#[tokio::test]
async fn test_compare_invalid_algorithm() {
let temp_dir = TempDir::new().unwrap();
let img1_path = temp_dir.path().join("img1.png");
let img2_path = temp_dir.path().join("img2.png");
create_test_image(50, 50, [255, 0, 0], &img1_path);
create_test_image(50, 50, [0, 255, 0], &img2_path);
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare")
.arg(&img1_path)
.arg(&img2_path)
.arg("-a")
.arg("invalid-algorithm");
cmd.assert()
.failure()
.stderr(predicate::str::contains("Unknown algorithm"));
}
#[tokio::test]
async fn test_compare_text_stdout_output_format() {
let temp_dir = TempDir::new().unwrap();
let img1_path = temp_dir.path().join("img1.png");
let img2_path = temp_dir.path().join("img2.png");
create_test_image(50, 50, [255, 0, 0], &img1_path);
create_test_image(50, 50, [0, 255, 0], &img2_path);
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare")
.arg(&img1_path)
.arg(&img2_path)
.arg("--format")
.arg("text");
cmd.assert()
.code(1)
.stdout(predicate::str::contains("Image Comparison Results"))
.stdout(predicate::str::contains("Algorithm:"))
.stdout(predicate::str::contains("Similarity:"))
.stdout(predicate::str::contains("Similar:"));
}
#[tokio::test]
async fn test_compare_text_output_format() {
let temp_dir = TempDir::new().unwrap();
let img1_path = temp_dir.path().join("img1.png");
let img2_path = temp_dir.path().join("img2.png");
let output_path = temp_dir.path().join("reports").join("results.txt");
create_test_image(50, 50, [255, 0, 0], &img1_path);
create_test_image(50, 50, [0, 255, 0], &img2_path);
let mut cmd = Command::cargo_bin("webshot").unwrap();
cmd.arg("compare")
.arg(&img1_path)
.arg(&img2_path)
.arg("--format")
.arg("text")
.arg("-o")
.arg(&output_path);
cmd.assert().code(1);
let text_output = fs::read_to_string(&output_path).unwrap();
assert!(text_output.contains("Image Comparison Results"));
assert!(text_output.contains("Algorithm:"));
assert!(text_output.contains("Similarity:"));
assert!(text_output.contains("Similar:"));
}