feat: add download CLI commands with ref support (#183)
* feat: add download and waitfordownload CLI commands Add CLI support for the existing download functionality in the daemon: - `download <selector> <path>`: Click an element to trigger download and save to specified path - `wait --download [path] [--timeout ms]`: Wait for any download to complete, optionally save to path with configurable timeout Includes comprehensive unit tests and help documentation. * fix: download command ref support and output message - Fix handleDownload to use browser.getLocator() for ref selector support - Fix CLI output to show "Downloaded to" instead of "Screenshot saved" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Chris Tate <chris@ctate.dev>
This commit is contained in:
co-authored by
Claude Opus 4.5
Chris Tate
parent
307f970d53
commit
55f4eaa728
@@ -194,6 +194,17 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
|||||||
})?;
|
})?;
|
||||||
Ok(json!({ "id": id, "action": "upload", "selector": sel, "files": &rest[1..] }))
|
Ok(json!({ "id": id, "action": "upload", "selector": sel, "files": &rest[1..] }))
|
||||||
}
|
}
|
||||||
|
"download" => {
|
||||||
|
let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments {
|
||||||
|
context: "download".to_string(),
|
||||||
|
usage: "download <selector> <path>",
|
||||||
|
})?;
|
||||||
|
let path = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
||||||
|
context: "download".to_string(),
|
||||||
|
usage: "download <selector> <path>",
|
||||||
|
})?;
|
||||||
|
Ok(json!({ "id": id, "action": "download", "selector": sel, "path": path }))
|
||||||
|
}
|
||||||
|
|
||||||
// === Keyboard ===
|
// === Keyboard ===
|
||||||
"press" | "key" => {
|
"press" | "key" => {
|
||||||
@@ -284,6 +295,27 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for --download flag: wait --download [path] [--timeout ms]
|
||||||
|
if rest.iter().any(|&s| s == "--download" || s == "-d") {
|
||||||
|
let mut cmd = json!({ "id": id, "action": "waitfordownload" });
|
||||||
|
// Check for optional path (first non-flag argument after --download)
|
||||||
|
let download_idx = rest.iter().position(|&s| s == "--download" || s == "-d").unwrap();
|
||||||
|
if let Some(path) = rest.get(download_idx + 1) {
|
||||||
|
if !path.starts_with("--") {
|
||||||
|
cmd["path"] = json!(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check for optional timeout
|
||||||
|
if let Some(idx) = rest.iter().position(|&s| s == "--timeout") {
|
||||||
|
if let Some(timeout_str) = rest.get(idx + 1) {
|
||||||
|
if let Ok(timeout) = timeout_str.parse::<u64>() {
|
||||||
|
cmd["timeout"] = json!(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
// Default: selector or timeout
|
// Default: selector or timeout
|
||||||
if let Some(arg) = rest.get(0) {
|
if let Some(arg) = rest.get(0) {
|
||||||
if arg.parse::<u64>().is_ok() {
|
if arg.parse::<u64>().is_ok() {
|
||||||
@@ -1752,6 +1784,76 @@ mod tests {
|
|||||||
assert!(cmd.get("value").is_none());
|
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 ===
|
// === Connect (CDP) tests ===
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+45
-1
@@ -220,7 +220,22 @@ pub fn print_response(resp: &Response, json_mode: bool) {
|
|||||||
}
|
}
|
||||||
return;
|
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()) {
|
if let Some(path) = data.get("path").and_then(|v| v.as_str()) {
|
||||||
println!("{} Screenshot saved to {}", color::success_indicator(), color::green(path));
|
println!("{} Screenshot saved to {}", color::success_indicator(), color::green(path));
|
||||||
return;
|
return;
|
||||||
@@ -513,6 +528,28 @@ Examples:
|
|||||||
agent-browser upload @e3 ./image1.png ./image2.png
|
agent-browser upload @e3 ./image1.png ./image2.png
|
||||||
"##
|
"##
|
||||||
}
|
}
|
||||||
|
"download" => {
|
||||||
|
r##"
|
||||||
|
agent-browser download - Download a file by clicking an element
|
||||||
|
|
||||||
|
Usage: agent-browser download <selector> <path>
|
||||||
|
|
||||||
|
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 <name> 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 ===
|
// === Keyboard ===
|
||||||
"press" | "key" => {
|
"press" | "key" => {
|
||||||
@@ -642,6 +679,10 @@ Modes:
|
|||||||
--load <state> Wait for load state (load, domcontentloaded, networkidle)
|
--load <state> Wait for load state (load, domcontentloaded, networkidle)
|
||||||
--fn <expression> Wait for JavaScript expression to be truthy
|
--fn <expression> Wait for JavaScript expression to be truthy
|
||||||
--text <text> Wait for text to appear on page
|
--text <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 <ms> Timeout in milliseconds for download to start
|
||||||
|
|
||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
@@ -654,6 +695,8 @@ Examples:
|
|||||||
agent-browser wait --load networkidle
|
agent-browser wait --load networkidle
|
||||||
agent-browser wait --fn "window.appReady === true"
|
agent-browser wait --fn "window.appReady === true"
|
||||||
agent-browser wait --text "Welcome back"
|
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 <sel> <val...> Select dropdown option
|
select <sel> <val...> Select dropdown option
|
||||||
drag <src> <dst> Drag and drop
|
drag <src> <dst> Drag and drop
|
||||||
upload <sel> <files...> Upload files
|
upload <sel> <files...> Upload files
|
||||||
|
download <sel> <path> Download file by clicking element
|
||||||
scroll <dir> [px] Scroll (up/down/left/right)
|
scroll <dir> [px] Scroll (up/down/left/right)
|
||||||
scrollintoview <sel> Scroll element into view
|
scrollintoview <sel> Scroll element into view
|
||||||
wait <sel|ms> Wait for element or time
|
wait <sel|ms> Wait for element or time
|
||||||
|
|||||||
+2
-4
@@ -1074,11 +1074,9 @@ async function handleDownload(
|
|||||||
browser: BrowserManager
|
browser: BrowserManager
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
|
const locator = browser.getLocator(command.selector);
|
||||||
|
|
||||||
const [download] = await Promise.all([
|
const [download] = await Promise.all([page.waitForEvent('download'), locator.click()]);
|
||||||
page.waitForEvent('download'),
|
|
||||||
page.click(command.selector),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await download.saveAs(command.path);
|
await download.saveAs(command.path);
|
||||||
return successResponse(command.id, {
|
return successResponse(command.id, {
|
||||||
|
|||||||
Reference in New Issue
Block a user