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) |
|
||||
| `--headers <json>` | Set HTTP headers scoped to the URL's origin |
|
||||
| `--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) |
|
||||
| `--full, -f` | Full page screenshot |
|
||||
| `--name, -n` | Locator name filter |
|
||||
|
||||
+155
-77
@@ -216,7 +216,10 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
// === Scroll ===
|
||||
"scroll" => {
|
||||
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 }))
|
||||
}
|
||||
"scrollintoview" | "scrollinto" => {
|
||||
@@ -231,45 +234,57 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
"wait" => {
|
||||
// Check for --url flag: wait --url "**/dashboard"
|
||||
if let Some(idx) = rest.iter().position(|&s| s == "--url" || s == "-u") {
|
||||
let url = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments {
|
||||
context: "wait --url".to_string(),
|
||||
usage: "wait --url <pattern>",
|
||||
})?;
|
||||
let url = rest
|
||||
.get(idx + 1)
|
||||
.ok_or_else(|| ParseError::MissingArguments {
|
||||
context: "wait --url".to_string(),
|
||||
usage: "wait --url <pattern>",
|
||||
})?;
|
||||
return Ok(json!({ "id": id, "action": "waitforurl", "url": url }));
|
||||
}
|
||||
|
||||
|
||||
// Check for --load flag: wait --load networkidle
|
||||
if let Some(idx) = rest.iter().position(|&s| s == "--load" || s == "-l") {
|
||||
let state = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments {
|
||||
context: "wait --load".to_string(),
|
||||
usage: "wait --load <state>",
|
||||
})?;
|
||||
let state = rest
|
||||
.get(idx + 1)
|
||||
.ok_or_else(|| ParseError::MissingArguments {
|
||||
context: "wait --load".to_string(),
|
||||
usage: "wait --load <state>",
|
||||
})?;
|
||||
return Ok(json!({ "id": id, "action": "waitforloadstate", "state": state }));
|
||||
}
|
||||
|
||||
|
||||
// Check for --fn flag: wait --fn "window.ready === true"
|
||||
if let Some(idx) = rest.iter().position(|&s| s == "--fn" || s == "-f") {
|
||||
let expr = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments {
|
||||
context: "wait --fn".to_string(),
|
||||
usage: "wait --fn <expression>",
|
||||
})?;
|
||||
let expr = rest
|
||||
.get(idx + 1)
|
||||
.ok_or_else(|| ParseError::MissingArguments {
|
||||
context: "wait --fn".to_string(),
|
||||
usage: "wait --fn <expression>",
|
||||
})?;
|
||||
return Ok(json!({ "id": id, "action": "waitforfunction", "expression": expr }));
|
||||
}
|
||||
|
||||
|
||||
// Check for --text flag: wait --text "Welcome"
|
||||
if let Some(idx) = rest.iter().position(|&s| s == "--text" || s == "-t") {
|
||||
let text = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments {
|
||||
context: "wait --text".to_string(),
|
||||
usage: "wait --text <text>",
|
||||
})?;
|
||||
let text = rest
|
||||
.get(idx + 1)
|
||||
.ok_or_else(|| ParseError::MissingArguments {
|
||||
context: "wait --text".to_string(),
|
||||
usage: "wait --text <text>",
|
||||
})?;
|
||||
// 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
|
||||
if let Some(arg) = rest.get(0) {
|
||||
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 {
|
||||
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(),
|
||||
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" })),
|
||||
_ => 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" })),
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// === Window ===
|
||||
"window" => {
|
||||
@@ -456,7 +473,6 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
}
|
||||
Ok(cmd)
|
||||
}
|
||||
Some("dismiss") => Ok(json!({ "id": id, "action": "dialog", "response": "dismiss" })),
|
||||
Some(sub) => Err(ParseError::UnknownSubcommand {
|
||||
subcommand: sub.to_string(),
|
||||
valid_options: VALID,
|
||||
@@ -667,7 +683,7 @@ fn parse_get(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
|
||||
fn parse_is(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
const VALID: &[&str] = &["visible", "enabled", "checked"];
|
||||
|
||||
|
||||
match rest.get(0).map(|s| *s) {
|
||||
Some("visible") => {
|
||||
let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
||||
@@ -702,19 +718,31 @@ fn parse_is(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 {
|
||||
context: "find".to_string(),
|
||||
usage: "find <locator> <value> [action] [text]",
|
||||
})?;
|
||||
|
||||
|
||||
let name_idx = rest.iter().position(|&s| s == "--name");
|
||||
let name = name_idx.and_then(|i| rest.get(i + 1).map(|s| *s));
|
||||
let exact = rest.iter().any(|&s| s == "--exact");
|
||||
|
||||
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 {
|
||||
context: format!("find {}", locator),
|
||||
usage: match *locator {
|
||||
@@ -779,10 +807,12 @@ fn parse_find(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
context: "find nth".to_string(),
|
||||
usage: "find nth <index> <selector> [action] [text]",
|
||||
})?;
|
||||
let idx = idx_str.parse::<i32>().map_err(|_| ParseError::MissingArguments {
|
||||
context: "find nth".to_string(),
|
||||
usage: "find nth <index> <selector> [action] [text]",
|
||||
})?;
|
||||
let idx = idx_str
|
||||
.parse::<i32>()
|
||||
.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 {
|
||||
context: "find nth".to_string(),
|
||||
usage: "find nth <index> <selector> [action] [text]",
|
||||
@@ -806,7 +836,7 @@ fn parse_find(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
|
||||
fn parse_mouse(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
const VALID: &[&str] = &["move", "down", "up", "wheel"];
|
||||
|
||||
|
||||
match rest.get(0).map(|s| *s) {
|
||||
Some("move") => {
|
||||
let x_str = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
||||
@@ -817,14 +847,18 @@ fn parse_mouse(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
context: "mouse move".to_string(),
|
||||
usage: "mouse move <x> <y>",
|
||||
})?;
|
||||
let x = x_str.parse::<i32>().map_err(|_| ParseError::MissingArguments {
|
||||
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>",
|
||||
})?;
|
||||
let x = x_str
|
||||
.parse::<i32>()
|
||||
.map_err(|_| ParseError::MissingArguments {
|
||||
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 }))
|
||||
}
|
||||
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") }))
|
||||
}
|
||||
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);
|
||||
Ok(json!({ "id": id, "action": "wheel", "deltaX": dx, "deltaY": dy }))
|
||||
}
|
||||
@@ -850,8 +887,18 @@ fn parse_mouse(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) {
|
||||
Some("viewport") => {
|
||||
let w_str = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
||||
@@ -862,14 +909,18 @@ fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
context: "set viewport".to_string(),
|
||||
usage: "set viewport <width> <height>",
|
||||
})?;
|
||||
let w = w_str.parse::<i32>().map_err(|_| ParseError::MissingArguments {
|
||||
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>",
|
||||
})?;
|
||||
let w = w_str
|
||||
.parse::<i32>()
|
||||
.map_err(|_| ParseError::MissingArguments {
|
||||
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 }))
|
||||
}
|
||||
Some("device") => {
|
||||
@@ -888,18 +939,25 @@ fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
context: "set geo".to_string(),
|
||||
usage: "set geo <latitude> <longitude>",
|
||||
})?;
|
||||
let lat = lat_str.parse::<f64>().map_err(|_| ParseError::MissingArguments {
|
||||
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>",
|
||||
})?;
|
||||
let lat = lat_str
|
||||
.parse::<f64>()
|
||||
.map_err(|_| ParseError::MissingArguments {
|
||||
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 }))
|
||||
}
|
||||
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 }))
|
||||
}
|
||||
Some("headers") => {
|
||||
@@ -908,8 +966,8 @@ fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
usage: "set headers <json>",
|
||||
})?;
|
||||
// Parse the JSON string into an object
|
||||
let headers: serde_json::Value = serde_json::from_str(headers_json)
|
||||
.map_err(|_| ParseError::MissingArguments {
|
||||
let headers: serde_json::Value =
|
||||
serde_json::from_str(headers_json).map_err(|_| ParseError::MissingArguments {
|
||||
context: "set headers".to_string(),
|
||||
usage: "set headers <json> (must be valid JSON object)",
|
||||
})?;
|
||||
@@ -954,7 +1012,7 @@ fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
|
||||
fn parse_network(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
const VALID: &[&str] = &["route", "unroute", "requests"];
|
||||
|
||||
|
||||
match rest.get(0).map(|s| *s) {
|
||||
Some("route") => {
|
||||
let url = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
||||
@@ -996,7 +1054,7 @@ fn parse_network(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
|
||||
fn parse_storage(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
const VALID: &[&str] = &["local", "session"];
|
||||
|
||||
|
||||
match rest.get(0).map(|s| *s) {
|
||||
Some("local") | Some("session") => {
|
||||
let storage_type = rest.get(0).unwrap();
|
||||
@@ -1013,13 +1071,18 @@ fn parse_storage(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
context: format!("storage {} set", storage_type),
|
||||
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 })),
|
||||
_ => {
|
||||
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 {
|
||||
cmd.as_object_mut().unwrap().insert("key".to_string(), json!(k));
|
||||
cmd.as_object_mut()
|
||||
.unwrap()
|
||||
.insert("key".to_string(), json!(k));
|
||||
}
|
||||
Ok(cmd)
|
||||
}
|
||||
@@ -1052,6 +1115,9 @@ mod tests {
|
||||
extensions: Vec::new(),
|
||||
cdp: None,
|
||||
proxy: None,
|
||||
proxy_bypass: None,
|
||||
args: None,
|
||||
user_agent: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1120,7 +1186,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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["type"], "local");
|
||||
assert_eq!(cmd["key"], "mykey");
|
||||
@@ -1129,7 +1196,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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["type"], "session");
|
||||
assert_eq!(cmd["key"], "skey");
|
||||
@@ -1191,7 +1259,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_navigate_with_multiple_headers() {
|
||||
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();
|
||||
assert_eq!(cmd["headers"]["Authorization"], "Bearer token");
|
||||
assert_eq!(cmd["headers"]["X-Custom"], "value");
|
||||
@@ -1445,7 +1514,10 @@ mod tests {
|
||||
fn test_wait_load_missing_state() {
|
||||
let result = parse_command(&args("wait --load"), &default_flags());
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. }));
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
ParseError::MissingArguments { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1544,14 +1616,20 @@ mod tests {
|
||||
fn test_unknown_command() {
|
||||
let result = parse_command(&args("unknowncommand"), &default_flags());
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), ParseError::UnknownCommand { .. }));
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
ParseError::UnknownCommand { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_args() {
|
||||
let result = parse_command(&[], &default_flags());
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. }));
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
ParseError::MissingArguments { .. }
|
||||
));
|
||||
}
|
||||
|
||||
// === Error message tests ===
|
||||
|
||||
+41
-3
@@ -167,6 +167,10 @@ pub fn ensure_daemon(
|
||||
headed: bool,
|
||||
executable_path: Option<&str>,
|
||||
extensions: &[String],
|
||||
args: Option<&str>,
|
||||
user_agent: Option<&str>,
|
||||
proxy: Option<&str>,
|
||||
proxy_bypass: Option<&str>,
|
||||
) -> Result<DaemonResult, String> {
|
||||
if is_daemon_running(session) && daemon_ready(session) {
|
||||
return Ok(DaemonResult {
|
||||
@@ -199,7 +203,7 @@ pub fn ensure_daemon(
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
||||
|
||||
let mut cmd = Command::new("node");
|
||||
cmd.arg(daemon_path)
|
||||
.env("AGENT_BROWSER_DAEMON", "1")
|
||||
@@ -217,6 +221,22 @@ pub fn ensure_daemon(
|
||||
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
|
||||
unsafe {
|
||||
cmd.pre_exec(|| {
|
||||
@@ -256,10 +276,26 @@ pub fn ensure_daemon(
|
||||
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
|
||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
||||
const DETACHED_PROCESS: u32 = 0x00000008;
|
||||
|
||||
|
||||
cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
@@ -270,7 +306,9 @@ pub fn ensure_daemon(
|
||||
|
||||
for _ in 0..50 {
|
||||
if daemon_ready(session) {
|
||||
return Ok(DaemonResult { already_running: false });
|
||||
return Ok(DaemonResult {
|
||||
already_running: false,
|
||||
});
|
||||
}
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
+55
-8
@@ -11,6 +11,9 @@ pub struct Flags {
|
||||
pub cdp: Option<String>,
|
||||
pub extensions: Vec<String>,
|
||||
pub proxy: Option<String>,
|
||||
pub proxy_bypass: Option<String>,
|
||||
pub args: Option<String>,
|
||||
pub user_agent: 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(),
|
||||
cdp: None,
|
||||
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(),
|
||||
};
|
||||
|
||||
@@ -77,6 +83,24 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
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" => {
|
||||
if let Some(p) = args.get(i + 1) {
|
||||
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
|
||||
const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug"];
|
||||
// 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() {
|
||||
if skip_next {
|
||||
@@ -141,7 +177,10 @@ mod tests {
|
||||
r#"{"Authorization": "Bearer token"}"#.to_string(),
|
||||
];
|
||||
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]
|
||||
@@ -188,14 +227,16 @@ mod tests {
|
||||
assert_eq!(flags.headers, Some(r#"{"Auth":"token"}"#.to_string()));
|
||||
assert!(flags.json);
|
||||
assert!(flags.headed);
|
||||
|
||||
|
||||
let clean = clean_args(&input);
|
||||
assert_eq!(clean, vec!["open", "example.com"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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()));
|
||||
}
|
||||
|
||||
@@ -207,19 +248,25 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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.executable_path, Some("/custom/chrome".to_string()));
|
||||
}
|
||||
|
||||
+51
-16
@@ -198,6 +198,10 @@ fn main() {
|
||||
flags.headed,
|
||||
flags.executable_path.as_deref(),
|
||||
&flags.extensions,
|
||||
flags.args.as_deref(),
|
||||
flags.user_agent.as_deref(),
|
||||
flags.proxy.as_deref(),
|
||||
flags.proxy_bypass.as_deref(),
|
||||
) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
@@ -210,17 +214,27 @@ fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
// Warn if executable_path was specified but daemon was already running
|
||||
if daemon_result.already_running
|
||||
&& (flags.executable_path.is_some() || !flags.extensions.is_empty())
|
||||
{
|
||||
if !flags.json {
|
||||
if flags.executable_path.is_some() {
|
||||
eprintln!("{} --executable-path ignored: daemon already running. Use 'agent-browser close' first to restart with new path.", color::warning_indicator());
|
||||
}
|
||||
if !flags.extensions.is_empty() {
|
||||
eprintln!("{} --extension ignored: daemon already running. Use 'agent-browser close' first to restart with extensions.", color::warning_indicator());
|
||||
}
|
||||
// Warn if launch-time options were specified but daemon was already running
|
||||
if daemon_result.already_running {
|
||||
let has_extensions = !flags.extensions.is_empty();
|
||||
let ignored_flags: Vec<&str> = [
|
||||
flags.executable_path.as_ref().map(|_| "--executable-path"),
|
||||
if has_extensions { Some("--extension") } else { None },
|
||||
flags.args.as_ref().map(|_| "--args"),
|
||||
flags.user_agent.as_ref().map(|_| "--user-agent"),
|
||||
flags.proxy.as_ref().map(|_| "--proxy"),
|
||||
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)
|
||||
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!({
|
||||
"id": gen_id(),
|
||||
"action": "launch",
|
||||
"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 {
|
||||
let proxy_obj = parse_proxy(proxy_str);
|
||||
launch_cmd.as_object_mut()
|
||||
.expect("json! macro guarantees object type")
|
||||
.insert("proxy".to_string(), proxy_obj);
|
||||
let mut proxy_obj = parse_proxy(proxy_str);
|
||||
// Add bypass if specified
|
||||
if let Some(ref bypass) = flags.proxy_bypass {
|
||||
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) {
|
||||
|
||||
+188
-92
@@ -239,7 +239,8 @@ pub fn print_response(resp: &Response, json_mode: bool) {
|
||||
pub fn print_command_help(command: &str) -> bool {
|
||||
let help = match command {
|
||||
// === Navigation ===
|
||||
"open" | "goto" | "navigate" => r##"
|
||||
"open" | "goto" | "navigate" => {
|
||||
r##"
|
||||
agent-browser open - Navigate to a URL
|
||||
|
||||
Usage: agent-browser open <url>
|
||||
@@ -261,8 +262,10 @@ Examples:
|
||||
agent-browser open localhost:3000
|
||||
agent-browser open api.example.com --headers '{"Authorization": "Bearer token"}'
|
||||
# ^ Headers only sent to api.example.com, not other domains
|
||||
"##,
|
||||
"back" => r##"
|
||||
"##
|
||||
}
|
||||
"back" => {
|
||||
r##"
|
||||
agent-browser back - Navigate back in history
|
||||
|
||||
Usage: agent-browser back
|
||||
@@ -276,8 +279,10 @@ Global Options:
|
||||
|
||||
Examples:
|
||||
agent-browser back
|
||||
"##,
|
||||
"forward" => r##"
|
||||
"##
|
||||
}
|
||||
"forward" => {
|
||||
r##"
|
||||
agent-browser forward - Navigate forward in history
|
||||
|
||||
Usage: agent-browser forward
|
||||
@@ -291,8 +296,10 @@ Global Options:
|
||||
|
||||
Examples:
|
||||
agent-browser forward
|
||||
"##,
|
||||
"reload" => r##"
|
||||
"##
|
||||
}
|
||||
"reload" => {
|
||||
r##"
|
||||
agent-browser reload - Reload the current page
|
||||
|
||||
Usage: agent-browser reload
|
||||
@@ -306,10 +313,12 @@ Global Options:
|
||||
|
||||
Examples:
|
||||
agent-browser reload
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Core Actions ===
|
||||
"click" => r##"
|
||||
"click" => {
|
||||
r##"
|
||||
agent-browser click - Click an element
|
||||
|
||||
Usage: agent-browser click <selector>
|
||||
@@ -326,8 +335,10 @@ Examples:
|
||||
agent-browser click @e1
|
||||
agent-browser click "button.primary"
|
||||
agent-browser click "//button[@type='submit']"
|
||||
"##,
|
||||
"dblclick" => r##"
|
||||
"##
|
||||
}
|
||||
"dblclick" => {
|
||||
r##"
|
||||
agent-browser dblclick - Double-click an element
|
||||
|
||||
Usage: agent-browser dblclick <selector>
|
||||
@@ -342,8 +353,10 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser dblclick "#editable-text"
|
||||
agent-browser dblclick @e5
|
||||
"##,
|
||||
"fill" => r##"
|
||||
"##
|
||||
}
|
||||
"fill" => {
|
||||
r##"
|
||||
agent-browser fill - Clear and fill an input field
|
||||
|
||||
Usage: agent-browser fill <selector> <text>
|
||||
@@ -359,8 +372,10 @@ Examples:
|
||||
agent-browser fill "#email" "user@example.com"
|
||||
agent-browser fill @e3 "Hello World"
|
||||
agent-browser fill "input[name='search']" "query"
|
||||
"##,
|
||||
"type" => r##"
|
||||
"##
|
||||
}
|
||||
"type" => {
|
||||
r##"
|
||||
agent-browser type - Type text into an element
|
||||
|
||||
Usage: agent-browser type <selector> <text>
|
||||
@@ -375,8 +390,10 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser type "#search" "hello"
|
||||
agent-browser type @e2 "additional text"
|
||||
"##,
|
||||
"hover" => r##"
|
||||
"##
|
||||
}
|
||||
"hover" => {
|
||||
r##"
|
||||
agent-browser hover - Hover over an element
|
||||
|
||||
Usage: agent-browser hover <selector>
|
||||
@@ -391,8 +408,10 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser hover "#dropdown-trigger"
|
||||
agent-browser hover @e4
|
||||
"##,
|
||||
"focus" => r##"
|
||||
"##
|
||||
}
|
||||
"focus" => {
|
||||
r##"
|
||||
agent-browser focus - Focus an element
|
||||
|
||||
Usage: agent-browser focus <selector>
|
||||
@@ -406,8 +425,10 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser focus "#input-field"
|
||||
agent-browser focus @e2
|
||||
"##,
|
||||
"check" => r##"
|
||||
"##
|
||||
}
|
||||
"check" => {
|
||||
r##"
|
||||
agent-browser check - Check a checkbox
|
||||
|
||||
Usage: agent-browser check <selector>
|
||||
@@ -421,8 +442,10 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser check "#terms-checkbox"
|
||||
agent-browser check @e7
|
||||
"##,
|
||||
"uncheck" => r##"
|
||||
"##
|
||||
}
|
||||
"uncheck" => {
|
||||
r##"
|
||||
agent-browser uncheck - Uncheck a checkbox
|
||||
|
||||
Usage: agent-browser uncheck <selector>
|
||||
@@ -436,8 +459,10 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser uncheck "#newsletter-opt-in"
|
||||
agent-browser uncheck @e8
|
||||
"##,
|
||||
"select" => r##"
|
||||
"##
|
||||
}
|
||||
"select" => {
|
||||
r##"
|
||||
agent-browser select - Select a dropdown option
|
||||
|
||||
Usage: agent-browser select <selector> <value...>
|
||||
@@ -452,8 +477,10 @@ Examples:
|
||||
agent-browser select "#country" "US"
|
||||
agent-browser select @e5 "option2"
|
||||
agent-browser select "#menu" "opt1" "opt2" "opt3"
|
||||
"##,
|
||||
"drag" => r##"
|
||||
"##
|
||||
}
|
||||
"drag" => {
|
||||
r##"
|
||||
agent-browser drag - Drag and drop
|
||||
|
||||
Usage: agent-browser drag <source> <target>
|
||||
@@ -467,8 +494,10 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser drag "#draggable" "#drop-zone"
|
||||
agent-browser drag @e1 @e2
|
||||
"##,
|
||||
"upload" => r##"
|
||||
"##
|
||||
}
|
||||
"upload" => {
|
||||
r##"
|
||||
agent-browser upload - Upload files
|
||||
|
||||
Usage: agent-browser upload <selector> <files...>
|
||||
@@ -482,10 +511,12 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser upload "#file-input" ./document.pdf
|
||||
agent-browser upload @e3 ./image1.png ./image2.png
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Keyboard ===
|
||||
"press" | "key" => r##"
|
||||
"press" | "key" => {
|
||||
r##"
|
||||
agent-browser press - Press a key or key combination
|
||||
|
||||
Usage: agent-browser press <key>
|
||||
@@ -513,8 +544,10 @@ Examples:
|
||||
agent-browser press Control+a
|
||||
agent-browser press Control+Shift+s
|
||||
agent-browser press Escape
|
||||
"##,
|
||||
"keydown" => r##"
|
||||
"##
|
||||
}
|
||||
"keydown" => {
|
||||
r##"
|
||||
agent-browser keydown - Press a key down (without release)
|
||||
|
||||
Usage: agent-browser keydown <key>
|
||||
@@ -529,8 +562,10 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser keydown Shift
|
||||
agent-browser keydown Control
|
||||
"##,
|
||||
"keyup" => r##"
|
||||
"##
|
||||
}
|
||||
"keyup" => {
|
||||
r##"
|
||||
agent-browser keyup - Release a key
|
||||
|
||||
Usage: agent-browser keyup <key>
|
||||
@@ -544,10 +579,12 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser keyup Shift
|
||||
agent-browser keyup Control
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Scroll ===
|
||||
"scroll" => r##"
|
||||
"scroll" => {
|
||||
r##"
|
||||
agent-browser scroll - Scroll the page
|
||||
|
||||
Usage: agent-browser scroll [direction] [amount]
|
||||
@@ -567,8 +604,10 @@ Examples:
|
||||
agent-browser scroll down 500
|
||||
agent-browser scroll up 200
|
||||
agent-browser scroll left 100
|
||||
"##,
|
||||
"scrollintoview" | "scrollinto" => r##"
|
||||
"##
|
||||
}
|
||||
"scrollintoview" | "scrollinto" => {
|
||||
r##"
|
||||
agent-browser scrollintoview - Scroll element into view
|
||||
|
||||
Usage: agent-browser scrollintoview <selector>
|
||||
@@ -584,10 +623,12 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser scrollintoview "#footer"
|
||||
agent-browser scrollintoview @e15
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Wait ===
|
||||
"wait" => r##"
|
||||
"wait" => {
|
||||
r##"
|
||||
agent-browser wait - Wait for condition
|
||||
|
||||
Usage: agent-browser wait <selector|ms|option>
|
||||
@@ -613,10 +654,12 @@ Examples:
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser wait --fn "window.appReady === true"
|
||||
agent-browser wait --text "Welcome back"
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Screenshot/PDF ===
|
||||
"screenshot" => r##"
|
||||
"screenshot" => {
|
||||
r##"
|
||||
agent-browser screenshot - Take a screenshot
|
||||
|
||||
Usage: agent-browser screenshot [path]
|
||||
@@ -635,8 +678,10 @@ Examples:
|
||||
agent-browser screenshot
|
||||
agent-browser screenshot ./screenshot.png
|
||||
agent-browser screenshot --full ./full-page.png
|
||||
"##,
|
||||
"pdf" => r##"
|
||||
"##
|
||||
}
|
||||
"pdf" => {
|
||||
r##"
|
||||
agent-browser pdf - Save page as PDF
|
||||
|
||||
Usage: agent-browser pdf <path>
|
||||
@@ -650,10 +695,12 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser pdf ./page.pdf
|
||||
agent-browser pdf ~/Documents/report.pdf
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Snapshot ===
|
||||
"snapshot" => r##"
|
||||
"snapshot" => {
|
||||
r##"
|
||||
agent-browser snapshot - Get accessibility tree snapshot
|
||||
|
||||
Usage: agent-browser snapshot [options]
|
||||
@@ -677,10 +724,12 @@ Examples:
|
||||
agent-browser snapshot -i
|
||||
agent-browser snapshot --compact --depth 5
|
||||
agent-browser snapshot -s "#main-content"
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Eval ===
|
||||
"eval" => r##"
|
||||
"eval" => {
|
||||
r##"
|
||||
agent-browser eval - Execute JavaScript
|
||||
|
||||
Usage: agent-browser eval <script>
|
||||
@@ -695,10 +744,12 @@ Examples:
|
||||
agent-browser eval "document.title"
|
||||
agent-browser eval "window.location.href"
|
||||
agent-browser eval "document.querySelectorAll('a').length"
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Close ===
|
||||
"close" | "quit" | "exit" => r##"
|
||||
"close" | "quit" | "exit" => {
|
||||
r##"
|
||||
agent-browser close - Close the browser
|
||||
|
||||
Usage: agent-browser close
|
||||
@@ -714,10 +765,12 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser close
|
||||
agent-browser close --session mysession
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Get ===
|
||||
"get" => r##"
|
||||
"get" => {
|
||||
r##"
|
||||
agent-browser get - Retrieve information from elements or page
|
||||
|
||||
Usage: agent-browser get <subcommand> [args]
|
||||
@@ -750,10 +803,12 @@ Examples:
|
||||
agent-browser get box "#header"
|
||||
agent-browser get styles "button"
|
||||
agent-browser get styles @e1
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Is ===
|
||||
"is" => r##"
|
||||
"is" => {
|
||||
r##"
|
||||
agent-browser is - Check element state
|
||||
|
||||
Usage: agent-browser is <subcommand> <selector>
|
||||
@@ -773,10 +828,12 @@ Examples:
|
||||
agent-browser is visible "#modal"
|
||||
agent-browser is enabled "#submit-btn"
|
||||
agent-browser is checked "#agree-checkbox"
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Find ===
|
||||
"find" => r##"
|
||||
"find" => {
|
||||
r##"
|
||||
agent-browser find - Find and interact with elements by locator
|
||||
|
||||
Usage: agent-browser find <locator> <value> [action] [text]
|
||||
@@ -814,10 +871,12 @@ Examples:
|
||||
agent-browser find testid "login-form" click
|
||||
agent-browser find first "li.item" click
|
||||
agent-browser find nth 2 ".card" hover
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Mouse ===
|
||||
"mouse" => r##"
|
||||
"mouse" => {
|
||||
r##"
|
||||
agent-browser mouse - Low-level mouse operations
|
||||
|
||||
Usage: agent-browser mouse <subcommand> [args]
|
||||
@@ -841,10 +900,12 @@ Examples:
|
||||
agent-browser mouse down right
|
||||
agent-browser mouse wheel 100
|
||||
agent-browser mouse wheel -50 0
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Set ===
|
||||
"set" => r##"
|
||||
"set" => {
|
||||
r##"
|
||||
agent-browser set - Configure browser settings
|
||||
|
||||
Usage: agent-browser set <setting> [args]
|
||||
@@ -874,10 +935,12 @@ Examples:
|
||||
agent-browser set credentials admin secret123
|
||||
agent-browser set media dark
|
||||
agent-browser set media light reduced-motion
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Network ===
|
||||
"network" => r##"
|
||||
"network" => {
|
||||
r##"
|
||||
agent-browser network - Network interception and monitoring
|
||||
|
||||
Usage: agent-browser network <subcommand> [args]
|
||||
@@ -904,10 +967,12 @@ Examples:
|
||||
agent-browser network requests
|
||||
agent-browser network requests --filter "api"
|
||||
agent-browser network requests --clear
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Storage ===
|
||||
"storage" => r##"
|
||||
"storage" => {
|
||||
r##"
|
||||
agent-browser storage - Manage web storage
|
||||
|
||||
Usage: agent-browser storage <type> [operation] [key] [value]
|
||||
@@ -933,10 +998,12 @@ Examples:
|
||||
agent-browser storage local set theme "dark"
|
||||
agent-browser storage local clear
|
||||
agent-browser storage session get userId
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Cookies ===
|
||||
"cookies" => r##"
|
||||
"cookies" => {
|
||||
r##"
|
||||
agent-browser cookies - Manage browser cookies
|
||||
|
||||
Usage: agent-browser cookies [operation] [args]
|
||||
@@ -957,10 +1024,12 @@ Examples:
|
||||
agent-browser cookies get
|
||||
agent-browser cookies set session_id "abc123"
|
||||
agent-browser cookies clear
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Tabs ===
|
||||
"tab" => r##"
|
||||
"tab" => {
|
||||
r##"
|
||||
agent-browser tab - Manage browser tabs
|
||||
|
||||
Usage: agent-browser tab [operation] [args]
|
||||
@@ -985,10 +1054,12 @@ Examples:
|
||||
agent-browser tab 2
|
||||
agent-browser tab close
|
||||
agent-browser tab close 1
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Window ===
|
||||
"window" => r##"
|
||||
"window" => {
|
||||
r##"
|
||||
agent-browser window - Manage browser windows
|
||||
|
||||
Usage: agent-browser window <operation>
|
||||
@@ -1004,10 +1075,12 @@ Global Options:
|
||||
|
||||
Examples:
|
||||
agent-browser window new
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Frame ===
|
||||
"frame" => r##"
|
||||
"frame" => {
|
||||
r##"
|
||||
agent-browser frame - Switch frame context
|
||||
|
||||
Usage: agent-browser frame <selector|main>
|
||||
@@ -1026,10 +1099,12 @@ Examples:
|
||||
agent-browser frame "#embed-iframe"
|
||||
agent-browser frame "iframe[name='content']"
|
||||
agent-browser frame main
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Dialog ===
|
||||
"dialog" => r##"
|
||||
"dialog" => {
|
||||
r##"
|
||||
agent-browser dialog - Handle browser dialogs
|
||||
|
||||
Usage: agent-browser dialog <response> [text]
|
||||
@@ -1048,10 +1123,12 @@ Examples:
|
||||
agent-browser dialog accept
|
||||
agent-browser dialog accept "my input"
|
||||
agent-browser dialog dismiss
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Trace ===
|
||||
"trace" => r##"
|
||||
"trace" => {
|
||||
r##"
|
||||
agent-browser trace - Record execution trace
|
||||
|
||||
Usage: agent-browser trace <operation> [path]
|
||||
@@ -1071,7 +1148,8 @@ Examples:
|
||||
agent-browser trace start ./my-trace
|
||||
agent-browser trace stop
|
||||
agent-browser trace stop ./debug-trace.zip
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Record (video) ===
|
||||
"record" => r##"
|
||||
@@ -1110,7 +1188,8 @@ Examples:
|
||||
"##,
|
||||
|
||||
// === Console/Errors ===
|
||||
"console" => r##"
|
||||
"console" => {
|
||||
r##"
|
||||
agent-browser console - View console logs
|
||||
|
||||
Usage: agent-browser console [--clear]
|
||||
@@ -1127,8 +1206,10 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser console
|
||||
agent-browser console --clear
|
||||
"##,
|
||||
"errors" => r##"
|
||||
"##
|
||||
}
|
||||
"errors" => {
|
||||
r##"
|
||||
agent-browser errors - View page errors
|
||||
|
||||
Usage: agent-browser errors [--clear]
|
||||
@@ -1145,10 +1226,12 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser errors
|
||||
agent-browser errors --clear
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Highlight ===
|
||||
"highlight" => r##"
|
||||
"highlight" => {
|
||||
r##"
|
||||
agent-browser highlight - Highlight an element
|
||||
|
||||
Usage: agent-browser highlight <selector>
|
||||
@@ -1162,10 +1245,12 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser highlight "#target-element"
|
||||
agent-browser highlight @e5
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === State ===
|
||||
"state" => r##"
|
||||
"state" => {
|
||||
r##"
|
||||
agent-browser state - Save/load browser state
|
||||
|
||||
Usage: agent-browser state <operation> <path>
|
||||
@@ -1183,10 +1268,12 @@ Global Options:
|
||||
Examples:
|
||||
agent-browser state save ./auth-state.json
|
||||
agent-browser state load ./auth-state.json
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Session ===
|
||||
"session" => r##"
|
||||
"session" => {
|
||||
r##"
|
||||
agent-browser session - Manage sessions
|
||||
|
||||
Usage: agent-browser session [operation]
|
||||
@@ -1209,10 +1296,12 @@ Examples:
|
||||
agent-browser session
|
||||
agent-browser session list
|
||||
agent-browser --session test open example.com
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
// === Install ===
|
||||
"install" => r##"
|
||||
"install" => {
|
||||
r##"
|
||||
agent-browser install - Install browser binaries
|
||||
|
||||
Usage: agent-browser install [--with-deps]
|
||||
@@ -1225,7 +1314,8 @@ Options:
|
||||
Examples:
|
||||
agent-browser install
|
||||
agent-browser install --with-deps
|
||||
"##,
|
||||
"##
|
||||
}
|
||||
|
||||
_ => return false,
|
||||
};
|
||||
@@ -1324,8 +1414,14 @@ Options:
|
||||
--session <name> Isolated session (or AGENT_BROWSER_SESSION env)
|
||||
--headers <json> HTTP headers scoped to URL's origin (for auth)
|
||||
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
|
||||
--extension <path> Load browser extensions (repeatable).
|
||||
--proxy <url> Proxy server (http://[user:pass@]host:port)
|
||||
--extension <path> Load browser extensions (repeatable)
|
||||
--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
|
||||
--full, -f Full page screenshot
|
||||
--headed Show browser window (not headless)
|
||||
|
||||
+7
-1
@@ -865,14 +865,18 @@ export class BrowserManager {
|
||||
if (hasExtensions) {
|
||||
const extPaths = options.extensions!.join(',');
|
||||
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(
|
||||
path.join(os.tmpdir(), `agent-browser-ext-${session}`),
|
||||
{
|
||||
headless: false,
|
||||
executablePath: options.executablePath,
|
||||
args: [`--disable-extensions-except=${extPaths}`, `--load-extension=${extPaths}`],
|
||||
args: allArgs,
|
||||
viewport,
|
||||
extraHTTPHeaders: options.headers,
|
||||
userAgent: options.userAgent,
|
||||
...(options.proxy && { proxy: options.proxy }),
|
||||
}
|
||||
);
|
||||
@@ -881,11 +885,13 @@ export class BrowserManager {
|
||||
this.browser = await launcher.launch({
|
||||
headless: options.headless ?? true,
|
||||
executablePath: options.executablePath,
|
||||
args: options.args,
|
||||
});
|
||||
this.cdpEndpoint = null;
|
||||
context = await this.browser.newContext({
|
||||
viewport,
|
||||
extraHTTPHeaders: options.headers,
|
||||
userAgent: options.userAgent,
|
||||
...(options.proxy && { proxy: options.proxy }),
|
||||
});
|
||||
}
|
||||
|
||||
+24
-1
@@ -197,12 +197,35 @@ export async function startDaemon(options?: { streamPort?: number }): Promise<vo
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean)
|
||||
: 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({
|
||||
id: 'auto',
|
||||
action: 'launch',
|
||||
action: 'launch' as const,
|
||||
headless: process.env.AGENT_BROWSER_HEADED !== '1',
|
||||
executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH,
|
||||
extensions: extensions,
|
||||
args,
|
||||
userAgent: process.env.AGENT_BROWSER_USER_AGENT,
|
||||
proxy,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ const launchSchema = baseCommandSchema.extend({
|
||||
password: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
userAgent: z.string().optional(),
|
||||
provider: z.string().optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface LaunchCommand extends BaseCommand {
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
args?: string[];
|
||||
userAgent?: 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