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:
@@ -109,7 +109,7 @@ agent-browser hover <sel> # Hover element
|
||||
agent-browser select <sel> <val> # Select dropdown option
|
||||
agent-browser check <sel> # Check 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 drag <src> <tgt> # Drag and drop
|
||||
agent-browser upload <sel> <files> # Upload files
|
||||
|
||||
+123
-6
@@ -298,12 +298,48 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
|
||||
// === Scroll ===
|
||||
"scroll" => {
|
||||
let dir = rest.first().unwrap_or(&"down");
|
||||
let amount = rest
|
||||
.get(1)
|
||||
.and_then(|s| s.parse::<i32>().ok())
|
||||
.unwrap_or(300);
|
||||
Ok(json!({ "id": id, "action": "scroll", "direction": dir, "amount": amount }))
|
||||
let mut cmd = json!({ "id": id, "action": "scroll" });
|
||||
let obj = cmd.as_object_mut().unwrap();
|
||||
let mut positional_index = 0;
|
||||
let mut i = 0;
|
||||
while i < rest.len() {
|
||||
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" => {
|
||||
let sel = rest.first().ok_or_else(|| ParseError::MissingArguments {
|
||||
@@ -3436,4 +3472,85 @@ mod tests {
|
||||
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
@@ -973,14 +973,17 @@ Use Cases:
|
||||
r##"
|
||||
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:
|
||||
direction up, down, left, right (default: down)
|
||||
amount Pixels to scroll (default: 300)
|
||||
|
||||
Options:
|
||||
-s, --selector <sel> CSS selector for a scrollable container
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
@@ -990,6 +993,7 @@ Examples:
|
||||
agent-browser scroll down 500
|
||||
agent-browser scroll up 200
|
||||
agent-browser scroll left 100
|
||||
agent-browser scroll down 500 --selector "div.scroll-container"
|
||||
"##
|
||||
}
|
||||
"scrollintoview" | "scrollinto" => {
|
||||
|
||||
@@ -22,7 +22,7 @@ agent-browser focus <sel> # Focus element
|
||||
agent-browser select <sel> <val> # Select dropdown option
|
||||
agent-browser check <sel> # Check 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 drag <src> <dst> # Drag and drop
|
||||
agent-browser upload <sel> <files> # Upload files
|
||||
|
||||
@@ -67,6 +67,7 @@ agent-browser press Enter # Press key
|
||||
agent-browser keyboard type "text" # Type at current focus (no selector)
|
||||
agent-browser keyboard inserttext "text" # Insert without key events
|
||||
agent-browser scroll down 500 # Scroll page
|
||||
agent-browser scroll down 500 --selector "div.content" # Scroll within a specific container
|
||||
|
||||
# Get information
|
||||
agent-browser get text @e1 # Get element text
|
||||
|
||||
+25
-25
@@ -882,41 +882,41 @@ async function handleWait(command: WaitCommand, browser: BrowserManager): Promis
|
||||
async function handleScroll(command: ScrollCommand, browser: BrowserManager): Promise<Response> {
|
||||
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) {
|
||||
const element = browser.getLocator(command.selector);
|
||||
await element.scrollIntoViewIfNeeded();
|
||||
|
||||
if (command.x !== undefined || command.y !== undefined) {
|
||||
if (hasExplicitDelta || deltaX !== 0 || deltaY !== 0) {
|
||||
await element.evaluate(
|
||||
(el, { x, y }) => {
|
||||
el.scrollBy(x ?? 0, y ?? 0);
|
||||
el.scrollBy(x, y);
|
||||
},
|
||||
{ x: command.x, y: command.y }
|
||||
{ x: deltaX, y: deltaY }
|
||||
);
|
||||
}
|
||||
} 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})`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user