diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 9f3837c..36e1720 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -290,7 +290,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrome-use" -version = "1.5.10" +version = "1.5.11" dependencies = [ "aes", "aes-gcm", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a9322e5..982d14e 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chrome-use" -version = "1.5.10" +version = "1.5.11" edition = "2021" description = "Fast browser automation CLI for AI agents" license = "Apache-2.0" diff --git a/cli/src/commands.rs b/cli/src/commands.rs index b67fbc0..faa6c5d 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -729,10 +729,56 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result]", + usage: "scroll [direction] [amount] [--selector ] [--at ] [--frame ]", }); } } + "--at" => { + // `--at x,y`: dispatch the wheel at this viewport pixel, so it + // scrolls whatever element/iframe is under the pointer — including + // cross-origin iframes that `window.scrollBy` can't reach (#36). + let val = rest.get(i + 1).ok_or(ParseError::MissingArguments { + context: "scroll --at".to_string(), + usage: "scroll [direction] [amount] --at ", + })?; + let mut parts = val.split(','); + match ( + parts.next().and_then(|s| s.trim().parse::().ok()), + parts.next().and_then(|s| s.trim().parse::().ok()), + ) { + (Some(x), Some(y)) => { + obj.insert("at".to_string(), json!([x, y])); + } + _ => { + return Err(ParseError::InvalidValue { + message: format!("scroll --at: invalid coordinate `{}`", val), + usage: "scroll [direction] [amount] --at (e.g. --at 640,400)", + }) + } + } + i += 1; + } + "--frame" => { + // `--frame n`: scroll the n-th frame from `chrome-use frames` by + // dispatching the wheel at that frame's center — reaches content in + // a cross-origin iframe without needing a selector into it (#36). + let val = rest.get(i + 1).ok_or(ParseError::MissingArguments { + context: "scroll --frame".to_string(), + usage: "scroll [direction] [amount] --frame ", + })?; + match val.trim().parse::() { + Ok(n) => { + obj.insert("frame".to_string(), json!(n)); + } + Err(_) => { + return Err(ParseError::InvalidValue { + message: format!("scroll --frame: invalid index `{}`", val), + usage: "scroll [direction] [amount] --frame (index from `chrome-use frames`)", + }) + } + } + i += 1; + } arg if arg.starts_with('-') => {} _ => { match positional_index { @@ -5868,6 +5914,35 @@ mod tests { assert_eq!(cmd["selector"], ".sidebar"); } + #[test] + fn test_scroll_at_coordinate() { + // `--at x,y` carries a [x, y] array for a wheel dispatched at that pixel + // (issue #36: cross-origin iframe scroll). + let cmd = parse_command(&args("scroll down 700 --at 640,400"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "scroll"); + assert_eq!(cmd["direction"], "down"); + assert_eq!(cmd["amount"], 700); + assert_eq!(cmd["at"], json!([640.0, 400.0])); + } + + #[test] + fn test_scroll_at_rejects_garbage() { + assert!(parse_command(&args("scroll --at nope"), &default_flags()).is_err()); + assert!(parse_command(&args("scroll --at 1"), &default_flags()).is_err()); + } + + #[test] + fn test_scroll_frame_index() { + let cmd = parse_command(&args("scroll down 700 --frame 2"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "scroll"); + assert_eq!(cmd["frame"], 2); + } + + #[test] + fn test_scroll_frame_rejects_non_integer() { + assert!(parse_command(&args("scroll --frame two"), &default_flags()).is_err()); + } + #[test] fn test_scroll_selector_before_positional() { let cmd = diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index f1caf80..b622568 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -3467,17 +3467,169 @@ async fn handle_scroll(cmd: &Value, state: &mut DaemonState) -> Result Result<(f64, f64), String> { + let dims = mgr + .client + .send_command_typed::<_, Value>( + "Runtime.evaluate", + &super::cdp::types::EvaluateParams { + expression: "[window.innerWidth, window.innerHeight]".to_string(), + return_by_value: Some(true), + await_promise: Some(false), + }, + Some(session_id), + ) + .await + .ok(); + let arr = dims + .as_ref() + .and_then(|v| v.get("result")) + .and_then(|v| v.get("value")) + .and_then(|v| v.as_array()); + let w = arr + .and_then(|a| a.first()) + .and_then(|v| v.as_f64()) + .filter(|w| *w > 0.0) + .unwrap_or(1280.0); + let h = arr + .and_then(|a| a.get(1)) + .and_then(|v| v.as_f64()) + .filter(|h| *h > 0.0) + .unwrap_or(800.0); + Ok((w / 2.0, h / 2.0)) +} + +/// Center of the `n`-th frame (as listed by `chrome-use frames`) in top-viewport +/// CSS pixels, so `scroll --frame n` lands its wheel inside a cross-origin iframe +/// without needing a selector into it (issue #36). Resolves the frame's owning +/// `