add --selector flag to scroll command (#537)

* add --selector flag to scroll command

The `scroll` command uses `window.scrollBy()`, which has no effect on apps
that use custom scrollable containers (e.g. a nested div with overflow-y: auto).

The backend `handleScroll` already supports a `selector` parameter, but the CLI
never exposed it. This adds `-s` / `--selector` to the `scroll` command so users
can target a specific scrollable element:

    agent-browser scroll down 500 --selector "div.scroll-container"

Also fixes the backend to apply `direction`/`amount` when a selector is present
(previously those fields were only used in the no-selector branch).

Closes #501

* fixes
This commit is contained in:
Chris Tate
2026-02-24 07:40:46 -06:00
committed by GitHub
parent 77f2caa1bc
commit f319195974
6 changed files with 157 additions and 35 deletions
+1 -1
View File
@@ -109,7 +109,7 @@ agent-browser hover <sel> # Hover element
agent-browser select <sel> <val> # Select dropdown option agent-browser select <sel> <val> # Select dropdown option
agent-browser check <sel> # Check checkbox agent-browser check <sel> # Check checkbox
agent-browser uncheck <sel> # Uncheck checkbox agent-browser uncheck <sel> # Uncheck checkbox
agent-browser scroll <dir> [px] # Scroll (up/down/left/right) agent-browser scroll <dir> [px] # Scroll (up/down/left/right, --selector <sel>)
agent-browser scrollintoview <sel> # Scroll element into view (alias: scrollinto) agent-browser scrollintoview <sel> # Scroll element into view (alias: scrollinto)
agent-browser drag <src> <tgt> # Drag and drop agent-browser drag <src> <tgt> # Drag and drop
agent-browser upload <sel> <files> # Upload files agent-browser upload <sel> <files> # Upload files
+123 -6
View File
@@ -298,12 +298,48 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
// === Scroll === // === Scroll ===
"scroll" => { "scroll" => {
let dir = rest.first().unwrap_or(&"down"); let mut cmd = json!({ "id": id, "action": "scroll" });
let amount = rest let obj = cmd.as_object_mut().unwrap();
.get(1) let mut positional_index = 0;
.and_then(|s| s.parse::<i32>().ok()) let mut i = 0;
.unwrap_or(300); while i < rest.len() {
Ok(json!({ "id": id, "action": "scroll", "direction": dir, "amount": amount })) match rest[i] {
"-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: "scroll --selector".to_string(),
usage: "scroll [direction] [amount] [--selector <sel>]",
});
}
}
arg if arg.starts_with('-') => {}
_ => {
match positional_index {
0 => {
obj.insert("direction".to_string(), json!(rest[i]));
}
1 => {
if let Ok(n) = rest[i].parse::<i32>() {
obj.insert("amount".to_string(), json!(n));
}
}
_ => {}
}
positional_index += 1;
}
}
i += 1;
}
if !obj.contains_key("direction") {
obj.insert("direction".to_string(), json!("down"));
}
if !obj.contains_key("amount") {
obj.insert("amount".to_string(), json!(300));
}
Ok(cmd)
} }
"scrollintoview" | "scrollinto" => { "scrollintoview" | "scrollinto" => {
let sel = rest.first().ok_or_else(|| ParseError::MissingArguments { let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {
@@ -3436,4 +3472,85 @@ mod tests {
ParseError::MissingArguments { .. } ParseError::MissingArguments { .. }
)); ));
} }
// === Scroll Tests ===
#[test]
fn test_scroll_defaults() {
let cmd = parse_command(&args("scroll"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "scroll");
assert_eq!(cmd["direction"], "down");
assert_eq!(cmd["amount"], 300);
assert!(cmd.get("selector").is_none());
}
#[test]
fn test_scroll_direction_and_amount() {
let cmd = parse_command(&args("scroll up 200"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "scroll");
assert_eq!(cmd["direction"], "up");
assert_eq!(cmd["amount"], 200);
}
#[test]
fn test_scroll_with_selector() {
let cmd = parse_command(
&args("scroll down 500 --selector div.scroll-container"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "scroll");
assert_eq!(cmd["direction"], "down");
assert_eq!(cmd["amount"], 500);
assert_eq!(cmd["selector"], "div.scroll-container");
}
#[test]
fn test_scroll_with_selector_short_flag() {
let cmd = parse_command(
&args("scroll left 100 -s .sidebar"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "scroll");
assert_eq!(cmd["direction"], "left");
assert_eq!(cmd["amount"], 100);
assert_eq!(cmd["selector"], ".sidebar");
}
#[test]
fn test_scroll_selector_before_positional() {
let cmd = parse_command(
&args("scroll --selector .panel down 400"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "scroll");
assert_eq!(cmd["direction"], "down");
assert_eq!(cmd["amount"], 400);
assert_eq!(cmd["selector"], ".panel");
}
#[test]
fn test_scroll_selector_only() {
let cmd = parse_command(
&args("scroll --selector .content"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "scroll");
assert_eq!(cmd["direction"], "down");
assert_eq!(cmd["amount"], 300);
assert_eq!(cmd["selector"], ".content");
}
#[test]
fn test_scroll_selector_missing_value() {
let result = parse_command(&args("scroll down 500 --selector"), &default_flags());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
}
} }
+6 -2
View File
@@ -973,14 +973,17 @@ Use Cases:
r##" r##"
agent-browser scroll - Scroll the page agent-browser scroll - Scroll the page
Usage: agent-browser scroll [direction] [amount] Usage: agent-browser scroll [direction] [amount] [options]
Scrolls the page in the specified direction. Scrolls the page or a specific element in the specified direction.
Arguments: Arguments:
direction up, down, left, right (default: down) direction up, down, left, right (default: down)
amount Pixels to scroll (default: 300) amount Pixels to scroll (default: 300)
Options:
-s, --selector <sel> CSS selector for a scrollable container
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
@@ -990,6 +993,7 @@ Examples:
agent-browser scroll down 500 agent-browser scroll down 500
agent-browser scroll up 200 agent-browser scroll up 200
agent-browser scroll left 100 agent-browser scroll left 100
agent-browser scroll down 500 --selector "div.scroll-container"
"## "##
} }
"scrollintoview" | "scrollinto" => { "scrollintoview" | "scrollinto" => {
+1 -1
View File
@@ -22,7 +22,7 @@ agent-browser focus <sel> # Focus element
agent-browser select <sel> <val> # Select dropdown option agent-browser select <sel> <val> # Select dropdown option
agent-browser check <sel> # Check checkbox agent-browser check <sel> # Check checkbox
agent-browser uncheck <sel> # Uncheck checkbox agent-browser uncheck <sel> # Uncheck checkbox
agent-browser scroll <dir> [px] # Scroll (up/down/left/right) agent-browser scroll <dir> [px] # Scroll (up/down/left/right, --selector <sel>)
agent-browser scrollintoview <sel> # Scroll element into view agent-browser scrollintoview <sel> # Scroll element into view
agent-browser drag <src> <dst> # Drag and drop agent-browser drag <src> <dst> # Drag and drop
agent-browser upload <sel> <files> # Upload files agent-browser upload <sel> <files> # Upload files
+1
View File
@@ -67,6 +67,7 @@ agent-browser press Enter # Press key
agent-browser keyboard type "text" # Type at current focus (no selector) agent-browser keyboard type "text" # Type at current focus (no selector)
agent-browser keyboard inserttext "text" # Insert without key events agent-browser keyboard inserttext "text" # Insert without key events
agent-browser scroll down 500 # Scroll page agent-browser scroll down 500 # Scroll page
agent-browser scroll down 500 --selector "div.content" # Scroll within a specific container
# Get information # Get information
agent-browser get text @e1 # Get element text agent-browser get text @e1 # Get element text
+25 -25
View File
@@ -882,41 +882,41 @@ async function handleWait(command: WaitCommand, browser: BrowserManager): Promis
async function handleScroll(command: ScrollCommand, browser: BrowserManager): Promise<Response> { async function handleScroll(command: ScrollCommand, browser: BrowserManager): Promise<Response> {
const page = browser.getPage(); const page = browser.getPage();
let deltaX = command.x ?? 0;
let deltaY = command.y ?? 0;
const hasExplicitDelta = command.x !== undefined || command.y !== undefined;
if (command.direction) {
const amount = command.amount ?? 100;
switch (command.direction) {
case 'up':
deltaY = -amount;
break;
case 'down':
deltaY = amount;
break;
case 'left':
deltaX = -amount;
break;
case 'right':
deltaX = amount;
break;
}
}
if (command.selector) { if (command.selector) {
const element = browser.getLocator(command.selector); const element = browser.getLocator(command.selector);
await element.scrollIntoViewIfNeeded(); await element.scrollIntoViewIfNeeded();
if (command.x !== undefined || command.y !== undefined) { if (hasExplicitDelta || deltaX !== 0 || deltaY !== 0) {
await element.evaluate( await element.evaluate(
(el, { x, y }) => { (el, { x, y }) => {
el.scrollBy(x ?? 0, y ?? 0); el.scrollBy(x, y);
}, },
{ x: command.x, y: command.y } { x: deltaX, y: deltaY }
); );
} }
} else { } else {
// Scroll the page
let deltaX = command.x ?? 0;
let deltaY = command.y ?? 0;
if (command.direction) {
const amount = command.amount ?? 100;
switch (command.direction) {
case 'up':
deltaY = -amount;
break;
case 'down':
deltaY = amount;
break;
case 'left':
deltaX = -amount;
break;
case 'right':
deltaX = amount;
break;
}
}
await page.evaluate(`window.scrollBy(${deltaX}, ${deltaY})`); await page.evaluate(`window.scrollBy(${deltaX}, ${deltaY})`);
} }