diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 3e18f60..e4ecf42 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -194,6 +194,17 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result { + let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "download".to_string(), + usage: "download ", + })?; + let path = rest.get(1).ok_or_else(|| ParseError::MissingArguments { + context: "download".to_string(), + usage: "download ", + })?; + Ok(json!({ "id": id, "action": "download", "selector": sel, "path": path })) + } // === Keyboard === "press" | "key" => { @@ -284,6 +295,27 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result() { + cmd["timeout"] = json!(timeout); + } + } + } + return Ok(cmd); + } + // Default: selector or timeout if let Some(arg) = rest.get(0) { if arg.parse::().is_ok() { @@ -1752,6 +1784,76 @@ mod tests { assert!(cmd.get("value").is_none()); } + // === Download Tests === + + #[test] + fn test_download() { + let cmd = parse_command(&args("download #btn ./file.pdf"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "download"); + assert_eq!(cmd["selector"], "#btn"); + assert_eq!(cmd["path"], "./file.pdf"); + } + + #[test] + fn test_download_with_ref() { + let cmd = parse_command(&args("download @e5 ./report.xlsx"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "download"); + assert_eq!(cmd["selector"], "@e5"); + assert_eq!(cmd["path"], "./report.xlsx"); + } + + #[test] + fn test_download_missing_path() { + let result = parse_command(&args("download #btn"), &default_flags()); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. })); + } + + #[test] + fn test_download_missing_selector() { + let result = parse_command(&args("download"), &default_flags()); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. })); + } + + // === Wait for Download Tests === + + #[test] + fn test_wait_download() { + let cmd = parse_command(&args("wait --download"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "waitfordownload"); + assert!(cmd.get("path").is_none()); + } + + #[test] + fn test_wait_download_with_path() { + let cmd = parse_command(&args("wait --download ./file.pdf"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "waitfordownload"); + assert_eq!(cmd["path"], "./file.pdf"); + } + + #[test] + fn test_wait_download_with_timeout() { + let cmd = parse_command(&args("wait --download --timeout 30000"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "waitfordownload"); + assert_eq!(cmd["timeout"], 30000); + } + + #[test] + fn test_wait_download_with_path_and_timeout() { + let cmd = parse_command(&args("wait --download ./file.pdf --timeout 30000"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "waitfordownload"); + assert_eq!(cmd["path"], "./file.pdf"); + assert_eq!(cmd["timeout"], 30000); + } + + #[test] + fn test_wait_download_short_flag() { + let cmd = parse_command(&args("wait -d ./file.pdf"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "waitfordownload"); + assert_eq!(cmd["path"], "./file.pdf"); + } + // === Connect (CDP) tests === #[test] diff --git a/cli/src/output.rs b/cli/src/output.rs index 714a2ae..6769f38 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -220,7 +220,22 @@ pub fn print_response(resp: &Response, json_mode: bool) { } return; } - // Screenshot path (no "started" or "frames" field) + // Download response (has "suggestedFilename" or "filename" field) + if data.get("suggestedFilename").is_some() || data.get("filename").is_some() { + if let Some(path) = data.get("path").and_then(|v| v.as_str()) { + let filename = data.get("suggestedFilename") + .or_else(|| data.get("filename")) + .and_then(|v| v.as_str()) + .unwrap_or(""); + if filename.is_empty() { + println!("{} Downloaded to {}", color::success_indicator(), color::green(path)); + } else { + println!("{} Downloaded to {} ({})", color::success_indicator(), color::green(path), filename); + } + return; + } + } + // Screenshot path (no "started", "frames", or download fields) if let Some(path) = data.get("path").and_then(|v| v.as_str()) { println!("{} Screenshot saved to {}", color::success_indicator(), color::green(path)); return; @@ -513,6 +528,28 @@ Examples: agent-browser upload @e3 ./image1.png ./image2.png "## } + "download" => { + r##" +agent-browser download - Download a file by clicking an element + +Usage: agent-browser download + +Clicks an element that triggers a download and saves the file to the specified path. + +Arguments: + selector Element to click (CSS selector or @ref) + path Path where the downloaded file will be saved + +Global Options: + --json Output as JSON + --session Use specific session + +Examples: + agent-browser download "#download-btn" ./file.pdf + agent-browser download @e5 ./report.xlsx + agent-browser download "a[href$='.zip']" ./archive.zip +"## + } // === Keyboard === "press" | "key" => { @@ -642,6 +679,10 @@ Modes: --load Wait for load state (load, domcontentloaded, networkidle) --fn Wait for JavaScript expression to be truthy --text Wait for text to appear on page + --download [path] Wait for a download to complete (optionally save to path) + +Download Options (with --download): + --timeout Timeout in milliseconds for download to start Global Options: --json Output as JSON @@ -654,6 +695,8 @@ Examples: agent-browser wait --load networkidle agent-browser wait --fn "window.appReady === true" agent-browser wait --text "Welcome back" + agent-browser wait --download ./file.pdf + agent-browser wait --download ./report.xlsx --timeout 30000 "## } @@ -1384,6 +1427,7 @@ Core Commands: select Select dropdown option drag Drag and drop upload Upload files + download Download file by clicking element scroll [px] Scroll (up/down/left/right) scrollintoview Scroll element into view wait Wait for element or time diff --git a/src/actions.ts b/src/actions.ts index 7fde0dd..bf490c3 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -1074,11 +1074,9 @@ async function handleDownload( browser: BrowserManager ): Promise { const page = browser.getPage(); + const locator = browser.getLocator(command.selector); - const [download] = await Promise.all([ - page.waitForEvent('download'), - page.click(command.selector), - ]); + const [download] = await Promise.all([page.waitForEvent('download'), locator.click()]); await download.saveAs(command.path); return successResponse(command.id, {