feat: add browser launch --args, --user-agent, --proxy-bypass configuration support. (#35)
* feat: add browser launch args, user-agent, and proxy configuration support * fix: User Agent env need added * fix: command pass error --------- Co-authored-by: Chris Tate <chris@ctate.dev>
This commit is contained in:
@@ -298,6 +298,10 @@ agent-browser snapshot -i -c -d 5 # Combine options
|
|||||||
| `--session <name>` | Use isolated session (or `AGENT_BROWSER_SESSION` env) |
|
| `--session <name>` | Use isolated session (or `AGENT_BROWSER_SESSION` env) |
|
||||||
| `--headers <json>` | Set HTTP headers scoped to the URL's origin |
|
| `--headers <json>` | Set HTTP headers scoped to the URL's origin |
|
||||||
| `--executable-path <path>` | Custom browser executable (or `AGENT_BROWSER_EXECUTABLE_PATH` env) |
|
| `--executable-path <path>` | Custom browser executable (or `AGENT_BROWSER_EXECUTABLE_PATH` env) |
|
||||||
|
| `--args <args>` | Browser launch args, comma or newline separated (or `AGENT_BROWSER_ARGS` env) |
|
||||||
|
| `--user-agent <ua>` | Custom User-Agent string (or `AGENT_BROWSER_USER_AGENT` env) |
|
||||||
|
| `--proxy <url>` | Proxy server URL with optional auth (or `AGENT_BROWSER_PROXY` env) |
|
||||||
|
| `--proxy-bypass <hosts>` | Hosts to bypass proxy (or `AGENT_BROWSER_PROXY_BYPASS` env) |
|
||||||
| `--json` | JSON output (for agents) |
|
| `--json` | JSON output (for agents) |
|
||||||
| `--full, -f` | Full page screenshot |
|
| `--full, -f` | Full page screenshot |
|
||||||
| `--name, -n` | Locator name filter |
|
| `--name, -n` | Locator name filter |
|
||||||
|
|||||||
+144
-66
@@ -216,7 +216,10 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
|||||||
// === Scroll ===
|
// === Scroll ===
|
||||||
"scroll" => {
|
"scroll" => {
|
||||||
let dir = rest.get(0).unwrap_or(&"down");
|
let dir = rest.get(0).unwrap_or(&"down");
|
||||||
let amount = rest.get(1).and_then(|s| s.parse::<i32>().ok()).unwrap_or(300);
|
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 }))
|
Ok(json!({ "id": id, "action": "scroll", "direction": dir, "amount": amount }))
|
||||||
}
|
}
|
||||||
"scrollintoview" | "scrollinto" => {
|
"scrollintoview" | "scrollinto" => {
|
||||||
@@ -231,45 +234,57 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
|||||||
"wait" => {
|
"wait" => {
|
||||||
// Check for --url flag: wait --url "**/dashboard"
|
// Check for --url flag: wait --url "**/dashboard"
|
||||||
if let Some(idx) = rest.iter().position(|&s| s == "--url" || s == "-u") {
|
if let Some(idx) = rest.iter().position(|&s| s == "--url" || s == "-u") {
|
||||||
let url = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments {
|
let url = rest
|
||||||
context: "wait --url".to_string(),
|
.get(idx + 1)
|
||||||
usage: "wait --url <pattern>",
|
.ok_or_else(|| ParseError::MissingArguments {
|
||||||
})?;
|
context: "wait --url".to_string(),
|
||||||
|
usage: "wait --url <pattern>",
|
||||||
|
})?;
|
||||||
return Ok(json!({ "id": id, "action": "waitforurl", "url": url }));
|
return Ok(json!({ "id": id, "action": "waitforurl", "url": url }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for --load flag: wait --load networkidle
|
// Check for --load flag: wait --load networkidle
|
||||||
if let Some(idx) = rest.iter().position(|&s| s == "--load" || s == "-l") {
|
if let Some(idx) = rest.iter().position(|&s| s == "--load" || s == "-l") {
|
||||||
let state = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments {
|
let state = rest
|
||||||
context: "wait --load".to_string(),
|
.get(idx + 1)
|
||||||
usage: "wait --load <state>",
|
.ok_or_else(|| ParseError::MissingArguments {
|
||||||
})?;
|
context: "wait --load".to_string(),
|
||||||
|
usage: "wait --load <state>",
|
||||||
|
})?;
|
||||||
return Ok(json!({ "id": id, "action": "waitforloadstate", "state": state }));
|
return Ok(json!({ "id": id, "action": "waitforloadstate", "state": state }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for --fn flag: wait --fn "window.ready === true"
|
// Check for --fn flag: wait --fn "window.ready === true"
|
||||||
if let Some(idx) = rest.iter().position(|&s| s == "--fn" || s == "-f") {
|
if let Some(idx) = rest.iter().position(|&s| s == "--fn" || s == "-f") {
|
||||||
let expr = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments {
|
let expr = rest
|
||||||
context: "wait --fn".to_string(),
|
.get(idx + 1)
|
||||||
usage: "wait --fn <expression>",
|
.ok_or_else(|| ParseError::MissingArguments {
|
||||||
})?;
|
context: "wait --fn".to_string(),
|
||||||
|
usage: "wait --fn <expression>",
|
||||||
|
})?;
|
||||||
return Ok(json!({ "id": id, "action": "waitforfunction", "expression": expr }));
|
return Ok(json!({ "id": id, "action": "waitforfunction", "expression": expr }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for --text flag: wait --text "Welcome"
|
// Check for --text flag: wait --text "Welcome"
|
||||||
if let Some(idx) = rest.iter().position(|&s| s == "--text" || s == "-t") {
|
if let Some(idx) = rest.iter().position(|&s| s == "--text" || s == "-t") {
|
||||||
let text = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments {
|
let text = rest
|
||||||
context: "wait --text".to_string(),
|
.get(idx + 1)
|
||||||
usage: "wait --text <text>",
|
.ok_or_else(|| ParseError::MissingArguments {
|
||||||
})?;
|
context: "wait --text".to_string(),
|
||||||
|
usage: "wait --text <text>",
|
||||||
|
})?;
|
||||||
// Use getByText locator to wait for text to appear
|
// Use getByText locator to wait for text to appear
|
||||||
return Ok(json!({ "id": id, "action": "wait", "selector": format!("text={}", text) }));
|
return Ok(
|
||||||
|
json!({ "id": id, "action": "wait", "selector": format!("text={}", text) }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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() {
|
||||||
Ok(json!({ "id": id, "action": "wait", "timeout": arg.parse::<u64>().unwrap() }))
|
Ok(
|
||||||
|
json!({ "id": id, "action": "wait", "timeout": arg.parse::<u64>().unwrap() }),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Ok(json!({ "id": id, "action": "wait", "selector": arg }))
|
Ok(json!({ "id": id, "action": "wait", "selector": arg }))
|
||||||
}
|
}
|
||||||
@@ -384,7 +399,9 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
|||||||
context: "cookies set".to_string(),
|
context: "cookies set".to_string(),
|
||||||
usage: "cookies set <name> <value>",
|
usage: "cookies set <name> <value>",
|
||||||
})?;
|
})?;
|
||||||
Ok(json!({ "id": id, "action": "cookies_set", "cookies": [{ "name": name, "value": value }] }))
|
Ok(
|
||||||
|
json!({ "id": id, "action": "cookies_set", "cookies": [{ "name": name, "value": value }] }),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
"clear" => Ok(json!({ "id": id, "action": "cookies_clear" })),
|
"clear" => Ok(json!({ "id": id, "action": "cookies_clear" })),
|
||||||
_ => Ok(json!({ "id": id, "action": "cookies_get" })),
|
_ => Ok(json!({ "id": id, "action": "cookies_get" })),
|
||||||
@@ -414,7 +431,7 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
|||||||
}
|
}
|
||||||
_ => Ok(json!({ "id": id, "action": "tab_list" })),
|
_ => Ok(json!({ "id": id, "action": "tab_list" })),
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
// === Window ===
|
// === Window ===
|
||||||
"window" => {
|
"window" => {
|
||||||
@@ -456,7 +473,6 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
|||||||
}
|
}
|
||||||
Ok(cmd)
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
Some("dismiss") => Ok(json!({ "id": id, "action": "dialog", "response": "dismiss" })),
|
|
||||||
Some(sub) => Err(ParseError::UnknownSubcommand {
|
Some(sub) => Err(ParseError::UnknownSubcommand {
|
||||||
subcommand: sub.to_string(),
|
subcommand: sub.to_string(),
|
||||||
valid_options: VALID,
|
valid_options: VALID,
|
||||||
@@ -702,7 +718,18 @@ fn parse_is(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_find(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
fn parse_find(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||||
const VALID: &[&str] = &["role", "text", "label", "placeholder", "alt", "title", "testid", "first", "last", "nth"];
|
const VALID: &[&str] = &[
|
||||||
|
"role",
|
||||||
|
"text",
|
||||||
|
"label",
|
||||||
|
"placeholder",
|
||||||
|
"alt",
|
||||||
|
"title",
|
||||||
|
"testid",
|
||||||
|
"first",
|
||||||
|
"last",
|
||||||
|
"nth",
|
||||||
|
];
|
||||||
|
|
||||||
let locator = rest.get(0).ok_or_else(|| ParseError::MissingArguments {
|
let locator = rest.get(0).ok_or_else(|| ParseError::MissingArguments {
|
||||||
context: "find".to_string(),
|
context: "find".to_string(),
|
||||||
@@ -714,7 +741,8 @@ fn parse_find(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
let exact = rest.iter().any(|&s| s == "--exact");
|
let exact = rest.iter().any(|&s| s == "--exact");
|
||||||
|
|
||||||
match *locator {
|
match *locator {
|
||||||
"role" | "text" | "label" | "placeholder" | "alt" | "title" | "testid" | "first" | "last" => {
|
"role" | "text" | "label" | "placeholder" | "alt" | "title" | "testid" | "first"
|
||||||
|
| "last" => {
|
||||||
let value = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
let value = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
||||||
context: format!("find {}", locator),
|
context: format!("find {}", locator),
|
||||||
usage: match *locator {
|
usage: match *locator {
|
||||||
@@ -779,10 +807,12 @@ fn parse_find(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
context: "find nth".to_string(),
|
context: "find nth".to_string(),
|
||||||
usage: "find nth <index> <selector> [action] [text]",
|
usage: "find nth <index> <selector> [action] [text]",
|
||||||
})?;
|
})?;
|
||||||
let idx = idx_str.parse::<i32>().map_err(|_| ParseError::MissingArguments {
|
let idx = idx_str
|
||||||
context: "find nth".to_string(),
|
.parse::<i32>()
|
||||||
usage: "find nth <index> <selector> [action] [text]",
|
.map_err(|_| ParseError::MissingArguments {
|
||||||
})?;
|
context: "find nth".to_string(),
|
||||||
|
usage: "find nth <index> <selector> [action] [text]",
|
||||||
|
})?;
|
||||||
let sel = rest.get(2).ok_or_else(|| ParseError::MissingArguments {
|
let sel = rest.get(2).ok_or_else(|| ParseError::MissingArguments {
|
||||||
context: "find nth".to_string(),
|
context: "find nth".to_string(),
|
||||||
usage: "find nth <index> <selector> [action] [text]",
|
usage: "find nth <index> <selector> [action] [text]",
|
||||||
@@ -817,14 +847,18 @@ fn parse_mouse(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
context: "mouse move".to_string(),
|
context: "mouse move".to_string(),
|
||||||
usage: "mouse move <x> <y>",
|
usage: "mouse move <x> <y>",
|
||||||
})?;
|
})?;
|
||||||
let x = x_str.parse::<i32>().map_err(|_| ParseError::MissingArguments {
|
let x = x_str
|
||||||
context: "mouse move".to_string(),
|
.parse::<i32>()
|
||||||
usage: "mouse move <x> <y>",
|
.map_err(|_| ParseError::MissingArguments {
|
||||||
})?;
|
context: "mouse move".to_string(),
|
||||||
let y = y_str.parse::<i32>().map_err(|_| ParseError::MissingArguments {
|
usage: "mouse move <x> <y>",
|
||||||
context: "mouse move".to_string(),
|
})?;
|
||||||
usage: "mouse move <x> <y>",
|
let y = y_str
|
||||||
})?;
|
.parse::<i32>()
|
||||||
|
.map_err(|_| ParseError::MissingArguments {
|
||||||
|
context: "mouse move".to_string(),
|
||||||
|
usage: "mouse move <x> <y>",
|
||||||
|
})?;
|
||||||
Ok(json!({ "id": id, "action": "mousemove", "x": x, "y": y }))
|
Ok(json!({ "id": id, "action": "mousemove", "x": x, "y": y }))
|
||||||
}
|
}
|
||||||
Some("down") => {
|
Some("down") => {
|
||||||
@@ -834,7 +868,10 @@ fn parse_mouse(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
Ok(json!({ "id": id, "action": "mouseup", "button": rest.get(1).unwrap_or(&"left") }))
|
Ok(json!({ "id": id, "action": "mouseup", "button": rest.get(1).unwrap_or(&"left") }))
|
||||||
}
|
}
|
||||||
Some("wheel") => {
|
Some("wheel") => {
|
||||||
let dy = rest.get(1).and_then(|s| s.parse::<i32>().ok()).unwrap_or(100);
|
let dy = rest
|
||||||
|
.get(1)
|
||||||
|
.and_then(|s| s.parse::<i32>().ok())
|
||||||
|
.unwrap_or(100);
|
||||||
let dx = rest.get(2).and_then(|s| s.parse::<i32>().ok()).unwrap_or(0);
|
let dx = rest.get(2).and_then(|s| s.parse::<i32>().ok()).unwrap_or(0);
|
||||||
Ok(json!({ "id": id, "action": "wheel", "deltaX": dx, "deltaY": dy }))
|
Ok(json!({ "id": id, "action": "wheel", "deltaX": dx, "deltaY": dy }))
|
||||||
}
|
}
|
||||||
@@ -850,7 +887,17 @@ fn parse_mouse(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||||
const VALID: &[&str] = &["viewport", "device", "geo", "geolocation", "offline", "headers", "credentials", "auth", "media"];
|
const VALID: &[&str] = &[
|
||||||
|
"viewport",
|
||||||
|
"device",
|
||||||
|
"geo",
|
||||||
|
"geolocation",
|
||||||
|
"offline",
|
||||||
|
"headers",
|
||||||
|
"credentials",
|
||||||
|
"auth",
|
||||||
|
"media",
|
||||||
|
];
|
||||||
|
|
||||||
match rest.get(0).map(|s| *s) {
|
match rest.get(0).map(|s| *s) {
|
||||||
Some("viewport") => {
|
Some("viewport") => {
|
||||||
@@ -862,14 +909,18 @@ fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
context: "set viewport".to_string(),
|
context: "set viewport".to_string(),
|
||||||
usage: "set viewport <width> <height>",
|
usage: "set viewport <width> <height>",
|
||||||
})?;
|
})?;
|
||||||
let w = w_str.parse::<i32>().map_err(|_| ParseError::MissingArguments {
|
let w = w_str
|
||||||
context: "set viewport".to_string(),
|
.parse::<i32>()
|
||||||
usage: "set viewport <width> <height>",
|
.map_err(|_| ParseError::MissingArguments {
|
||||||
})?;
|
context: "set viewport".to_string(),
|
||||||
let h = h_str.parse::<i32>().map_err(|_| ParseError::MissingArguments {
|
usage: "set viewport <width> <height>",
|
||||||
context: "set viewport".to_string(),
|
})?;
|
||||||
usage: "set viewport <width> <height>",
|
let h = h_str
|
||||||
})?;
|
.parse::<i32>()
|
||||||
|
.map_err(|_| ParseError::MissingArguments {
|
||||||
|
context: "set viewport".to_string(),
|
||||||
|
usage: "set viewport <width> <height>",
|
||||||
|
})?;
|
||||||
Ok(json!({ "id": id, "action": "viewport", "width": w, "height": h }))
|
Ok(json!({ "id": id, "action": "viewport", "width": w, "height": h }))
|
||||||
}
|
}
|
||||||
Some("device") => {
|
Some("device") => {
|
||||||
@@ -888,18 +939,25 @@ fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
context: "set geo".to_string(),
|
context: "set geo".to_string(),
|
||||||
usage: "set geo <latitude> <longitude>",
|
usage: "set geo <latitude> <longitude>",
|
||||||
})?;
|
})?;
|
||||||
let lat = lat_str.parse::<f64>().map_err(|_| ParseError::MissingArguments {
|
let lat = lat_str
|
||||||
context: "set geo".to_string(),
|
.parse::<f64>()
|
||||||
usage: "set geo <latitude> <longitude>",
|
.map_err(|_| ParseError::MissingArguments {
|
||||||
})?;
|
context: "set geo".to_string(),
|
||||||
let lng = lng_str.parse::<f64>().map_err(|_| ParseError::MissingArguments {
|
usage: "set geo <latitude> <longitude>",
|
||||||
context: "set geo".to_string(),
|
})?;
|
||||||
usage: "set geo <latitude> <longitude>",
|
let lng = lng_str
|
||||||
})?;
|
.parse::<f64>()
|
||||||
|
.map_err(|_| ParseError::MissingArguments {
|
||||||
|
context: "set geo".to_string(),
|
||||||
|
usage: "set geo <latitude> <longitude>",
|
||||||
|
})?;
|
||||||
Ok(json!({ "id": id, "action": "geolocation", "latitude": lat, "longitude": lng }))
|
Ok(json!({ "id": id, "action": "geolocation", "latitude": lat, "longitude": lng }))
|
||||||
}
|
}
|
||||||
Some("offline") => {
|
Some("offline") => {
|
||||||
let off = rest.get(1).map(|s| *s != "off" && *s != "false").unwrap_or(true);
|
let off = rest
|
||||||
|
.get(1)
|
||||||
|
.map(|s| *s != "off" && *s != "false")
|
||||||
|
.unwrap_or(true);
|
||||||
Ok(json!({ "id": id, "action": "offline", "offline": off }))
|
Ok(json!({ "id": id, "action": "offline", "offline": off }))
|
||||||
}
|
}
|
||||||
Some("headers") => {
|
Some("headers") => {
|
||||||
@@ -908,8 +966,8 @@ fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
usage: "set headers <json>",
|
usage: "set headers <json>",
|
||||||
})?;
|
})?;
|
||||||
// Parse the JSON string into an object
|
// Parse the JSON string into an object
|
||||||
let headers: serde_json::Value = serde_json::from_str(headers_json)
|
let headers: serde_json::Value =
|
||||||
.map_err(|_| ParseError::MissingArguments {
|
serde_json::from_str(headers_json).map_err(|_| ParseError::MissingArguments {
|
||||||
context: "set headers".to_string(),
|
context: "set headers".to_string(),
|
||||||
usage: "set headers <json> (must be valid JSON object)",
|
usage: "set headers <json> (must be valid JSON object)",
|
||||||
})?;
|
})?;
|
||||||
@@ -1013,13 +1071,18 @@ fn parse_storage(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
context: format!("storage {} set", storage_type),
|
context: format!("storage {} set", storage_type),
|
||||||
usage: "storage <local|session> set <key> <value>",
|
usage: "storage <local|session> set <key> <value>",
|
||||||
})?;
|
})?;
|
||||||
Ok(json!({ "id": id, "action": "storage_set", "type": storage_type, "key": k, "value": v }))
|
Ok(
|
||||||
|
json!({ "id": id, "action": "storage_set", "type": storage_type, "key": k, "value": v }),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
"clear" => Ok(json!({ "id": id, "action": "storage_clear", "type": storage_type })),
|
"clear" => Ok(json!({ "id": id, "action": "storage_clear", "type": storage_type })),
|
||||||
_ => {
|
_ => {
|
||||||
let mut cmd = json!({ "id": id, "action": "storage_get", "type": storage_type });
|
let mut cmd =
|
||||||
|
json!({ "id": id, "action": "storage_get", "type": storage_type });
|
||||||
if let Some(k) = key {
|
if let Some(k) = key {
|
||||||
cmd.as_object_mut().unwrap().insert("key".to_string(), json!(k));
|
cmd.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.insert("key".to_string(), json!(k));
|
||||||
}
|
}
|
||||||
Ok(cmd)
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
@@ -1052,6 +1115,9 @@ mod tests {
|
|||||||
extensions: Vec::new(),
|
extensions: Vec::new(),
|
||||||
cdp: None,
|
cdp: None,
|
||||||
proxy: None,
|
proxy: None,
|
||||||
|
proxy_bypass: None,
|
||||||
|
args: None,
|
||||||
|
user_agent: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1120,7 +1186,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_storage_local_set() {
|
fn test_storage_local_set() {
|
||||||
let cmd = parse_command(&args("storage local set mykey myvalue"), &default_flags()).unwrap();
|
let cmd =
|
||||||
|
parse_command(&args("storage local set mykey myvalue"), &default_flags()).unwrap();
|
||||||
assert_eq!(cmd["action"], "storage_set");
|
assert_eq!(cmd["action"], "storage_set");
|
||||||
assert_eq!(cmd["type"], "local");
|
assert_eq!(cmd["type"], "local");
|
||||||
assert_eq!(cmd["key"], "mykey");
|
assert_eq!(cmd["key"], "mykey");
|
||||||
@@ -1129,7 +1196,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_storage_session_set() {
|
fn test_storage_session_set() {
|
||||||
let cmd = parse_command(&args("storage session set skey svalue"), &default_flags()).unwrap();
|
let cmd =
|
||||||
|
parse_command(&args("storage session set skey svalue"), &default_flags()).unwrap();
|
||||||
assert_eq!(cmd["action"], "storage_set");
|
assert_eq!(cmd["action"], "storage_set");
|
||||||
assert_eq!(cmd["type"], "session");
|
assert_eq!(cmd["type"], "session");
|
||||||
assert_eq!(cmd["key"], "skey");
|
assert_eq!(cmd["key"], "skey");
|
||||||
@@ -1191,7 +1259,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_navigate_with_multiple_headers() {
|
fn test_navigate_with_multiple_headers() {
|
||||||
let mut flags = default_flags();
|
let mut flags = default_flags();
|
||||||
flags.headers = Some(r#"{"Authorization": "Bearer token", "X-Custom": "value"}"#.to_string());
|
flags.headers =
|
||||||
|
Some(r#"{"Authorization": "Bearer token", "X-Custom": "value"}"#.to_string());
|
||||||
let cmd = parse_command(&args("open api.example.com"), &flags).unwrap();
|
let cmd = parse_command(&args("open api.example.com"), &flags).unwrap();
|
||||||
assert_eq!(cmd["headers"]["Authorization"], "Bearer token");
|
assert_eq!(cmd["headers"]["Authorization"], "Bearer token");
|
||||||
assert_eq!(cmd["headers"]["X-Custom"], "value");
|
assert_eq!(cmd["headers"]["X-Custom"], "value");
|
||||||
@@ -1445,7 +1514,10 @@ mod tests {
|
|||||||
fn test_wait_load_missing_state() {
|
fn test_wait_load_missing_state() {
|
||||||
let result = parse_command(&args("wait --load"), &default_flags());
|
let result = parse_command(&args("wait --load"), &default_flags());
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. }));
|
assert!(matches!(
|
||||||
|
result.unwrap_err(),
|
||||||
|
ParseError::MissingArguments { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1544,14 +1616,20 @@ mod tests {
|
|||||||
fn test_unknown_command() {
|
fn test_unknown_command() {
|
||||||
let result = parse_command(&args("unknowncommand"), &default_flags());
|
let result = parse_command(&args("unknowncommand"), &default_flags());
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert!(matches!(result.unwrap_err(), ParseError::UnknownCommand { .. }));
|
assert!(matches!(
|
||||||
|
result.unwrap_err(),
|
||||||
|
ParseError::UnknownCommand { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_empty_args() {
|
fn test_empty_args() {
|
||||||
let result = parse_command(&[], &default_flags());
|
let result = parse_command(&[], &default_flags());
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. }));
|
assert!(matches!(
|
||||||
|
result.unwrap_err(),
|
||||||
|
ParseError::MissingArguments { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Error message tests ===
|
// === Error message tests ===
|
||||||
|
|||||||
+39
-1
@@ -167,6 +167,10 @@ pub fn ensure_daemon(
|
|||||||
headed: bool,
|
headed: bool,
|
||||||
executable_path: Option<&str>,
|
executable_path: Option<&str>,
|
||||||
extensions: &[String],
|
extensions: &[String],
|
||||||
|
args: Option<&str>,
|
||||||
|
user_agent: Option<&str>,
|
||||||
|
proxy: Option<&str>,
|
||||||
|
proxy_bypass: Option<&str>,
|
||||||
) -> Result<DaemonResult, String> {
|
) -> Result<DaemonResult, String> {
|
||||||
if is_daemon_running(session) && daemon_ready(session) {
|
if is_daemon_running(session) && daemon_ready(session) {
|
||||||
return Ok(DaemonResult {
|
return Ok(DaemonResult {
|
||||||
@@ -217,6 +221,22 @@ pub fn ensure_daemon(
|
|||||||
cmd.env("AGENT_BROWSER_EXTENSIONS", extensions.join(","));
|
cmd.env("AGENT_BROWSER_EXTENSIONS", extensions.join(","));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(a) = args {
|
||||||
|
cmd.env("AGENT_BROWSER_ARGS", a);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ua) = user_agent {
|
||||||
|
cmd.env("AGENT_BROWSER_USER_AGENT", ua);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(p) = proxy {
|
||||||
|
cmd.env("AGENT_BROWSER_PROXY", p);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pb) = proxy_bypass {
|
||||||
|
cmd.env("AGENT_BROWSER_PROXY_BYPASS", pb);
|
||||||
|
}
|
||||||
|
|
||||||
// Create new process group and session to fully detach
|
// Create new process group and session to fully detach
|
||||||
unsafe {
|
unsafe {
|
||||||
cmd.pre_exec(|| {
|
cmd.pre_exec(|| {
|
||||||
@@ -256,6 +276,22 @@ pub fn ensure_daemon(
|
|||||||
cmd.env("AGENT_BROWSER_EXTENSIONS", extensions.join(","));
|
cmd.env("AGENT_BROWSER_EXTENSIONS", extensions.join(","));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(a) = args {
|
||||||
|
cmd.env("AGENT_BROWSER_ARGS", a);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ua) = user_agent {
|
||||||
|
cmd.env("AGENT_BROWSER_USER_AGENT", ua);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(p) = proxy {
|
||||||
|
cmd.env("AGENT_BROWSER_PROXY", p);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pb) = proxy_bypass {
|
||||||
|
cmd.env("AGENT_BROWSER_PROXY_BYPASS", pb);
|
||||||
|
}
|
||||||
|
|
||||||
// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
|
// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
|
||||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
||||||
const DETACHED_PROCESS: u32 = 0x00000008;
|
const DETACHED_PROCESS: u32 = 0x00000008;
|
||||||
@@ -270,7 +306,9 @@ pub fn ensure_daemon(
|
|||||||
|
|
||||||
for _ in 0..50 {
|
for _ in 0..50 {
|
||||||
if daemon_ready(session) {
|
if daemon_ready(session) {
|
||||||
return Ok(DaemonResult { already_running: false });
|
return Ok(DaemonResult {
|
||||||
|
already_running: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(100));
|
thread::sleep(Duration::from_millis(100));
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-7
@@ -11,6 +11,9 @@ pub struct Flags {
|
|||||||
pub cdp: Option<String>,
|
pub cdp: Option<String>,
|
||||||
pub extensions: Vec<String>,
|
pub extensions: Vec<String>,
|
||||||
pub proxy: Option<String>,
|
pub proxy: Option<String>,
|
||||||
|
pub proxy_bypass: Option<String>,
|
||||||
|
pub args: Option<String>,
|
||||||
|
pub user_agent: Option<String>,
|
||||||
pub provider: Option<String>,
|
pub provider: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +33,10 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok(),
|
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok(),
|
||||||
cdp: None,
|
cdp: None,
|
||||||
extensions: extensions_env,
|
extensions: extensions_env,
|
||||||
proxy: None,
|
proxy: env::var("AGENT_BROWSER_PROXY").ok(),
|
||||||
|
proxy_bypass: env::var("AGENT_BROWSER_PROXY_BYPASS").ok(),
|
||||||
|
args: env::var("AGENT_BROWSER_ARGS").ok(),
|
||||||
|
user_agent: env::var("AGENT_BROWSER_USER_AGENT").ok(),
|
||||||
provider: env::var("AGENT_BROWSER_PROVIDER").ok(),
|
provider: env::var("AGENT_BROWSER_PROVIDER").ok(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,6 +83,24 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"--proxy-bypass" => {
|
||||||
|
if let Some(s) = args.get(i + 1) {
|
||||||
|
flags.proxy_bypass = Some(s.clone());
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"--args" => {
|
||||||
|
if let Some(s) = args.get(i + 1) {
|
||||||
|
flags.args = Some(s.clone());
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"--user-agent" => {
|
||||||
|
if let Some(s) = args.get(i + 1) {
|
||||||
|
flags.user_agent = Some(s.clone());
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
"-p" | "--provider" => {
|
"-p" | "--provider" => {
|
||||||
if let Some(p) = args.get(i + 1) {
|
if let Some(p) = args.get(i + 1) {
|
||||||
flags.provider = Some(p.clone());
|
flags.provider = Some(p.clone());
|
||||||
@@ -97,7 +121,19 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
|||||||
// Global flags that should be stripped from command args
|
// Global flags that should be stripped from command args
|
||||||
const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug"];
|
const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug"];
|
||||||
// Global flags that take a value (need to skip the next arg too)
|
// Global flags that take a value (need to skip the next arg too)
|
||||||
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &["--session", "--headers", "--executable-path", "--cdp", "--extension", "--proxy", "-p", "--provider"];
|
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[
|
||||||
|
"--session",
|
||||||
|
"--headers",
|
||||||
|
"--executable-path",
|
||||||
|
"--cdp",
|
||||||
|
"--extension",
|
||||||
|
"--proxy",
|
||||||
|
"--proxy-bypass",
|
||||||
|
"--args",
|
||||||
|
"--user-agent",
|
||||||
|
"-p",
|
||||||
|
"--provider",
|
||||||
|
];
|
||||||
|
|
||||||
for arg in args.iter() {
|
for arg in args.iter() {
|
||||||
if skip_next {
|
if skip_next {
|
||||||
@@ -141,7 +177,10 @@ mod tests {
|
|||||||
r#"{"Authorization": "Bearer token"}"#.to_string(),
|
r#"{"Authorization": "Bearer token"}"#.to_string(),
|
||||||
];
|
];
|
||||||
let flags = parse_flags(&input);
|
let flags = parse_flags(&input);
|
||||||
assert_eq!(flags.headers, Some(r#"{"Authorization": "Bearer token"}"#.to_string()));
|
assert_eq!(
|
||||||
|
flags.headers,
|
||||||
|
Some(r#"{"Authorization": "Bearer token"}"#.to_string())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -195,7 +234,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_executable_path_flag() {
|
fn test_parse_executable_path_flag() {
|
||||||
let flags = parse_flags(&args("--executable-path /path/to/chromium open example.com"));
|
let flags = parse_flags(&args(
|
||||||
|
"--executable-path /path/to/chromium open example.com",
|
||||||
|
));
|
||||||
assert_eq!(flags.executable_path, Some("/path/to/chromium".to_string()));
|
assert_eq!(flags.executable_path, Some("/path/to/chromium".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,19 +248,25 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_clean_args_removes_executable_path() {
|
fn test_clean_args_removes_executable_path() {
|
||||||
let cleaned = clean_args(&args("--executable-path /path/to/chromium open example.com"));
|
let cleaned = clean_args(&args(
|
||||||
|
"--executable-path /path/to/chromium open example.com",
|
||||||
|
));
|
||||||
assert_eq!(cleaned, vec!["open", "example.com"]);
|
assert_eq!(cleaned, vec!["open", "example.com"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_clean_args_removes_executable_path_with_other_flags() {
|
fn test_clean_args_removes_executable_path_with_other_flags() {
|
||||||
let cleaned = clean_args(&args("--json --executable-path /path/to/chromium --headed open example.com"));
|
let cleaned = clean_args(&args(
|
||||||
|
"--json --executable-path /path/to/chromium --headed open example.com",
|
||||||
|
));
|
||||||
assert_eq!(cleaned, vec!["open", "example.com"]);
|
assert_eq!(cleaned, vec!["open", "example.com"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_flags_with_session_and_executable_path() {
|
fn test_parse_flags_with_session_and_executable_path() {
|
||||||
let flags = parse_flags(&args("--session test --executable-path /custom/chrome open example.com"));
|
let flags = parse_flags(&args(
|
||||||
|
"--session test --executable-path /custom/chrome open example.com",
|
||||||
|
));
|
||||||
assert_eq!(flags.session, "test");
|
assert_eq!(flags.session, "test");
|
||||||
assert_eq!(flags.executable_path, Some("/custom/chrome".to_string()));
|
assert_eq!(flags.executable_path, Some("/custom/chrome".to_string()));
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-16
@@ -198,6 +198,10 @@ fn main() {
|
|||||||
flags.headed,
|
flags.headed,
|
||||||
flags.executable_path.as_deref(),
|
flags.executable_path.as_deref(),
|
||||||
&flags.extensions,
|
&flags.extensions,
|
||||||
|
flags.args.as_deref(),
|
||||||
|
flags.user_agent.as_deref(),
|
||||||
|
flags.proxy.as_deref(),
|
||||||
|
flags.proxy_bypass.as_deref(),
|
||||||
) {
|
) {
|
||||||
Ok(result) => result,
|
Ok(result) => result,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -210,17 +214,27 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Warn if executable_path was specified but daemon was already running
|
// Warn if launch-time options were specified but daemon was already running
|
||||||
if daemon_result.already_running
|
if daemon_result.already_running {
|
||||||
&& (flags.executable_path.is_some() || !flags.extensions.is_empty())
|
let has_extensions = !flags.extensions.is_empty();
|
||||||
{
|
let ignored_flags: Vec<&str> = [
|
||||||
if !flags.json {
|
flags.executable_path.as_ref().map(|_| "--executable-path"),
|
||||||
if flags.executable_path.is_some() {
|
if has_extensions { Some("--extension") } else { None },
|
||||||
eprintln!("{} --executable-path ignored: daemon already running. Use 'agent-browser close' first to restart with new path.", color::warning_indicator());
|
flags.args.as_ref().map(|_| "--args"),
|
||||||
}
|
flags.user_agent.as_ref().map(|_| "--user-agent"),
|
||||||
if !flags.extensions.is_empty() {
|
flags.proxy.as_ref().map(|_| "--proxy"),
|
||||||
eprintln!("{} --extension ignored: daemon already running. Use 'agent-browser close' first to restart with extensions.", color::warning_indicator());
|
flags.proxy_bypass.as_ref().map(|_| "--proxy-bypass"),
|
||||||
}
|
]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !ignored_flags.is_empty() && !flags.json {
|
||||||
|
eprintln!(
|
||||||
|
"{} {} ignored: daemon already running. Use 'agent-browser close' first to restart with new options.",
|
||||||
|
color::warning_indicator(),
|
||||||
|
ignored_flags.join(", ")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,18 +362,39 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Launch headed browser or proxy if flags are set (without CDP or provider)
|
// Launch headed browser or proxy if flags are set (without CDP or provider)
|
||||||
if (flags.headed || flags.proxy.is_some()) && flags.cdp.is_none() && flags.provider.is_none() {
|
if (flags.headed || flags.proxy.is_some() || flags.args.is_some() || flags.user_agent.is_some()) && flags.cdp.is_none() && flags.provider.is_none() {
|
||||||
let mut launch_cmd = json!({
|
let mut launch_cmd = json!({
|
||||||
"id": gen_id(),
|
"id": gen_id(),
|
||||||
"action": "launch",
|
"action": "launch",
|
||||||
"headless": !flags.headed
|
"headless": !flags.headed
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let cmd_obj = launch_cmd.as_object_mut()
|
||||||
|
.expect("json! macro guarantees object type");
|
||||||
|
|
||||||
if let Some(ref proxy_str) = flags.proxy {
|
if let Some(ref proxy_str) = flags.proxy {
|
||||||
let proxy_obj = parse_proxy(proxy_str);
|
let mut proxy_obj = parse_proxy(proxy_str);
|
||||||
launch_cmd.as_object_mut()
|
// Add bypass if specified
|
||||||
.expect("json! macro guarantees object type")
|
if let Some(ref bypass) = flags.proxy_bypass {
|
||||||
.insert("proxy".to_string(), proxy_obj);
|
if let Some(obj) = proxy_obj.as_object_mut() {
|
||||||
|
obj.insert("bypass".to_string(), json!(bypass));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd_obj.insert("proxy".to_string(), proxy_obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref ua) = flags.user_agent {
|
||||||
|
cmd_obj.insert("userAgent".to_string(), json!(ua));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref a) = flags.args {
|
||||||
|
// Parse args (comma or newline separated)
|
||||||
|
let args_vec: Vec<String> = a
|
||||||
|
.split(&[',', '\n'][..])
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect();
|
||||||
|
cmd_obj.insert("args".to_string(), json!(args_vec));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = send_command(launch_cmd, &flags.session) {
|
if let Err(e) = send_command(launch_cmd, &flags.session) {
|
||||||
|
|||||||
+188
-92
@@ -239,7 +239,8 @@ pub fn print_response(resp: &Response, json_mode: bool) {
|
|||||||
pub fn print_command_help(command: &str) -> bool {
|
pub fn print_command_help(command: &str) -> bool {
|
||||||
let help = match command {
|
let help = match command {
|
||||||
// === Navigation ===
|
// === Navigation ===
|
||||||
"open" | "goto" | "navigate" => r##"
|
"open" | "goto" | "navigate" => {
|
||||||
|
r##"
|
||||||
agent-browser open - Navigate to a URL
|
agent-browser open - Navigate to a URL
|
||||||
|
|
||||||
Usage: agent-browser open <url>
|
Usage: agent-browser open <url>
|
||||||
@@ -261,8 +262,10 @@ Examples:
|
|||||||
agent-browser open localhost:3000
|
agent-browser open localhost:3000
|
||||||
agent-browser open api.example.com --headers '{"Authorization": "Bearer token"}'
|
agent-browser open api.example.com --headers '{"Authorization": "Bearer token"}'
|
||||||
# ^ Headers only sent to api.example.com, not other domains
|
# ^ Headers only sent to api.example.com, not other domains
|
||||||
"##,
|
"##
|
||||||
"back" => r##"
|
}
|
||||||
|
"back" => {
|
||||||
|
r##"
|
||||||
agent-browser back - Navigate back in history
|
agent-browser back - Navigate back in history
|
||||||
|
|
||||||
Usage: agent-browser back
|
Usage: agent-browser back
|
||||||
@@ -276,8 +279,10 @@ Global Options:
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser back
|
agent-browser back
|
||||||
"##,
|
"##
|
||||||
"forward" => r##"
|
}
|
||||||
|
"forward" => {
|
||||||
|
r##"
|
||||||
agent-browser forward - Navigate forward in history
|
agent-browser forward - Navigate forward in history
|
||||||
|
|
||||||
Usage: agent-browser forward
|
Usage: agent-browser forward
|
||||||
@@ -291,8 +296,10 @@ Global Options:
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser forward
|
agent-browser forward
|
||||||
"##,
|
"##
|
||||||
"reload" => r##"
|
}
|
||||||
|
"reload" => {
|
||||||
|
r##"
|
||||||
agent-browser reload - Reload the current page
|
agent-browser reload - Reload the current page
|
||||||
|
|
||||||
Usage: agent-browser reload
|
Usage: agent-browser reload
|
||||||
@@ -306,10 +313,12 @@ Global Options:
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser reload
|
agent-browser reload
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Core Actions ===
|
// === Core Actions ===
|
||||||
"click" => r##"
|
"click" => {
|
||||||
|
r##"
|
||||||
agent-browser click - Click an element
|
agent-browser click - Click an element
|
||||||
|
|
||||||
Usage: agent-browser click <selector>
|
Usage: agent-browser click <selector>
|
||||||
@@ -326,8 +335,10 @@ Examples:
|
|||||||
agent-browser click @e1
|
agent-browser click @e1
|
||||||
agent-browser click "button.primary"
|
agent-browser click "button.primary"
|
||||||
agent-browser click "//button[@type='submit']"
|
agent-browser click "//button[@type='submit']"
|
||||||
"##,
|
"##
|
||||||
"dblclick" => r##"
|
}
|
||||||
|
"dblclick" => {
|
||||||
|
r##"
|
||||||
agent-browser dblclick - Double-click an element
|
agent-browser dblclick - Double-click an element
|
||||||
|
|
||||||
Usage: agent-browser dblclick <selector>
|
Usage: agent-browser dblclick <selector>
|
||||||
@@ -342,8 +353,10 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser dblclick "#editable-text"
|
agent-browser dblclick "#editable-text"
|
||||||
agent-browser dblclick @e5
|
agent-browser dblclick @e5
|
||||||
"##,
|
"##
|
||||||
"fill" => r##"
|
}
|
||||||
|
"fill" => {
|
||||||
|
r##"
|
||||||
agent-browser fill - Clear and fill an input field
|
agent-browser fill - Clear and fill an input field
|
||||||
|
|
||||||
Usage: agent-browser fill <selector> <text>
|
Usage: agent-browser fill <selector> <text>
|
||||||
@@ -359,8 +372,10 @@ Examples:
|
|||||||
agent-browser fill "#email" "user@example.com"
|
agent-browser fill "#email" "user@example.com"
|
||||||
agent-browser fill @e3 "Hello World"
|
agent-browser fill @e3 "Hello World"
|
||||||
agent-browser fill "input[name='search']" "query"
|
agent-browser fill "input[name='search']" "query"
|
||||||
"##,
|
"##
|
||||||
"type" => r##"
|
}
|
||||||
|
"type" => {
|
||||||
|
r##"
|
||||||
agent-browser type - Type text into an element
|
agent-browser type - Type text into an element
|
||||||
|
|
||||||
Usage: agent-browser type <selector> <text>
|
Usage: agent-browser type <selector> <text>
|
||||||
@@ -375,8 +390,10 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser type "#search" "hello"
|
agent-browser type "#search" "hello"
|
||||||
agent-browser type @e2 "additional text"
|
agent-browser type @e2 "additional text"
|
||||||
"##,
|
"##
|
||||||
"hover" => r##"
|
}
|
||||||
|
"hover" => {
|
||||||
|
r##"
|
||||||
agent-browser hover - Hover over an element
|
agent-browser hover - Hover over an element
|
||||||
|
|
||||||
Usage: agent-browser hover <selector>
|
Usage: agent-browser hover <selector>
|
||||||
@@ -391,8 +408,10 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser hover "#dropdown-trigger"
|
agent-browser hover "#dropdown-trigger"
|
||||||
agent-browser hover @e4
|
agent-browser hover @e4
|
||||||
"##,
|
"##
|
||||||
"focus" => r##"
|
}
|
||||||
|
"focus" => {
|
||||||
|
r##"
|
||||||
agent-browser focus - Focus an element
|
agent-browser focus - Focus an element
|
||||||
|
|
||||||
Usage: agent-browser focus <selector>
|
Usage: agent-browser focus <selector>
|
||||||
@@ -406,8 +425,10 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser focus "#input-field"
|
agent-browser focus "#input-field"
|
||||||
agent-browser focus @e2
|
agent-browser focus @e2
|
||||||
"##,
|
"##
|
||||||
"check" => r##"
|
}
|
||||||
|
"check" => {
|
||||||
|
r##"
|
||||||
agent-browser check - Check a checkbox
|
agent-browser check - Check a checkbox
|
||||||
|
|
||||||
Usage: agent-browser check <selector>
|
Usage: agent-browser check <selector>
|
||||||
@@ -421,8 +442,10 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser check "#terms-checkbox"
|
agent-browser check "#terms-checkbox"
|
||||||
agent-browser check @e7
|
agent-browser check @e7
|
||||||
"##,
|
"##
|
||||||
"uncheck" => r##"
|
}
|
||||||
|
"uncheck" => {
|
||||||
|
r##"
|
||||||
agent-browser uncheck - Uncheck a checkbox
|
agent-browser uncheck - Uncheck a checkbox
|
||||||
|
|
||||||
Usage: agent-browser uncheck <selector>
|
Usage: agent-browser uncheck <selector>
|
||||||
@@ -436,8 +459,10 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser uncheck "#newsletter-opt-in"
|
agent-browser uncheck "#newsletter-opt-in"
|
||||||
agent-browser uncheck @e8
|
agent-browser uncheck @e8
|
||||||
"##,
|
"##
|
||||||
"select" => r##"
|
}
|
||||||
|
"select" => {
|
||||||
|
r##"
|
||||||
agent-browser select - Select a dropdown option
|
agent-browser select - Select a dropdown option
|
||||||
|
|
||||||
Usage: agent-browser select <selector> <value...>
|
Usage: agent-browser select <selector> <value...>
|
||||||
@@ -452,8 +477,10 @@ Examples:
|
|||||||
agent-browser select "#country" "US"
|
agent-browser select "#country" "US"
|
||||||
agent-browser select @e5 "option2"
|
agent-browser select @e5 "option2"
|
||||||
agent-browser select "#menu" "opt1" "opt2" "opt3"
|
agent-browser select "#menu" "opt1" "opt2" "opt3"
|
||||||
"##,
|
"##
|
||||||
"drag" => r##"
|
}
|
||||||
|
"drag" => {
|
||||||
|
r##"
|
||||||
agent-browser drag - Drag and drop
|
agent-browser drag - Drag and drop
|
||||||
|
|
||||||
Usage: agent-browser drag <source> <target>
|
Usage: agent-browser drag <source> <target>
|
||||||
@@ -467,8 +494,10 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser drag "#draggable" "#drop-zone"
|
agent-browser drag "#draggable" "#drop-zone"
|
||||||
agent-browser drag @e1 @e2
|
agent-browser drag @e1 @e2
|
||||||
"##,
|
"##
|
||||||
"upload" => r##"
|
}
|
||||||
|
"upload" => {
|
||||||
|
r##"
|
||||||
agent-browser upload - Upload files
|
agent-browser upload - Upload files
|
||||||
|
|
||||||
Usage: agent-browser upload <selector> <files...>
|
Usage: agent-browser upload <selector> <files...>
|
||||||
@@ -482,10 +511,12 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser upload "#file-input" ./document.pdf
|
agent-browser upload "#file-input" ./document.pdf
|
||||||
agent-browser upload @e3 ./image1.png ./image2.png
|
agent-browser upload @e3 ./image1.png ./image2.png
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Keyboard ===
|
// === Keyboard ===
|
||||||
"press" | "key" => r##"
|
"press" | "key" => {
|
||||||
|
r##"
|
||||||
agent-browser press - Press a key or key combination
|
agent-browser press - Press a key or key combination
|
||||||
|
|
||||||
Usage: agent-browser press <key>
|
Usage: agent-browser press <key>
|
||||||
@@ -513,8 +544,10 @@ Examples:
|
|||||||
agent-browser press Control+a
|
agent-browser press Control+a
|
||||||
agent-browser press Control+Shift+s
|
agent-browser press Control+Shift+s
|
||||||
agent-browser press Escape
|
agent-browser press Escape
|
||||||
"##,
|
"##
|
||||||
"keydown" => r##"
|
}
|
||||||
|
"keydown" => {
|
||||||
|
r##"
|
||||||
agent-browser keydown - Press a key down (without release)
|
agent-browser keydown - Press a key down (without release)
|
||||||
|
|
||||||
Usage: agent-browser keydown <key>
|
Usage: agent-browser keydown <key>
|
||||||
@@ -529,8 +562,10 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser keydown Shift
|
agent-browser keydown Shift
|
||||||
agent-browser keydown Control
|
agent-browser keydown Control
|
||||||
"##,
|
"##
|
||||||
"keyup" => r##"
|
}
|
||||||
|
"keyup" => {
|
||||||
|
r##"
|
||||||
agent-browser keyup - Release a key
|
agent-browser keyup - Release a key
|
||||||
|
|
||||||
Usage: agent-browser keyup <key>
|
Usage: agent-browser keyup <key>
|
||||||
@@ -544,10 +579,12 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser keyup Shift
|
agent-browser keyup Shift
|
||||||
agent-browser keyup Control
|
agent-browser keyup Control
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Scroll ===
|
// === Scroll ===
|
||||||
"scroll" => r##"
|
"scroll" => {
|
||||||
|
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]
|
||||||
@@ -567,8 +604,10 @@ 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
|
||||||
"##,
|
"##
|
||||||
"scrollintoview" | "scrollinto" => r##"
|
}
|
||||||
|
"scrollintoview" | "scrollinto" => {
|
||||||
|
r##"
|
||||||
agent-browser scrollintoview - Scroll element into view
|
agent-browser scrollintoview - Scroll element into view
|
||||||
|
|
||||||
Usage: agent-browser scrollintoview <selector>
|
Usage: agent-browser scrollintoview <selector>
|
||||||
@@ -584,10 +623,12 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser scrollintoview "#footer"
|
agent-browser scrollintoview "#footer"
|
||||||
agent-browser scrollintoview @e15
|
agent-browser scrollintoview @e15
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Wait ===
|
// === Wait ===
|
||||||
"wait" => r##"
|
"wait" => {
|
||||||
|
r##"
|
||||||
agent-browser wait - Wait for condition
|
agent-browser wait - Wait for condition
|
||||||
|
|
||||||
Usage: agent-browser wait <selector|ms|option>
|
Usage: agent-browser wait <selector|ms|option>
|
||||||
@@ -613,10 +654,12 @@ 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"
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Screenshot/PDF ===
|
// === Screenshot/PDF ===
|
||||||
"screenshot" => r##"
|
"screenshot" => {
|
||||||
|
r##"
|
||||||
agent-browser screenshot - Take a screenshot
|
agent-browser screenshot - Take a screenshot
|
||||||
|
|
||||||
Usage: agent-browser screenshot [path]
|
Usage: agent-browser screenshot [path]
|
||||||
@@ -635,8 +678,10 @@ Examples:
|
|||||||
agent-browser screenshot
|
agent-browser screenshot
|
||||||
agent-browser screenshot ./screenshot.png
|
agent-browser screenshot ./screenshot.png
|
||||||
agent-browser screenshot --full ./full-page.png
|
agent-browser screenshot --full ./full-page.png
|
||||||
"##,
|
"##
|
||||||
"pdf" => r##"
|
}
|
||||||
|
"pdf" => {
|
||||||
|
r##"
|
||||||
agent-browser pdf - Save page as PDF
|
agent-browser pdf - Save page as PDF
|
||||||
|
|
||||||
Usage: agent-browser pdf <path>
|
Usage: agent-browser pdf <path>
|
||||||
@@ -650,10 +695,12 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser pdf ./page.pdf
|
agent-browser pdf ./page.pdf
|
||||||
agent-browser pdf ~/Documents/report.pdf
|
agent-browser pdf ~/Documents/report.pdf
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Snapshot ===
|
// === Snapshot ===
|
||||||
"snapshot" => r##"
|
"snapshot" => {
|
||||||
|
r##"
|
||||||
agent-browser snapshot - Get accessibility tree snapshot
|
agent-browser snapshot - Get accessibility tree snapshot
|
||||||
|
|
||||||
Usage: agent-browser snapshot [options]
|
Usage: agent-browser snapshot [options]
|
||||||
@@ -677,10 +724,12 @@ Examples:
|
|||||||
agent-browser snapshot -i
|
agent-browser snapshot -i
|
||||||
agent-browser snapshot --compact --depth 5
|
agent-browser snapshot --compact --depth 5
|
||||||
agent-browser snapshot -s "#main-content"
|
agent-browser snapshot -s "#main-content"
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Eval ===
|
// === Eval ===
|
||||||
"eval" => r##"
|
"eval" => {
|
||||||
|
r##"
|
||||||
agent-browser eval - Execute JavaScript
|
agent-browser eval - Execute JavaScript
|
||||||
|
|
||||||
Usage: agent-browser eval <script>
|
Usage: agent-browser eval <script>
|
||||||
@@ -695,10 +744,12 @@ Examples:
|
|||||||
agent-browser eval "document.title"
|
agent-browser eval "document.title"
|
||||||
agent-browser eval "window.location.href"
|
agent-browser eval "window.location.href"
|
||||||
agent-browser eval "document.querySelectorAll('a').length"
|
agent-browser eval "document.querySelectorAll('a').length"
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Close ===
|
// === Close ===
|
||||||
"close" | "quit" | "exit" => r##"
|
"close" | "quit" | "exit" => {
|
||||||
|
r##"
|
||||||
agent-browser close - Close the browser
|
agent-browser close - Close the browser
|
||||||
|
|
||||||
Usage: agent-browser close
|
Usage: agent-browser close
|
||||||
@@ -714,10 +765,12 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser close
|
agent-browser close
|
||||||
agent-browser close --session mysession
|
agent-browser close --session mysession
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Get ===
|
// === Get ===
|
||||||
"get" => r##"
|
"get" => {
|
||||||
|
r##"
|
||||||
agent-browser get - Retrieve information from elements or page
|
agent-browser get - Retrieve information from elements or page
|
||||||
|
|
||||||
Usage: agent-browser get <subcommand> [args]
|
Usage: agent-browser get <subcommand> [args]
|
||||||
@@ -750,10 +803,12 @@ Examples:
|
|||||||
agent-browser get box "#header"
|
agent-browser get box "#header"
|
||||||
agent-browser get styles "button"
|
agent-browser get styles "button"
|
||||||
agent-browser get styles @e1
|
agent-browser get styles @e1
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Is ===
|
// === Is ===
|
||||||
"is" => r##"
|
"is" => {
|
||||||
|
r##"
|
||||||
agent-browser is - Check element state
|
agent-browser is - Check element state
|
||||||
|
|
||||||
Usage: agent-browser is <subcommand> <selector>
|
Usage: agent-browser is <subcommand> <selector>
|
||||||
@@ -773,10 +828,12 @@ Examples:
|
|||||||
agent-browser is visible "#modal"
|
agent-browser is visible "#modal"
|
||||||
agent-browser is enabled "#submit-btn"
|
agent-browser is enabled "#submit-btn"
|
||||||
agent-browser is checked "#agree-checkbox"
|
agent-browser is checked "#agree-checkbox"
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Find ===
|
// === Find ===
|
||||||
"find" => r##"
|
"find" => {
|
||||||
|
r##"
|
||||||
agent-browser find - Find and interact with elements by locator
|
agent-browser find - Find and interact with elements by locator
|
||||||
|
|
||||||
Usage: agent-browser find <locator> <value> [action] [text]
|
Usage: agent-browser find <locator> <value> [action] [text]
|
||||||
@@ -814,10 +871,12 @@ Examples:
|
|||||||
agent-browser find testid "login-form" click
|
agent-browser find testid "login-form" click
|
||||||
agent-browser find first "li.item" click
|
agent-browser find first "li.item" click
|
||||||
agent-browser find nth 2 ".card" hover
|
agent-browser find nth 2 ".card" hover
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Mouse ===
|
// === Mouse ===
|
||||||
"mouse" => r##"
|
"mouse" => {
|
||||||
|
r##"
|
||||||
agent-browser mouse - Low-level mouse operations
|
agent-browser mouse - Low-level mouse operations
|
||||||
|
|
||||||
Usage: agent-browser mouse <subcommand> [args]
|
Usage: agent-browser mouse <subcommand> [args]
|
||||||
@@ -841,10 +900,12 @@ Examples:
|
|||||||
agent-browser mouse down right
|
agent-browser mouse down right
|
||||||
agent-browser mouse wheel 100
|
agent-browser mouse wheel 100
|
||||||
agent-browser mouse wheel -50 0
|
agent-browser mouse wheel -50 0
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Set ===
|
// === Set ===
|
||||||
"set" => r##"
|
"set" => {
|
||||||
|
r##"
|
||||||
agent-browser set - Configure browser settings
|
agent-browser set - Configure browser settings
|
||||||
|
|
||||||
Usage: agent-browser set <setting> [args]
|
Usage: agent-browser set <setting> [args]
|
||||||
@@ -874,10 +935,12 @@ Examples:
|
|||||||
agent-browser set credentials admin secret123
|
agent-browser set credentials admin secret123
|
||||||
agent-browser set media dark
|
agent-browser set media dark
|
||||||
agent-browser set media light reduced-motion
|
agent-browser set media light reduced-motion
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Network ===
|
// === Network ===
|
||||||
"network" => r##"
|
"network" => {
|
||||||
|
r##"
|
||||||
agent-browser network - Network interception and monitoring
|
agent-browser network - Network interception and monitoring
|
||||||
|
|
||||||
Usage: agent-browser network <subcommand> [args]
|
Usage: agent-browser network <subcommand> [args]
|
||||||
@@ -904,10 +967,12 @@ Examples:
|
|||||||
agent-browser network requests
|
agent-browser network requests
|
||||||
agent-browser network requests --filter "api"
|
agent-browser network requests --filter "api"
|
||||||
agent-browser network requests --clear
|
agent-browser network requests --clear
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Storage ===
|
// === Storage ===
|
||||||
"storage" => r##"
|
"storage" => {
|
||||||
|
r##"
|
||||||
agent-browser storage - Manage web storage
|
agent-browser storage - Manage web storage
|
||||||
|
|
||||||
Usage: agent-browser storage <type> [operation] [key] [value]
|
Usage: agent-browser storage <type> [operation] [key] [value]
|
||||||
@@ -933,10 +998,12 @@ Examples:
|
|||||||
agent-browser storage local set theme "dark"
|
agent-browser storage local set theme "dark"
|
||||||
agent-browser storage local clear
|
agent-browser storage local clear
|
||||||
agent-browser storage session get userId
|
agent-browser storage session get userId
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Cookies ===
|
// === Cookies ===
|
||||||
"cookies" => r##"
|
"cookies" => {
|
||||||
|
r##"
|
||||||
agent-browser cookies - Manage browser cookies
|
agent-browser cookies - Manage browser cookies
|
||||||
|
|
||||||
Usage: agent-browser cookies [operation] [args]
|
Usage: agent-browser cookies [operation] [args]
|
||||||
@@ -957,10 +1024,12 @@ Examples:
|
|||||||
agent-browser cookies get
|
agent-browser cookies get
|
||||||
agent-browser cookies set session_id "abc123"
|
agent-browser cookies set session_id "abc123"
|
||||||
agent-browser cookies clear
|
agent-browser cookies clear
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Tabs ===
|
// === Tabs ===
|
||||||
"tab" => r##"
|
"tab" => {
|
||||||
|
r##"
|
||||||
agent-browser tab - Manage browser tabs
|
agent-browser tab - Manage browser tabs
|
||||||
|
|
||||||
Usage: agent-browser tab [operation] [args]
|
Usage: agent-browser tab [operation] [args]
|
||||||
@@ -985,10 +1054,12 @@ Examples:
|
|||||||
agent-browser tab 2
|
agent-browser tab 2
|
||||||
agent-browser tab close
|
agent-browser tab close
|
||||||
agent-browser tab close 1
|
agent-browser tab close 1
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Window ===
|
// === Window ===
|
||||||
"window" => r##"
|
"window" => {
|
||||||
|
r##"
|
||||||
agent-browser window - Manage browser windows
|
agent-browser window - Manage browser windows
|
||||||
|
|
||||||
Usage: agent-browser window <operation>
|
Usage: agent-browser window <operation>
|
||||||
@@ -1004,10 +1075,12 @@ Global Options:
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser window new
|
agent-browser window new
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Frame ===
|
// === Frame ===
|
||||||
"frame" => r##"
|
"frame" => {
|
||||||
|
r##"
|
||||||
agent-browser frame - Switch frame context
|
agent-browser frame - Switch frame context
|
||||||
|
|
||||||
Usage: agent-browser frame <selector|main>
|
Usage: agent-browser frame <selector|main>
|
||||||
@@ -1026,10 +1099,12 @@ Examples:
|
|||||||
agent-browser frame "#embed-iframe"
|
agent-browser frame "#embed-iframe"
|
||||||
agent-browser frame "iframe[name='content']"
|
agent-browser frame "iframe[name='content']"
|
||||||
agent-browser frame main
|
agent-browser frame main
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Dialog ===
|
// === Dialog ===
|
||||||
"dialog" => r##"
|
"dialog" => {
|
||||||
|
r##"
|
||||||
agent-browser dialog - Handle browser dialogs
|
agent-browser dialog - Handle browser dialogs
|
||||||
|
|
||||||
Usage: agent-browser dialog <response> [text]
|
Usage: agent-browser dialog <response> [text]
|
||||||
@@ -1048,10 +1123,12 @@ Examples:
|
|||||||
agent-browser dialog accept
|
agent-browser dialog accept
|
||||||
agent-browser dialog accept "my input"
|
agent-browser dialog accept "my input"
|
||||||
agent-browser dialog dismiss
|
agent-browser dialog dismiss
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Trace ===
|
// === Trace ===
|
||||||
"trace" => r##"
|
"trace" => {
|
||||||
|
r##"
|
||||||
agent-browser trace - Record execution trace
|
agent-browser trace - Record execution trace
|
||||||
|
|
||||||
Usage: agent-browser trace <operation> [path]
|
Usage: agent-browser trace <operation> [path]
|
||||||
@@ -1071,7 +1148,8 @@ Examples:
|
|||||||
agent-browser trace start ./my-trace
|
agent-browser trace start ./my-trace
|
||||||
agent-browser trace stop
|
agent-browser trace stop
|
||||||
agent-browser trace stop ./debug-trace.zip
|
agent-browser trace stop ./debug-trace.zip
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Record (video) ===
|
// === Record (video) ===
|
||||||
"record" => r##"
|
"record" => r##"
|
||||||
@@ -1110,7 +1188,8 @@ Examples:
|
|||||||
"##,
|
"##,
|
||||||
|
|
||||||
// === Console/Errors ===
|
// === Console/Errors ===
|
||||||
"console" => r##"
|
"console" => {
|
||||||
|
r##"
|
||||||
agent-browser console - View console logs
|
agent-browser console - View console logs
|
||||||
|
|
||||||
Usage: agent-browser console [--clear]
|
Usage: agent-browser console [--clear]
|
||||||
@@ -1127,8 +1206,10 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser console
|
agent-browser console
|
||||||
agent-browser console --clear
|
agent-browser console --clear
|
||||||
"##,
|
"##
|
||||||
"errors" => r##"
|
}
|
||||||
|
"errors" => {
|
||||||
|
r##"
|
||||||
agent-browser errors - View page errors
|
agent-browser errors - View page errors
|
||||||
|
|
||||||
Usage: agent-browser errors [--clear]
|
Usage: agent-browser errors [--clear]
|
||||||
@@ -1145,10 +1226,12 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser errors
|
agent-browser errors
|
||||||
agent-browser errors --clear
|
agent-browser errors --clear
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Highlight ===
|
// === Highlight ===
|
||||||
"highlight" => r##"
|
"highlight" => {
|
||||||
|
r##"
|
||||||
agent-browser highlight - Highlight an element
|
agent-browser highlight - Highlight an element
|
||||||
|
|
||||||
Usage: agent-browser highlight <selector>
|
Usage: agent-browser highlight <selector>
|
||||||
@@ -1162,10 +1245,12 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser highlight "#target-element"
|
agent-browser highlight "#target-element"
|
||||||
agent-browser highlight @e5
|
agent-browser highlight @e5
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === State ===
|
// === State ===
|
||||||
"state" => r##"
|
"state" => {
|
||||||
|
r##"
|
||||||
agent-browser state - Save/load browser state
|
agent-browser state - Save/load browser state
|
||||||
|
|
||||||
Usage: agent-browser state <operation> <path>
|
Usage: agent-browser state <operation> <path>
|
||||||
@@ -1183,10 +1268,12 @@ Global Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser state save ./auth-state.json
|
agent-browser state save ./auth-state.json
|
||||||
agent-browser state load ./auth-state.json
|
agent-browser state load ./auth-state.json
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Session ===
|
// === Session ===
|
||||||
"session" => r##"
|
"session" => {
|
||||||
|
r##"
|
||||||
agent-browser session - Manage sessions
|
agent-browser session - Manage sessions
|
||||||
|
|
||||||
Usage: agent-browser session [operation]
|
Usage: agent-browser session [operation]
|
||||||
@@ -1209,10 +1296,12 @@ Examples:
|
|||||||
agent-browser session
|
agent-browser session
|
||||||
agent-browser session list
|
agent-browser session list
|
||||||
agent-browser --session test open example.com
|
agent-browser --session test open example.com
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === Install ===
|
// === Install ===
|
||||||
"install" => r##"
|
"install" => {
|
||||||
|
r##"
|
||||||
agent-browser install - Install browser binaries
|
agent-browser install - Install browser binaries
|
||||||
|
|
||||||
Usage: agent-browser install [--with-deps]
|
Usage: agent-browser install [--with-deps]
|
||||||
@@ -1225,7 +1314,8 @@ Options:
|
|||||||
Examples:
|
Examples:
|
||||||
agent-browser install
|
agent-browser install
|
||||||
agent-browser install --with-deps
|
agent-browser install --with-deps
|
||||||
"##,
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
_ => return false,
|
_ => return false,
|
||||||
};
|
};
|
||||||
@@ -1324,8 +1414,14 @@ Options:
|
|||||||
--session <name> Isolated session (or AGENT_BROWSER_SESSION env)
|
--session <name> Isolated session (or AGENT_BROWSER_SESSION env)
|
||||||
--headers <json> HTTP headers scoped to URL's origin (for auth)
|
--headers <json> HTTP headers scoped to URL's origin (for auth)
|
||||||
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
|
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
|
||||||
--extension <path> Load browser extensions (repeatable).
|
--extension <path> Load browser extensions (repeatable)
|
||||||
--proxy <url> Proxy server (http://[user:pass@]host:port)
|
--args <args> Browser launch args, comma or newline separated (or AGENT_BROWSER_ARGS)
|
||||||
|
e.g., --args "--no-sandbox,--disable-blink-features=AutomationControlled"
|
||||||
|
--user-agent <ua> Custom User-Agent (or AGENT_BROWSER_USER_AGENT)
|
||||||
|
--proxy <server> Proxy server URL (or AGENT_BROWSER_PROXY)
|
||||||
|
e.g., --proxy "http://user:pass@127.0.0.1:7890"
|
||||||
|
--proxy-bypass <hosts> Bypass proxy for these hosts (or AGENT_BROWSER_PROXY_BYPASS)
|
||||||
|
e.g., --proxy-bypass "localhost,*.internal.com"
|
||||||
--json JSON output
|
--json JSON output
|
||||||
--full, -f Full page screenshot
|
--full, -f Full page screenshot
|
||||||
--headed Show browser window (not headless)
|
--headed Show browser window (not headless)
|
||||||
|
|||||||
+7
-1
@@ -865,14 +865,18 @@ export class BrowserManager {
|
|||||||
if (hasExtensions) {
|
if (hasExtensions) {
|
||||||
const extPaths = options.extensions!.join(',');
|
const extPaths = options.extensions!.join(',');
|
||||||
const session = process.env.AGENT_BROWSER_SESSION || 'default';
|
const session = process.env.AGENT_BROWSER_SESSION || 'default';
|
||||||
|
// Combine extension args with custom args
|
||||||
|
const extArgs = [`--disable-extensions-except=${extPaths}`, `--load-extension=${extPaths}`];
|
||||||
|
const allArgs = options.args ? [...extArgs, ...options.args] : extArgs;
|
||||||
context = await launcher.launchPersistentContext(
|
context = await launcher.launchPersistentContext(
|
||||||
path.join(os.tmpdir(), `agent-browser-ext-${session}`),
|
path.join(os.tmpdir(), `agent-browser-ext-${session}`),
|
||||||
{
|
{
|
||||||
headless: false,
|
headless: false,
|
||||||
executablePath: options.executablePath,
|
executablePath: options.executablePath,
|
||||||
args: [`--disable-extensions-except=${extPaths}`, `--load-extension=${extPaths}`],
|
args: allArgs,
|
||||||
viewport,
|
viewport,
|
||||||
extraHTTPHeaders: options.headers,
|
extraHTTPHeaders: options.headers,
|
||||||
|
userAgent: options.userAgent,
|
||||||
...(options.proxy && { proxy: options.proxy }),
|
...(options.proxy && { proxy: options.proxy }),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -881,11 +885,13 @@ export class BrowserManager {
|
|||||||
this.browser = await launcher.launch({
|
this.browser = await launcher.launch({
|
||||||
headless: options.headless ?? true,
|
headless: options.headless ?? true,
|
||||||
executablePath: options.executablePath,
|
executablePath: options.executablePath,
|
||||||
|
args: options.args,
|
||||||
});
|
});
|
||||||
this.cdpEndpoint = null;
|
this.cdpEndpoint = null;
|
||||||
context = await this.browser.newContext({
|
context = await this.browser.newContext({
|
||||||
viewport,
|
viewport,
|
||||||
extraHTTPHeaders: options.headers,
|
extraHTTPHeaders: options.headers,
|
||||||
|
userAgent: options.userAgent,
|
||||||
...(options.proxy && { proxy: options.proxy }),
|
...(options.proxy && { proxy: options.proxy }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-1
@@ -197,12 +197,35 @@ export async function startDaemon(options?: { streamPort?: number }): Promise<vo
|
|||||||
.map((p) => p.trim())
|
.map((p) => p.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
// Parse args from env (comma or newline separated)
|
||||||
|
const argsEnv = process.env.AGENT_BROWSER_ARGS;
|
||||||
|
const args = argsEnv
|
||||||
|
? argsEnv
|
||||||
|
.split(/[,\n]/)
|
||||||
|
.map((a) => a.trim())
|
||||||
|
.filter((a) => a.length > 0)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Parse proxy from env
|
||||||
|
const proxyServer = process.env.AGENT_BROWSER_PROXY;
|
||||||
|
const proxyBypass = process.env.AGENT_BROWSER_PROXY_BYPASS;
|
||||||
|
const proxy = proxyServer
|
||||||
|
? {
|
||||||
|
server: proxyServer,
|
||||||
|
...(proxyBypass && { bypass: proxyBypass }),
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
await browser.launch({
|
await browser.launch({
|
||||||
id: 'auto',
|
id: 'auto',
|
||||||
action: 'launch',
|
action: 'launch' as const,
|
||||||
headless: process.env.AGENT_BROWSER_HEADED !== '1',
|
headless: process.env.AGENT_BROWSER_HEADED !== '1',
|
||||||
executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH,
|
executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH,
|
||||||
extensions: extensions,
|
extensions: extensions,
|
||||||
|
args,
|
||||||
|
userAgent: process.env.AGENT_BROWSER_USER_AGENT,
|
||||||
|
proxy,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ const launchSchema = baseCommandSchema.extend({
|
|||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
args: z.array(z.string()).optional(),
|
||||||
|
userAgent: z.string().optional(),
|
||||||
provider: z.string().optional(),
|
provider: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export interface LaunchCommand extends BaseCommand {
|
|||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
};
|
};
|
||||||
|
args?: string[];
|
||||||
|
userAgent?: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,388 @@
|
|||||||
|
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
|
||||||
|
const CLI_PATH = resolve(__dirname, '../cli/target/release/agent-browser');
|
||||||
|
|
||||||
|
// Check if binary exists before running any tests
|
||||||
|
beforeAll(() => {
|
||||||
|
if (!existsSync(CLI_PATH)) {
|
||||||
|
throw new Error(`CLI binary not found at: ${CLI_PATH}. Please build the project first with 'cargo build --release'.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate unique session for each test
|
||||||
|
let testCounter = 0;
|
||||||
|
function getUniqueSession(): string {
|
||||||
|
return `e2e-${Date.now()}-${testCounter++}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCli(session: string, args: string): string {
|
||||||
|
return execSync(`${CLI_PATH} --session ${session} ${args}`, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
timeout: 30000,
|
||||||
|
}).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCliWithEnv(session: string, args: string, envVars: Record<string, string>): string {
|
||||||
|
const env = { ...process.env, ...envVars };
|
||||||
|
return execSync(`${CLI_PATH} --session ${session} ${args}`, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
env,
|
||||||
|
timeout: 30000,
|
||||||
|
}).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCliJson(session: string, args: string): {
|
||||||
|
success: boolean;
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
error?: string;
|
||||||
|
} {
|
||||||
|
const output = runCli(session, `${args} --json`);
|
||||||
|
return JSON.parse(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBrowser(session: string) {
|
||||||
|
try {
|
||||||
|
execSync(`${CLI_PATH} --session ${session} close`, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Ignore if already closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to ensure browser is closed before test
|
||||||
|
function ensureBrowserClosed(session: string) {
|
||||||
|
closeBrowser(session);
|
||||||
|
// Wait a bit to ensure daemon is fully stopped
|
||||||
|
execSync('sleep 0.2', { timeout: 1000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('E2E: Launch Options', () => {
|
||||||
|
let session: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
session = getUniqueSession();
|
||||||
|
ensureBrowserClosed(session);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
closeBrowser(session);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--args flag', () => {
|
||||||
|
it('should disable webdriver detection with --args', () => {
|
||||||
|
runCli(session, '--args "--disable-blink-features=AutomationControlled" open https://example.com');
|
||||||
|
const result = runCliJson(session, 'eval "navigator.webdriver"');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data?.result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support multiple comma-separated args', () => {
|
||||||
|
runCli(session, '--args "--disable-blink-features=AutomationControlled,--disable-dev-shm-usage" open https://example.com');
|
||||||
|
const result = runCliJson(session, 'eval "navigator.webdriver"');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data?.result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have webdriver=true without --args (default behavior)', () => {
|
||||||
|
runCli(session, 'open https://example.com');
|
||||||
|
const result = runCliJson(session, 'eval "navigator.webdriver"');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data?.result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--user-agent flag', () => {
|
||||||
|
it('should set custom user-agent', () => {
|
||||||
|
const customUA = 'E2ETestBot/1.0';
|
||||||
|
runCli(session, `--user-agent "${customUA}" open https://example.com`);
|
||||||
|
const result = runCliJson(session, 'eval "navigator.userAgent"');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data?.result).toBe(customUA);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default Chrome user-agent when not specified', () => {
|
||||||
|
runCli(session, 'open https://example.com');
|
||||||
|
const result = runCliJson(session, 'eval "navigator.userAgent"');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(String(result.data?.result)).toContain('Chrome');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--proxy flag', () => {
|
||||||
|
it('should fail navigation when proxy is unreachable (proves proxy is used)', () => {
|
||||||
|
// Launch with unreachable proxy - navigation should fail immediately
|
||||||
|
// This proves proxy is being used
|
||||||
|
let failed = false;
|
||||||
|
try {
|
||||||
|
runCli(session, '--proxy "http://127.0.0.1:59999" open https://example.com');
|
||||||
|
} catch (e) {
|
||||||
|
failed = true;
|
||||||
|
expect(String(e)).toContain('ERR_PROXY_CONNECTION_FAILED');
|
||||||
|
}
|
||||||
|
expect(failed).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('environment variables', () => {
|
||||||
|
it('should read AGENT_BROWSER_ARGS from environment', () => {
|
||||||
|
runCliWithEnv(session, 'open https://example.com', {
|
||||||
|
AGENT_BROWSER_ARGS: '--disable-blink-features=AutomationControlled',
|
||||||
|
});
|
||||||
|
const result = runCliJson(session, 'eval "navigator.webdriver"');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data?.result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read AGENT_BROWSER_USER_AGENT from environment', () => {
|
||||||
|
const customUA = 'EnvTestBot/2.0';
|
||||||
|
runCliWithEnv(session, 'open https://example.com', {
|
||||||
|
AGENT_BROWSER_USER_AGENT: customUA,
|
||||||
|
});
|
||||||
|
const result = runCliJson(session, 'eval "navigator.userAgent"');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data?.result).toBe(customUA);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('warning for already running daemon', () => {
|
||||||
|
it('should warn when launch-time options are ignored', () => {
|
||||||
|
// First, start daemon with default options
|
||||||
|
runCli(session, 'open https://example.com');
|
||||||
|
// Try to use --user-agent with already running daemon
|
||||||
|
const output = execSync(
|
||||||
|
`${CLI_PATH} --session ${session} --user-agent "IgnoredUA" get url 2>&1`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
expect(output).toContain('--user-agent ignored');
|
||||||
|
expect(output).toContain('daemon already running');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Single parameter tests', () => {
|
||||||
|
it('--args only', () => {
|
||||||
|
runCli(session, '--args "--disable-blink-features=AutomationControlled" open https://example.com');
|
||||||
|
const result = runCliJson(session, 'eval "navigator.webdriver"');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data?.result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('--user-agent only', () => {
|
||||||
|
const ua = 'SingleParam/1.0';
|
||||||
|
runCli(session, `--user-agent "${ua}" open https://example.com`);
|
||||||
|
const result = runCliJson(session, 'eval "navigator.userAgent"');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data?.result).toBe(ua);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('--proxy only', () => {
|
||||||
|
// Use real proxy to verify it works
|
||||||
|
runCli(session, '--proxy "http://localhost:7890" open https://httpbin.org/ip');
|
||||||
|
const urlResult = runCliJson(session, 'get url');
|
||||||
|
expect(urlResult.success).toBe(true);
|
||||||
|
expect(urlResult.data?.url).toBe('https://httpbin.org/ip');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Two parameter combinations', () => {
|
||||||
|
it('--args + --user-agent', () => {
|
||||||
|
const ua = 'TwoParam/1.0';
|
||||||
|
runCli(session, `--args "--disable-blink-features=AutomationControlled" --user-agent "${ua}" open https://example.com`);
|
||||||
|
|
||||||
|
const uaResult = runCliJson(session, 'eval "navigator.userAgent"');
|
||||||
|
expect(uaResult.success).toBe(true);
|
||||||
|
expect(uaResult.data?.result).toBe(ua);
|
||||||
|
|
||||||
|
const wdResult = runCliJson(session, 'eval "navigator.webdriver"');
|
||||||
|
expect(wdResult.success).toBe(true);
|
||||||
|
expect(wdResult.data?.result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('--proxy + --user-agent', () => {
|
||||||
|
const ua = 'ProxyUA/1.0';
|
||||||
|
runCli(session, `--proxy "http://localhost:7890" --user-agent "${ua}" open https://example.com`);
|
||||||
|
|
||||||
|
const uaResult = runCliJson(session, 'eval "navigator.userAgent"');
|
||||||
|
expect(uaResult.success).toBe(true);
|
||||||
|
expect(uaResult.data?.result).toBe(ua);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('--proxy + --args', () => {
|
||||||
|
runCli(session, '--proxy "http://localhost:7890" --args "--disable-blink-features=AutomationControlled" open https://example.com');
|
||||||
|
|
||||||
|
const wdResult = runCliJson(session, 'eval "navigator.webdriver"');
|
||||||
|
expect(wdResult.success).toBe(true);
|
||||||
|
expect(wdResult.data?.result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('--proxy + --proxy-bypass', () => {
|
||||||
|
// example.com should bypass proxy
|
||||||
|
runCli(session, '--proxy "http://localhost:7890" --proxy-bypass "example.com" open https://example.com');
|
||||||
|
|
||||||
|
const urlResult = runCliJson(session, 'get url');
|
||||||
|
expect(urlResult.success).toBe(true);
|
||||||
|
expect(urlResult.data?.url).toBe('https://example.com/');
|
||||||
|
|
||||||
|
const titleResult = runCliJson(session, 'get title');
|
||||||
|
expect(titleResult.success).toBe(true);
|
||||||
|
expect(String(titleResult.data?.title)).toContain('Example Domain');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Three parameter combinations', () => {
|
||||||
|
it('--proxy + --user-agent + --args', () => {
|
||||||
|
const ua = 'ThreeParam/1.0';
|
||||||
|
runCli(session, `--proxy "http://localhost:7890" --user-agent "${ua}" --args "--disable-blink-features=AutomationControlled" open https://example.com`);
|
||||||
|
|
||||||
|
const uaResult = runCliJson(session, 'eval "navigator.userAgent"');
|
||||||
|
expect(uaResult.success).toBe(true);
|
||||||
|
expect(uaResult.data?.result).toBe(ua);
|
||||||
|
|
||||||
|
const wdResult = runCliJson(session, 'eval "navigator.webdriver"');
|
||||||
|
expect(wdResult.success).toBe(true);
|
||||||
|
expect(wdResult.data?.result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('--proxy + --proxy-bypass + --user-agent', () => {
|
||||||
|
const ua = 'BypassUA/1.0';
|
||||||
|
runCli(session, `--proxy "http://localhost:7890" --proxy-bypass "example.com" --user-agent "${ua}" open https://example.com`);
|
||||||
|
|
||||||
|
const uaResult = runCliJson(session, 'eval "navigator.userAgent"');
|
||||||
|
expect(uaResult.success).toBe(true);
|
||||||
|
expect(uaResult.data?.result).toBe(ua);
|
||||||
|
|
||||||
|
const urlResult = runCliJson(session, 'get url');
|
||||||
|
expect(urlResult.success).toBe(true);
|
||||||
|
expect(urlResult.data?.url).toBe('https://example.com/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('--proxy + --proxy-bypass + --args', () => {
|
||||||
|
runCli(session, '--proxy "http://localhost:7890" --proxy-bypass "example.com" --args "--disable-blink-features=AutomationControlled" open https://example.com');
|
||||||
|
|
||||||
|
const wdResult = runCliJson(session, 'eval "navigator.webdriver"');
|
||||||
|
expect(wdResult.success).toBe(true);
|
||||||
|
expect(wdResult.data?.result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('--user-agent + --args + --proxy-bypass (no proxy)', () => {
|
||||||
|
const ua = 'NoProxy/1.0';
|
||||||
|
// Without proxy, proxy-bypass should be ignored
|
||||||
|
runCli(session, `--user-agent "${ua}" --args "--disable-blink-features=AutomationControlled" open https://example.com`);
|
||||||
|
|
||||||
|
const uaResult = runCliJson(session, 'eval "navigator.userAgent"');
|
||||||
|
expect(uaResult.success).toBe(true);
|
||||||
|
expect(uaResult.data?.result).toBe(ua);
|
||||||
|
|
||||||
|
const wdResult = runCliJson(session, 'eval "navigator.webdriver"');
|
||||||
|
expect(wdResult.success).toBe(true);
|
||||||
|
expect(wdResult.data?.result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Four parameter combinations (all parameters)', () => {
|
||||||
|
it('--proxy + --proxy-bypass + --user-agent + --args', () => {
|
||||||
|
const ua = 'AllParams/1.0';
|
||||||
|
runCli(session, `--proxy "http://localhost:7890" --proxy-bypass "example.com" --user-agent "${ua}" --args "--disable-blink-features=AutomationControlled" open https://example.com`);
|
||||||
|
|
||||||
|
// Verify user-agent
|
||||||
|
const uaResult = runCliJson(session, 'eval "navigator.userAgent"');
|
||||||
|
expect(uaResult.success).toBe(true);
|
||||||
|
expect(uaResult.data?.result).toBe(ua);
|
||||||
|
|
||||||
|
// Verify webdriver hidden (args works)
|
||||||
|
const wdResult = runCliJson(session, 'eval "navigator.webdriver"');
|
||||||
|
expect(wdResult.success).toBe(true);
|
||||||
|
expect(wdResult.data?.result).toBe(false);
|
||||||
|
|
||||||
|
// Verify page loaded (proxy + bypass works)
|
||||||
|
const urlResult = runCliJson(session, 'get url');
|
||||||
|
expect(urlResult.success).toBe(true);
|
||||||
|
expect(urlResult.data?.url).toBe('https://example.com/');
|
||||||
|
|
||||||
|
// Navigate to httpbin through proxy (not bypassed)
|
||||||
|
runCli(session, 'goto https://httpbin.org/headers');
|
||||||
|
const bodyResult = runCliJson(session, 'get text body');
|
||||||
|
expect(bodyResult.success).toBe(true);
|
||||||
|
const text = String(bodyResult.data?.text);
|
||||||
|
expect(text).toContain(ua);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Multiple args formats', () => {
|
||||||
|
it('comma-separated args with proxy and user-agent', () => {
|
||||||
|
const ua = 'CommaArgs/1.0';
|
||||||
|
runCli(session, `--proxy "http://localhost:7890" --user-agent "${ua}" --args "--disable-blink-features=AutomationControlled,--disable-dev-shm-usage" open https://example.com`);
|
||||||
|
|
||||||
|
const uaResult = runCliJson(session, 'eval "navigator.userAgent"');
|
||||||
|
expect(uaResult.success).toBe(true);
|
||||||
|
expect(uaResult.data?.result).toBe(ua);
|
||||||
|
|
||||||
|
const wdResult = runCliJson(session, 'eval "navigator.webdriver"');
|
||||||
|
expect(wdResult.success).toBe(true);
|
||||||
|
expect(wdResult.data?.result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Real-world scenarios', () => {
|
||||||
|
it('mobile device simulation with proxy', () => {
|
||||||
|
const mobileUA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15';
|
||||||
|
runCli(session, `--proxy "http://localhost:7890" --proxy-bypass "example.com" --user-agent "${mobileUA}" --args "--disable-blink-features=AutomationControlled" open https://example.com`);
|
||||||
|
|
||||||
|
const uaResult = runCliJson(session, 'eval "navigator.userAgent"');
|
||||||
|
expect(uaResult.success).toBe(true);
|
||||||
|
expect(String(uaResult.data?.result)).toContain('iPhone');
|
||||||
|
|
||||||
|
const wdResult = runCliJson(session, 'eval "navigator.webdriver"');
|
||||||
|
expect(wdResult.success).toBe(true);
|
||||||
|
expect(wdResult.data?.result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stealth browsing with all anti-detection features', () => {
|
||||||
|
const stealthUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
||||||
|
runCli(session, `--proxy "http://localhost:7890" --proxy-bypass "example.com,localhost" --user-agent "${stealthUA}" --args "--disable-blink-features=AutomationControlled,--disable-web-security" open https://example.com`);
|
||||||
|
|
||||||
|
const uaResult = runCliJson(session, 'eval "navigator.userAgent"');
|
||||||
|
expect(uaResult.success).toBe(true);
|
||||||
|
expect(uaResult.data?.result).toBe(stealthUA);
|
||||||
|
|
||||||
|
const wdResult = runCliJson(session, 'eval "navigator.webdriver"');
|
||||||
|
expect(wdResult.success).toBe(true);
|
||||||
|
expect(wdResult.data?.result).toBe(false);
|
||||||
|
|
||||||
|
const pluginsResult = runCliJson(session, 'eval "typeof navigator.plugins"');
|
||||||
|
expect(pluginsResult.success).toBe(true);
|
||||||
|
expect(pluginsResult.data?.result).toBe('object');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('combine proxy with user-agent and args', () => {
|
||||||
|
const customUA = 'ProxyTestBot/1.0';
|
||||||
|
runCli(session, `--proxy "http://localhost:7890" --user-agent "${customUA}" --args "--disable-blink-features=AutomationControlled" open https://example.com`);
|
||||||
|
|
||||||
|
// navigator.userAgent
|
||||||
|
const uaResult = runCliJson(session, 'eval "navigator.userAgent"');
|
||||||
|
expect(uaResult.success).toBe(true);
|
||||||
|
expect(uaResult.data?.result).toBe(customUA);
|
||||||
|
|
||||||
|
// webdriver
|
||||||
|
const wdResult = runCliJson(session, 'eval "navigator.webdriver"');
|
||||||
|
expect(wdResult.success).toBe(true);
|
||||||
|
expect(wdResult.data?.result).toBe(false);
|
||||||
|
|
||||||
|
// proxy well
|
||||||
|
const urlResult = runCliJson(session, 'get url');
|
||||||
|
expect(urlResult.success).toBe(true);
|
||||||
|
expect(urlResult.data?.url).toBe('https://example.com/');
|
||||||
|
|
||||||
|
// proxy httpbin validate User-Agent
|
||||||
|
runCli(session, 'goto https://httpbin.org/headers');
|
||||||
|
const bodyResult = runCliJson(session, 'get text body');
|
||||||
|
expect(bodyResult.success).toBe(true);
|
||||||
|
const text = String(bodyResult.data?.text);
|
||||||
|
expect(text).toContain(customUA);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { describe, it, expect, afterEach } from 'vitest';
|
||||||
|
import { BrowserManager } from '../src/browser.js';
|
||||||
|
|
||||||
|
describe('Launch Options', () => {
|
||||||
|
let browser: BrowserManager;
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (browser?.isLaunched()) {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('browser args', () => {
|
||||||
|
it('should launch with custom args to disable webdriver detection', async () => {
|
||||||
|
browser = new BrowserManager();
|
||||||
|
await browser.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--disable-blink-features=AutomationControlled'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = browser.getPage();
|
||||||
|
await page.goto('about:blank');
|
||||||
|
|
||||||
|
// Check that navigator.webdriver is false
|
||||||
|
const webdriver = await page.evaluate(() => navigator.webdriver);
|
||||||
|
expect(webdriver).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should launch with multiple args', async () => {
|
||||||
|
browser = new BrowserManager();
|
||||||
|
await browser.launch({
|
||||||
|
headless: true,
|
||||||
|
args: [
|
||||||
|
'--disable-blink-features=AutomationControlled',
|
||||||
|
'--disable-dev-shm-usage',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(browser.isLaunched()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should launch without args (default behavior)', async () => {
|
||||||
|
browser = new BrowserManager();
|
||||||
|
await browser.launch({
|
||||||
|
headless: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = browser.getPage();
|
||||||
|
await page.goto('about:blank');
|
||||||
|
|
||||||
|
// Default Playwright behavior - webdriver is true
|
||||||
|
const webdriver = await page.evaluate(() => navigator.webdriver);
|
||||||
|
expect(webdriver).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('custom user-agent', () => {
|
||||||
|
it('should launch with custom user-agent', async () => {
|
||||||
|
const customUA = 'CustomTestBot/1.0';
|
||||||
|
browser = new BrowserManager();
|
||||||
|
await browser.launch({
|
||||||
|
headless: true,
|
||||||
|
userAgent: customUA,
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = browser.getPage();
|
||||||
|
await page.goto('about:blank');
|
||||||
|
|
||||||
|
const ua = await page.evaluate(() => navigator.userAgent);
|
||||||
|
expect(ua).toBe(customUA);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default user-agent when not specified', async () => {
|
||||||
|
browser = new BrowserManager();
|
||||||
|
await browser.launch({
|
||||||
|
headless: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = browser.getPage();
|
||||||
|
await page.goto('about:blank');
|
||||||
|
|
||||||
|
const ua = await page.evaluate(() => navigator.userAgent);
|
||||||
|
// Default UA should contain Chrome/Chromium
|
||||||
|
expect(ua).toContain('Chrome');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('proxy configuration', () => {
|
||||||
|
it('should accept proxy configuration', async () => {
|
||||||
|
browser = new BrowserManager();
|
||||||
|
// Note: This test just verifies the proxy option is accepted without error
|
||||||
|
// Actual proxy testing requires a running proxy server
|
||||||
|
await browser.launch({
|
||||||
|
headless: true,
|
||||||
|
proxy: {
|
||||||
|
server: 'http://localhost:8080',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(browser.isLaunched()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept proxy with bypass list', async () => {
|
||||||
|
browser = new BrowserManager();
|
||||||
|
await browser.launch({
|
||||||
|
headless: true,
|
||||||
|
proxy: {
|
||||||
|
server: 'http://localhost:8080',
|
||||||
|
bypass: 'localhost,*.internal.com',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(browser.isLaunched()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail connection when proxy is unreachable (proves proxy is being used)', async () => {
|
||||||
|
browser = new BrowserManager();
|
||||||
|
await browser.launch({
|
||||||
|
headless: true,
|
||||||
|
proxy: {
|
||||||
|
server: 'http://127.0.0.1:59999', // Non-existent proxy
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = browser.getPage();
|
||||||
|
// Navigation should fail because proxy is unreachable
|
||||||
|
// This proves the proxy setting is actually being used
|
||||||
|
await expect(page.goto('https://example.com', { timeout: 5000 })).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('combined options', () => {
|
||||||
|
it('should launch with args, user-agent, and proxy combined', async () => {
|
||||||
|
const customUA = 'CombinedTestBot/2.0';
|
||||||
|
browser = new BrowserManager();
|
||||||
|
await browser.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--disable-blink-features=AutomationControlled'],
|
||||||
|
userAgent: customUA,
|
||||||
|
proxy: {
|
||||||
|
server: 'http://localhost:8080',
|
||||||
|
bypass: 'localhost',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = browser.getPage();
|
||||||
|
await page.goto('about:blank');
|
||||||
|
|
||||||
|
// Verify user-agent
|
||||||
|
const ua = await page.evaluate(() => navigator.userAgent);
|
||||||
|
expect(ua).toBe(customUA);
|
||||||
|
|
||||||
|
// Verify webdriver is hidden
|
||||||
|
const webdriver = await page.evaluate(() => navigator.webdriver);
|
||||||
|
expect(webdriver).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user