add iOS support (#358)
* ios * tests * docs * real device * better list * fixes
This commit is contained in:
@@ -103,6 +103,12 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
nav_cmd["headers"] = headers;
|
||||
}
|
||||
}
|
||||
// Include iOS device info if specified (needed for auto-launch with existing daemon)
|
||||
if flags.provider.as_deref() == Some("ios") {
|
||||
if let Some(ref device) = flags.device {
|
||||
nav_cmd["iosDevice"] = json!(device);
|
||||
}
|
||||
}
|
||||
Ok(nav_cmd)
|
||||
}
|
||||
"back" => Ok(json!({ "id": id, "action": "back" })),
|
||||
@@ -835,6 +841,48 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
}
|
||||
}
|
||||
|
||||
// === iOS-specific commands ===
|
||||
"tap" => {
|
||||
// Alias for click (semantic clarity for touch interfaces)
|
||||
let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments {
|
||||
context: "tap".to_string(),
|
||||
usage: "tap <selector>",
|
||||
})?;
|
||||
Ok(json!({ "id": id, "action": "tap", "selector": sel }))
|
||||
}
|
||||
"swipe" => {
|
||||
let direction = rest.get(0).ok_or_else(|| ParseError::MissingArguments {
|
||||
context: "swipe".to_string(),
|
||||
usage: "swipe <up|down|left|right> [distance]",
|
||||
})?;
|
||||
let valid_directions = ["up", "down", "left", "right"];
|
||||
if !valid_directions.contains(direction) {
|
||||
return Err(ParseError::InvalidValue {
|
||||
message: format!("Invalid swipe direction: {}", direction),
|
||||
usage: "swipe <up|down|left|right> [distance]",
|
||||
});
|
||||
}
|
||||
let mut cmd = json!({ "id": id, "action": "swipe", "direction": direction });
|
||||
if let Some(distance) = rest.get(1) {
|
||||
if let Ok(d) = distance.parse::<u32>() {
|
||||
cmd.as_object_mut().unwrap().insert("distance".to_string(), json!(d));
|
||||
}
|
||||
}
|
||||
Ok(cmd)
|
||||
}
|
||||
"device" => {
|
||||
match rest.get(0).map(|s| *s) {
|
||||
Some("list") | None => {
|
||||
// List available iOS simulators
|
||||
Ok(json!({ "id": id, "action": "device_list" }))
|
||||
}
|
||||
Some(sub) => Err(ParseError::UnknownSubcommand {
|
||||
subcommand: sub.to_string(),
|
||||
valid_options: &["list"],
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
_ => Err(ParseError::UnknownCommand {
|
||||
command: cmd.to_string(),
|
||||
}),
|
||||
@@ -1376,6 +1424,7 @@ mod tests {
|
||||
user_agent: None,
|
||||
provider: None,
|
||||
ignore_https_errors: false,
|
||||
device: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -215,6 +215,8 @@ pub fn ensure_daemon(
|
||||
ignore_https_errors: bool,
|
||||
profile: Option<&str>,
|
||||
state: Option<&str>,
|
||||
provider: Option<&str>,
|
||||
device: Option<&str>,
|
||||
) -> Result<DaemonResult, String> {
|
||||
// Check if daemon is running AND responsive
|
||||
if is_daemon_running(session) && daemon_ready(session) {
|
||||
@@ -343,6 +345,14 @@ pub fn ensure_daemon(
|
||||
cmd.env("AGENT_BROWSER_STATE", st);
|
||||
}
|
||||
|
||||
if let Some(p) = provider {
|
||||
cmd.env("AGENT_BROWSER_PROVIDER", p);
|
||||
}
|
||||
|
||||
if let Some(d) = device {
|
||||
cmd.env("AGENT_BROWSER_IOS_DEVICE", d);
|
||||
}
|
||||
|
||||
// Create new process group and session to fully detach
|
||||
unsafe {
|
||||
cmd.pre_exec(|| {
|
||||
@@ -410,6 +420,14 @@ pub fn ensure_daemon(
|
||||
cmd.env("AGENT_BROWSER_STATE", st);
|
||||
}
|
||||
|
||||
if let Some(p) = provider {
|
||||
cmd.env("AGENT_BROWSER_PROVIDER", p);
|
||||
}
|
||||
|
||||
if let Some(d) = device {
|
||||
cmd.env("AGENT_BROWSER_IOS_DEVICE", d);
|
||||
}
|
||||
|
||||
// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
|
||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
||||
const DETACHED_PROCESS: u32 = 0x00000008;
|
||||
|
||||
@@ -18,6 +18,7 @@ pub struct Flags {
|
||||
pub user_agent: Option<String>,
|
||||
pub provider: Option<String>,
|
||||
pub ignore_https_errors: bool,
|
||||
pub device: Option<String>,
|
||||
}
|
||||
|
||||
pub fn parse_flags(args: &[String]) -> Flags {
|
||||
@@ -49,6 +50,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
user_agent: env::var("AGENT_BROWSER_USER_AGENT").ok(),
|
||||
provider: env::var("AGENT_BROWSER_PROVIDER").ok(),
|
||||
ignore_https_errors: false,
|
||||
device: env::var("AGENT_BROWSER_IOS_DEVICE").ok(),
|
||||
};
|
||||
|
||||
let mut i = 0;
|
||||
@@ -131,6 +133,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
}
|
||||
}
|
||||
"--ignore-https-errors" => flags.ignore_https_errors = true,
|
||||
"--device" => {
|
||||
if let Some(d) = args.get(i + 1) {
|
||||
flags.device = Some(d.clone());
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
@@ -165,6 +173,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
||||
"--user-agent",
|
||||
"-p",
|
||||
"--provider",
|
||||
"--device",
|
||||
];
|
||||
|
||||
for arg in args.iter() {
|
||||
|
||||
@@ -207,6 +207,8 @@ fn main() {
|
||||
flags.ignore_https_errors,
|
||||
flags.profile.as_deref(),
|
||||
flags.state.as_deref(),
|
||||
flags.provider.as_deref(),
|
||||
flags.device.as_deref(),
|
||||
) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
|
||||
+121
-2
@@ -78,6 +78,53 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
|
||||
);
|
||||
return;
|
||||
}
|
||||
// iOS Devices
|
||||
if let Some(devices) = data.get("devices").and_then(|v| v.as_array()) {
|
||||
if devices.is_empty() {
|
||||
println!("No iOS devices available. Open Xcode to download simulator runtimes.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Separate real devices from simulators
|
||||
let real_devices: Vec<_> = devices
|
||||
.iter()
|
||||
.filter(|d| d.get("isRealDevice").and_then(|v| v.as_bool()).unwrap_or(false))
|
||||
.collect();
|
||||
let simulators: Vec<_> = devices
|
||||
.iter()
|
||||
.filter(|d| !d.get("isRealDevice").and_then(|v| v.as_bool()).unwrap_or(false))
|
||||
.collect();
|
||||
|
||||
if !real_devices.is_empty() {
|
||||
println!("Connected Devices:\n");
|
||||
for device in real_devices.iter() {
|
||||
let name = device.get("name").and_then(|v| v.as_str()).unwrap_or("Unknown");
|
||||
let runtime = device.get("runtime").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let udid = device.get("udid").and_then(|v| v.as_str()).unwrap_or("");
|
||||
println!(" {} {} ({})", color::green("●"), name, runtime);
|
||||
println!(" {}", color::dim(udid));
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
if !simulators.is_empty() {
|
||||
println!("Simulators:\n");
|
||||
for device in simulators.iter() {
|
||||
let name = device.get("name").and_then(|v| v.as_str()).unwrap_or("Unknown");
|
||||
let runtime = device.get("runtime").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let state = device.get("state").and_then(|v| v.as_str()).unwrap_or("Unknown");
|
||||
let udid = device.get("udid").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let state_indicator = if state == "Booted" {
|
||||
color::green("●")
|
||||
} else {
|
||||
color::dim("○")
|
||||
};
|
||||
println!(" {} {} ({})", state_indicator, name, runtime);
|
||||
println!(" {}", color::dim(udid));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Tabs
|
||||
if let Some(tabs) = data.get("tabs").and_then(|v| v.as_array()) {
|
||||
for (i, tab) in tabs.iter().enumerate() {
|
||||
@@ -1551,6 +1598,68 @@ Examples:
|
||||
"##
|
||||
}
|
||||
|
||||
// === iOS Commands ===
|
||||
"tap" => {
|
||||
r##"
|
||||
agent-browser tap - Tap an element (touch gesture)
|
||||
|
||||
Usage: agent-browser tap <selector>
|
||||
|
||||
Taps an element. This is an alias for 'click' that provides semantic clarity
|
||||
for touch-based interfaces like iOS Safari.
|
||||
|
||||
Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser tap "#submit-button"
|
||||
agent-browser tap @e1
|
||||
agent-browser -p ios tap "button:has-text('Sign In')"
|
||||
"##
|
||||
}
|
||||
"swipe" => {
|
||||
r##"
|
||||
agent-browser swipe - Swipe gesture (iOS)
|
||||
|
||||
Usage: agent-browser swipe <direction> [distance]
|
||||
|
||||
Performs a swipe gesture on iOS Safari. The direction determines
|
||||
which way the content moves (swipe up scrolls down, etc.).
|
||||
|
||||
Arguments:
|
||||
direction up, down, left, or right
|
||||
distance Optional distance in pixels (default: 300)
|
||||
|
||||
Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser -p ios swipe up
|
||||
agent-browser -p ios swipe down 500
|
||||
agent-browser -p ios swipe left
|
||||
"##
|
||||
}
|
||||
"device" => {
|
||||
r##"
|
||||
agent-browser device - Manage iOS simulators
|
||||
|
||||
Usage: agent-browser device <subcommand>
|
||||
|
||||
Subcommands:
|
||||
list List available iOS simulators
|
||||
|
||||
Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser device list
|
||||
agent-browser -p ios device list
|
||||
"##
|
||||
}
|
||||
|
||||
_ => return false,
|
||||
};
|
||||
println!("{}", help.trim());
|
||||
@@ -1660,7 +1769,8 @@ Options:
|
||||
--proxy-bypass <hosts> Bypass proxy for these hosts (or AGENT_BROWSER_PROXY_BYPASS)
|
||||
e.g., --proxy-bypass "localhost,*.internal.com"
|
||||
--ignore-https-errors Ignore HTTPS certificate errors
|
||||
-p, --provider <name> Cloud browser provider (or AGENT_BROWSER_PROVIDER env)
|
||||
-p, --provider <name> Browser provider: ios, browserbase, kernel, browseruse
|
||||
--device <name> iOS device name (e.g., "iPhone 15 Pro")
|
||||
--json JSON output
|
||||
--full, -f Full page screenshot
|
||||
--headed Show browser window (not headless)
|
||||
@@ -1671,8 +1781,10 @@ Options:
|
||||
Environment:
|
||||
AGENT_BROWSER_SESSION Session name (default: "default")
|
||||
AGENT_BROWSER_EXECUTABLE_PATH Custom browser executable path
|
||||
AGENT_BROWSER_PROVIDER Cloud browser provider
|
||||
AGENT_BROWSER_PROVIDER Browser provider (ios, browserbase, kernel, browseruse)
|
||||
AGENT_BROWSER_STREAM_PORT Enable WebSocket streaming on port (e.g., 9223)
|
||||
AGENT_BROWSER_IOS_DEVICE Default iOS device name
|
||||
AGENT_BROWSER_IOS_UDID Default iOS device UDID
|
||||
|
||||
Examples:
|
||||
agent-browser open example.com
|
||||
@@ -1684,6 +1796,13 @@ Examples:
|
||||
agent-browser screenshot --full
|
||||
agent-browser --cdp 9222 snapshot # Connect via CDP port
|
||||
agent-browser --profile ~/.myapp open example.com # Persistent profile
|
||||
|
||||
iOS Simulator (requires Xcode and Appium):
|
||||
agent-browser -p ios open example.com # Use default iPhone
|
||||
agent-browser -p ios --device "iPhone 15 Pro" open url # Specific device
|
||||
agent-browser -p ios device list # List simulators
|
||||
agent-browser -p ios swipe up # Swipe gesture
|
||||
agent-browser -p ios tap @e1 # Touch element
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user