Consistent Tab IDs & Global Tag Targeting (#892)
Introduces stable per-tab IDs and a global `--tab <id>` flag for scoping individual commands to a specific tab.
Breaking change: response payloads for `tab_list`, `tab_new`, `tab_switch`, `tab_close`, and `window_new` now use `tabId` instead of `index`. `tab_close` returns `{tabId, closed: true}` instead of `{closed, activeIndex}`. `agent-browser tab <unknown>` now errors instead of silently listing tabs.
Follow-up PR to land immediately after this fixes a compile error on the provider direct-page path, clears per-tab daemon state around scoped switches, and implements active-tab restoration so `--tab N` is non-intrusive as intended.
This commit is contained in:
+64
-20
@@ -1010,28 +1010,35 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// === Tabs ===
|
// === Tabs ===
|
||||||
"tab" => match rest.first().copied() {
|
"tab" => {
|
||||||
Some("new") => {
|
const VALID: &[&str] = &["list", "new", "close", "<id>"];
|
||||||
let mut cmd = json!({ "id": id, "action": "tab_new" });
|
match rest.first().copied() {
|
||||||
if let Some(url) = rest.get(1) {
|
Some("new") => {
|
||||||
cmd["url"] = json!(url);
|
let mut cmd = json!({ "id": id, "action": "tab_new" });
|
||||||
|
if let Some(url) = rest.get(1) {
|
||||||
|
cmd["url"] = json!(url);
|
||||||
|
}
|
||||||
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
Ok(cmd)
|
Some("list") => Ok(json!({ "id": id, "action": "tab_list" })),
|
||||||
}
|
Some("close") => {
|
||||||
Some("list") => Ok(json!({ "id": id, "action": "tab_list" })),
|
let mut cmd = json!({ "id": id, "action": "tab_close" });
|
||||||
Some("close") => {
|
if let Some(tab_id) = rest.get(1).and_then(|s| s.parse::<i32>().ok()) {
|
||||||
let mut cmd = json!({ "id": id, "action": "tab_close" });
|
cmd["tabId"] = json!(tab_id);
|
||||||
if let Some(index) = rest.get(1).and_then(|s| s.parse::<i32>().ok()) {
|
}
|
||||||
cmd["index"] = json!(index);
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
Ok(cmd)
|
Some(n) if n.parse::<i32>().is_ok() => {
|
||||||
|
let tab_id = n.parse::<i32>().expect("already checked parse succeeds");
|
||||||
|
Ok(json!({ "id": id, "action": "tab_switch", "tabId": tab_id }))
|
||||||
|
}
|
||||||
|
Some(sub) => Err(ParseError::UnknownSubcommand {
|
||||||
|
subcommand: sub.to_string(),
|
||||||
|
valid_options: VALID,
|
||||||
|
}),
|
||||||
|
None => Ok(json!({ "id": id, "action": "tab_list" })),
|
||||||
}
|
}
|
||||||
Some(n) if n.parse::<i32>().is_ok() => {
|
}
|
||||||
let index = n.parse::<i32>().expect("already checked parse succeeds");
|
|
||||||
Ok(json!({ "id": id, "action": "tab_switch", "index": index }))
|
|
||||||
}
|
|
||||||
_ => Ok(json!({ "id": id, "action": "tab_list" })),
|
|
||||||
},
|
|
||||||
|
|
||||||
// === Window ===
|
// === Window ===
|
||||||
"window" => {
|
"window" => {
|
||||||
@@ -2335,6 +2342,7 @@ mod tests {
|
|||||||
fn default_flags() -> Flags {
|
fn default_flags() -> Flags {
|
||||||
Flags {
|
Flags {
|
||||||
session: "test".to_string(),
|
session: "test".to_string(),
|
||||||
|
tab: None,
|
||||||
json: false,
|
json: false,
|
||||||
headed: false,
|
headed: false,
|
||||||
debug: false,
|
debug: false,
|
||||||
@@ -2847,7 +2855,7 @@ mod tests {
|
|||||||
fn test_tab_switch() {
|
fn test_tab_switch() {
|
||||||
let cmd = parse_command(&args("tab 2"), &default_flags()).unwrap();
|
let cmd = parse_command(&args("tab 2"), &default_flags()).unwrap();
|
||||||
assert_eq!(cmd["action"], "tab_switch");
|
assert_eq!(cmd["action"], "tab_switch");
|
||||||
assert_eq!(cmd["index"], 2);
|
assert_eq!(cmd["tabId"], 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2856,6 +2864,42 @@ mod tests {
|
|||||||
assert_eq!(cmd["action"], "tab_close");
|
assert_eq!(cmd["action"], "tab_close");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tab_close_with_id() {
|
||||||
|
let cmd = parse_command(&args("tab close 2"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "tab_close");
|
||||||
|
assert_eq!(cmd["tabId"], 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tab_switch_sends_tab_id() {
|
||||||
|
let cmd = parse_command(&args("tab 2"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["tabId"], 2);
|
||||||
|
assert!(cmd.get("index").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tab_close_sends_tab_id() {
|
||||||
|
let cmd = parse_command(&args("tab close 3"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["tabId"], 3);
|
||||||
|
assert!(cmd.get("index").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tab_no_args_defaults_to_list() {
|
||||||
|
let cmd = parse_command(&args("tab"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "tab_list");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tab_unknown_subcommand_errors() {
|
||||||
|
let result = parse_command(&args("tab select 3"), &default_flags());
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"tab select should error, not silently fall through to tab_list"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// === Network ===
|
// === Network ===
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ pub struct Config {
|
|||||||
pub json: Option<bool>,
|
pub json: Option<bool>,
|
||||||
pub debug: Option<bool>,
|
pub debug: Option<bool>,
|
||||||
pub session: Option<String>,
|
pub session: Option<String>,
|
||||||
|
pub tab: Option<u32>,
|
||||||
pub session_name: Option<String>,
|
pub session_name: Option<String>,
|
||||||
pub executable_path: Option<String>,
|
pub executable_path: Option<String>,
|
||||||
pub extensions: Option<Vec<String>>,
|
pub extensions: Option<Vec<String>>,
|
||||||
@@ -98,6 +99,7 @@ impl Config {
|
|||||||
json: other.json.or(self.json),
|
json: other.json.or(self.json),
|
||||||
debug: other.debug.or(self.debug),
|
debug: other.debug.or(self.debug),
|
||||||
session: other.session.or(self.session),
|
session: other.session.or(self.session),
|
||||||
|
tab: other.tab.or(self.tab),
|
||||||
session_name: other.session_name.or(self.session_name),
|
session_name: other.session_name.or(self.session_name),
|
||||||
executable_path: other.executable_path.or(self.executable_path),
|
executable_path: other.executable_path.or(self.executable_path),
|
||||||
extensions: match (self.extensions, other.extensions) {
|
extensions: match (self.extensions, other.extensions) {
|
||||||
@@ -196,6 +198,7 @@ fn parse_bool_arg(args: &[String], i: usize) -> (bool, bool) {
|
|||||||
fn extract_config_path(args: &[String]) -> Option<Option<String>> {
|
fn extract_config_path(args: &[String]) -> Option<Option<String>> {
|
||||||
const FLAGS_WITH_VALUE: &[&str] = &[
|
const FLAGS_WITH_VALUE: &[&str] = &[
|
||||||
"--session",
|
"--session",
|
||||||
|
"--tab",
|
||||||
"--headers",
|
"--headers",
|
||||||
"--executable-path",
|
"--executable-path",
|
||||||
"--cdp",
|
"--cdp",
|
||||||
@@ -273,6 +276,7 @@ pub struct Flags {
|
|||||||
pub headed: bool,
|
pub headed: bool,
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
pub session: String,
|
pub session: String,
|
||||||
|
pub tab: Option<u32>,
|
||||||
pub headers: Option<String>,
|
pub headers: Option<String>,
|
||||||
pub executable_path: Option<String>,
|
pub executable_path: Option<String>,
|
||||||
pub cdp: Option<String>,
|
pub cdp: Option<String>,
|
||||||
@@ -355,6 +359,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
.ok()
|
.ok()
|
||||||
.or(config.session)
|
.or(config.session)
|
||||||
.unwrap_or_else(|| "default".to_string()),
|
.unwrap_or_else(|| "default".to_string()),
|
||||||
|
tab: config.tab,
|
||||||
headers: config.headers,
|
headers: config.headers,
|
||||||
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH")
|
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH")
|
||||||
.ok()
|
.ok()
|
||||||
@@ -492,6 +497,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"--tab" => {
|
||||||
|
if let Some(s) = args.get(i + 1) {
|
||||||
|
flags.tab = s.parse::<u32>().ok();
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
"--idle-timeout" => {
|
"--idle-timeout" => {
|
||||||
if let Some(s) = args.get(i + 1) {
|
if let Some(s) = args.get(i + 1) {
|
||||||
match parse_idle_timeout(s) {
|
match parse_idle_timeout(s) {
|
||||||
@@ -775,6 +786,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
|||||||
// Global flags that always take a value (need to skip the next arg too)
|
// Global flags that always take a value (need to skip the next arg too)
|
||||||
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[
|
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[
|
||||||
"--session",
|
"--session",
|
||||||
|
"--tab",
|
||||||
"--headers",
|
"--headers",
|
||||||
"--executable-path",
|
"--executable-path",
|
||||||
"--cdp",
|
"--cdp",
|
||||||
@@ -1441,4 +1453,25 @@ mod tests {
|
|||||||
let clean = clean_args(&input);
|
let clean = clean_args(&input);
|
||||||
assert_eq!(clean, vec!["open", "example.com"]);
|
assert_eq!(clean, vec!["open", "example.com"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Tab flag tests ===
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_tab_flag() {
|
||||||
|
let flags = parse_flags(&args("--tab 4 snapshot"));
|
||||||
|
assert_eq!(flags.tab, Some(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clean_args_removes_tab_flag() {
|
||||||
|
let cleaned = clean_args(&args("--tab 4 snapshot"));
|
||||||
|
assert_eq!(cleaned, vec!["snapshot"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_tab_config() {
|
||||||
|
let json = r#"{"tab": 4}"#;
|
||||||
|
let config: Config = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(config.tab, Some(4));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -738,6 +738,12 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(tab_id) = flags.tab {
|
||||||
|
if cmd.get("tabId").is_none() {
|
||||||
|
cmd["tabId"] = json!(tab_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle --password-stdin for auth save
|
// Handle --password-stdin for auth save
|
||||||
if cmd.get("action").and_then(|v| v.as_str()) == Some("auth_save") {
|
if cmd.get("action").and_then(|v| v.as_str()) == Some("auth_save") {
|
||||||
if cmd.get("password").is_some() {
|
if cmd.get("password").is_some() {
|
||||||
|
|||||||
+27
-10
@@ -662,7 +662,9 @@ impl DaemonState {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let tab_id = mgr.assign_tab_id();
|
||||||
mgr.add_page(super::browser::PageInfo {
|
mgr.add_page(super::browser::PageInfo {
|
||||||
|
tab_id,
|
||||||
target_id: te.target_info.target_id.clone(),
|
target_id: te.target_info.target_id.clone(),
|
||||||
session_id: attach.session_id,
|
session_id: attach.session_id,
|
||||||
url: te.target_info.url.clone(),
|
url: te.target_info.url.clone(),
|
||||||
@@ -1273,6 +1275,20 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-dispatch: if tabId is set on a non-tab command, switch to that tab first
|
||||||
|
if !matches!(
|
||||||
|
action,
|
||||||
|
"tab_list" | "tab_new" | "tab_switch" | "tab_close" | "launch" | "close"
|
||||||
|
) {
|
||||||
|
if let Some(tab_id) = cmd.get("tabId").and_then(|v| v.as_u64()) {
|
||||||
|
if let Some(ref mut mgr) = state.browser {
|
||||||
|
if let Err(e) = mgr.tab_switch_by_id(tab_id as u32).await {
|
||||||
|
return error_response(&id, &e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let result = match action {
|
let result = match action {
|
||||||
"launch" => handle_launch(cmd, state).await,
|
"launch" => handle_launch(cmd, state).await,
|
||||||
"navigate" => handle_navigate(cmd, state).await,
|
"navigate" => handle_navigate(cmd, state).await,
|
||||||
@@ -3647,14 +3663,14 @@ async fn handle_tab_new(cmd: &Value, state: &mut DaemonState) -> Result<Value, S
|
|||||||
|
|
||||||
async fn handle_tab_switch(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
async fn handle_tab_switch(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
||||||
let mgr = state.browser.as_mut().ok_or("Browser not launched")?;
|
let mgr = state.browser.as_mut().ok_or("Browser not launched")?;
|
||||||
let index = cmd
|
let tab_id = cmd
|
||||||
.get("index")
|
.get("tabId")
|
||||||
.and_then(|v| v.as_u64())
|
.and_then(|v| v.as_u64())
|
||||||
.ok_or("Missing 'index' parameter")? as usize;
|
.ok_or("Missing 'tabId' parameter")? as u32;
|
||||||
state.ref_map.clear();
|
state.ref_map.clear();
|
||||||
state.iframe_sessions.clear();
|
state.iframe_sessions.clear();
|
||||||
state.active_frame_id = None;
|
state.active_frame_id = None;
|
||||||
let result = mgr.tab_switch(index).await?;
|
let result = mgr.tab_switch_by_id(tab_id).await?;
|
||||||
|
|
||||||
if let Some(ref server) = state.stream_server {
|
if let Some(ref server) = state.stream_server {
|
||||||
if let Ok(dims) = mgr
|
if let Ok(dims) = mgr
|
||||||
@@ -3679,14 +3695,11 @@ async fn handle_tab_switch(cmd: &Value, state: &mut DaemonState) -> Result<Value
|
|||||||
|
|
||||||
async fn handle_tab_close(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
async fn handle_tab_close(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
||||||
let mgr = state.browser.as_mut().ok_or("Browser not launched")?;
|
let mgr = state.browser.as_mut().ok_or("Browser not launched")?;
|
||||||
let index = cmd
|
let tab_id = cmd.get("tabId").and_then(|v| v.as_u64()).map(|i| i as u32);
|
||||||
.get("index")
|
|
||||||
.and_then(|v| v.as_u64())
|
|
||||||
.map(|i| i as usize);
|
|
||||||
state.ref_map.clear();
|
state.ref_map.clear();
|
||||||
state.iframe_sessions.clear();
|
state.iframe_sessions.clear();
|
||||||
state.active_frame_id = None;
|
state.active_frame_id = None;
|
||||||
mgr.tab_close(index).await
|
mgr.tab_close_by_id(tab_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_viewport(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
async fn handle_viewport(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
||||||
@@ -4065,7 +4078,9 @@ async fn handle_recording_start(cmd: &Value, state: &mut DaemonState) -> Result<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add page and switch to it
|
// Add page and switch to it
|
||||||
|
let tab_id = mgr.assign_tab_id();
|
||||||
mgr.add_page(super::browser::PageInfo {
|
mgr.add_page(super::browser::PageInfo {
|
||||||
|
tab_id,
|
||||||
target_id: create_result.target_id,
|
target_id: create_result.target_id,
|
||||||
session_id: new_session_id.clone(),
|
session_id: new_session_id.clone(),
|
||||||
url: nav_url.clone(),
|
url: nav_url.clone(),
|
||||||
@@ -5976,7 +5991,9 @@ async fn handle_window_new(cmd: &Value, state: &mut DaemonState) -> Result<Value
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let tab_id = mgr.assign_tab_id();
|
||||||
mgr.add_page(super::browser::PageInfo {
|
mgr.add_page(super::browser::PageInfo {
|
||||||
|
tab_id,
|
||||||
target_id: create_result.target_id,
|
target_id: create_result.target_id,
|
||||||
session_id: attach.session_id,
|
session_id: attach.session_id,
|
||||||
url: "about:blank".to_string(),
|
url: "about:blank".to_string(),
|
||||||
@@ -6004,7 +6021,7 @@ async fn handle_window_new(cmd: &Value, state: &mut DaemonState) -> Result<Value
|
|||||||
let total = mgr.page_count();
|
let total = mgr.page_count();
|
||||||
state.ref_map.clear();
|
state.ref_map.clear();
|
||||||
|
|
||||||
Ok(json!({ "index": total - 1, "total": total }))
|
Ok(json!({ "tabId": tab_id, "total": total }))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_diff_screenshot(cmd: &Value, state: &DaemonState) -> Result<Value, String> {
|
async fn handle_diff_screenshot(cmd: &Value, state: &DaemonState) -> Result<Value, String> {
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ pub fn to_ai_friendly_error(error: &str) -> String {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PageInfo {
|
pub struct PageInfo {
|
||||||
|
pub tab_id: u32,
|
||||||
pub target_id: String,
|
pub target_id: String,
|
||||||
pub session_id: String,
|
pub session_id: String,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
@@ -226,6 +227,7 @@ pub struct BrowserManager {
|
|||||||
pub ignore_https_errors: bool,
|
pub ignore_https_errors: bool,
|
||||||
/// Origins visited during this session, used by save_state to collect cross-origin localStorage.
|
/// Origins visited during this session, used by save_state to collect cross-origin localStorage.
|
||||||
visited_origins: HashSet<String>,
|
visited_origins: HashSet<String>,
|
||||||
|
next_tab_id: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
const LIGHTPANDA_CDP_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
const LIGHTPANDA_CDP_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
@@ -297,6 +299,7 @@ impl BrowserManager {
|
|||||||
download_path: download_path.clone(),
|
download_path: download_path.clone(),
|
||||||
ignore_https_errors,
|
ignore_https_errors,
|
||||||
visited_origins: HashSet::new(),
|
visited_origins: HashSet::new(),
|
||||||
|
next_tab_id: 1,
|
||||||
};
|
};
|
||||||
manager.discover_and_attach_targets().await?;
|
manager.discover_and_attach_targets().await?;
|
||||||
manager
|
manager
|
||||||
@@ -385,6 +388,7 @@ impl BrowserManager {
|
|||||||
download_path: None,
|
download_path: None,
|
||||||
ignore_https_errors: false,
|
ignore_https_errors: false,
|
||||||
visited_origins: HashSet::new(),
|
visited_origins: HashSet::new(),
|
||||||
|
next_tab_id: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
if direct_page {
|
if direct_page {
|
||||||
@@ -453,7 +457,10 @@ impl BrowserManager {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let tab_id = self.next_tab_id;
|
||||||
|
self.next_tab_id += 1;
|
||||||
self.pages.push(PageInfo {
|
self.pages.push(PageInfo {
|
||||||
|
tab_id,
|
||||||
target_id: result.target_id,
|
target_id: result.target_id,
|
||||||
session_id: attach_result.session_id.clone(),
|
session_id: attach_result.session_id.clone(),
|
||||||
url: "about:blank".to_string(),
|
url: "about:blank".to_string(),
|
||||||
@@ -476,7 +483,10 @@ impl BrowserManager {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let tab_id = self.next_tab_id;
|
||||||
|
self.next_tab_id += 1;
|
||||||
self.pages.push(PageInfo {
|
self.pages.push(PageInfo {
|
||||||
|
tab_id,
|
||||||
target_id: target.target_id.clone(),
|
target_id: target.target_id.clone(),
|
||||||
session_id: attach_result.session_id.clone(),
|
session_id: attach_result.session_id.clone(),
|
||||||
url: target.url.clone(),
|
url: target.url.clone(),
|
||||||
@@ -821,7 +831,10 @@ impl BrowserManager {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let tab_id = self.next_tab_id;
|
||||||
|
self.next_tab_id += 1;
|
||||||
self.pages.push(PageInfo {
|
self.pages.push(PageInfo {
|
||||||
|
tab_id,
|
||||||
target_id: result.target_id,
|
target_id: result.target_id,
|
||||||
session_id: attach_result.session_id.clone(),
|
session_id: attach_result.session_id.clone(),
|
||||||
url: "about:blank".to_string(),
|
url: "about:blank".to_string(),
|
||||||
@@ -864,7 +877,7 @@ impl BrowserManager {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, p)| {
|
.map(|(i, p)| {
|
||||||
json!({
|
json!({
|
||||||
"index": i,
|
"tabId": p.tab_id,
|
||||||
"title": p.title,
|
"title": p.title,
|
||||||
"url": p.url,
|
"url": p.url,
|
||||||
"type": p.target_type,
|
"type": p.target_type,
|
||||||
@@ -902,8 +915,11 @@ impl BrowserManager {
|
|||||||
|
|
||||||
self.enable_domains(&attach.session_id).await?;
|
self.enable_domains(&attach.session_id).await?;
|
||||||
|
|
||||||
|
let tab_id = self.next_tab_id;
|
||||||
|
self.next_tab_id += 1;
|
||||||
let index = self.pages.len();
|
let index = self.pages.len();
|
||||||
self.pages.push(PageInfo {
|
self.pages.push(PageInfo {
|
||||||
|
tab_id,
|
||||||
target_id: result.target_id,
|
target_id: result.target_id,
|
||||||
session_id: attach.session_id,
|
session_id: attach.session_id,
|
||||||
url: target_url.to_string(),
|
url: target_url.to_string(),
|
||||||
@@ -912,7 +928,7 @@ impl BrowserManager {
|
|||||||
});
|
});
|
||||||
self.active_page_index = index;
|
self.active_page_index = index;
|
||||||
|
|
||||||
Ok(json!({ "index": index, "url": target_url }))
|
Ok(json!({ "tabId": tab_id, "url": target_url, "total": self.pages.len() }))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn tab_switch(&mut self, index: usize) -> Result<Value, String> {
|
pub async fn tab_switch(&mut self, index: usize) -> Result<Value, String> {
|
||||||
@@ -942,7 +958,8 @@ impl BrowserManager {
|
|||||||
page.title = title.clone();
|
page.title = title.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(json!({ "index": index, "url": url, "title": title }))
|
let tab_id = self.pages[index].tab_id;
|
||||||
|
Ok(json!({ "tabId": tab_id, "url": url, "title": title }))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn tab_close(&mut self, index: Option<usize>) -> Result<Value, String> {
|
pub async fn tab_close(&mut self, index: Option<usize>) -> Result<Value, String> {
|
||||||
@@ -958,6 +975,7 @@ impl BrowserManager {
|
|||||||
|
|
||||||
let page = self.pages.remove(target_index);
|
let page = self.pages.remove(target_index);
|
||||||
self.update_active_page_after_removal(target_index);
|
self.update_active_page_after_removal(target_index);
|
||||||
|
let closed_tab_id = page.tab_id;
|
||||||
let _ = self
|
let _ = self
|
||||||
.client
|
.client
|
||||||
.send_command_typed::<_, Value>(
|
.send_command_typed::<_, Value>(
|
||||||
@@ -972,7 +990,7 @@ impl BrowserManager {
|
|||||||
let session_id = self.pages[self.active_page_index].session_id.clone();
|
let session_id = self.pages[self.active_page_index].session_id.clone();
|
||||||
self.enable_domains(&session_id).await?;
|
self.enable_domains(&session_id).await?;
|
||||||
|
|
||||||
Ok(json!({ "closed": target_index, "activeIndex": self.active_page_index }))
|
Ok(json!({ "tabId": closed_tab_id, "closed": true }))
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
@@ -1213,6 +1231,34 @@ impl BrowserManager {
|
|||||||
.to_string())
|
.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn tab_switch_by_id(&mut self, tab_id: u32) -> Result<Value, String> {
|
||||||
|
let index = self
|
||||||
|
.pages
|
||||||
|
.iter()
|
||||||
|
.position(|p| p.tab_id == tab_id)
|
||||||
|
.ok_or_else(|| format!("Tab ID {} not found", tab_id))?;
|
||||||
|
self.tab_switch(index).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn tab_close_by_id(&mut self, tab_id: Option<u32>) -> Result<Value, String> {
|
||||||
|
let index = match tab_id {
|
||||||
|
Some(id) => Some(
|
||||||
|
self.pages
|
||||||
|
.iter()
|
||||||
|
.position(|p| p.tab_id == id)
|
||||||
|
.ok_or_else(|| format!("Tab ID {} not found", id))?,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
self.tab_close(index).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assign_tab_id(&mut self) -> u32 {
|
||||||
|
let id = self.next_tab_id;
|
||||||
|
self.next_tab_id += 1;
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_page(&mut self, page: PageInfo) {
|
pub fn add_page(&mut self, page: PageInfo) {
|
||||||
let index = self.pages.len();
|
let index = self.pages.len();
|
||||||
self.pages.push(page);
|
self.pages.push(page);
|
||||||
@@ -1393,6 +1439,7 @@ async fn initialize_lightpanda_manager(
|
|||||||
download_path: None,
|
download_path: None,
|
||||||
ignore_https_errors: false,
|
ignore_https_errors: false,
|
||||||
visited_origins: HashSet::new(),
|
visited_origins: HashSet::new(),
|
||||||
|
next_tab_id: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
match discover_and_attach_lightpanda_targets(&mut manager, deadline).await {
|
match discover_and_attach_lightpanda_targets(&mut manager, deadline).await {
|
||||||
|
|||||||
+332
-5
@@ -905,12 +905,13 @@ async fn e2e_tabs() {
|
|||||||
.await;
|
.await;
|
||||||
assert_success(&resp);
|
assert_success(&resp);
|
||||||
|
|
||||||
// Tab list should show 1 tab
|
// Tab list should show 1 tab with tabId 1
|
||||||
let resp = execute_command(&json!({ "id": "3", "action": "tab_list" }), &mut state).await;
|
let resp = execute_command(&json!({ "id": "3", "action": "tab_list" }), &mut state).await;
|
||||||
assert_success(&resp);
|
assert_success(&resp);
|
||||||
let tabs = get_data(&resp)["tabs"].as_array().unwrap();
|
let tabs = get_data(&resp)["tabs"].as_array().unwrap();
|
||||||
assert_eq!(tabs.len(), 1);
|
assert_eq!(tabs.len(), 1);
|
||||||
assert_eq!(tabs[0]["active"], true);
|
assert_eq!(tabs[0]["active"], true);
|
||||||
|
assert_eq!(tabs[0]["tabId"], 1, "First tab should have tabId 1");
|
||||||
|
|
||||||
// Open new tab
|
// Open new tab
|
||||||
let resp = execute_command(
|
let resp = execute_command(
|
||||||
@@ -919,18 +920,21 @@ async fn e2e_tabs() {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert_success(&resp);
|
assert_success(&resp);
|
||||||
assert_eq!(get_data(&resp)["index"], 1);
|
assert_eq!(get_data(&resp)["tabId"], 2, "New tab should have tabId 2");
|
||||||
|
assert_eq!(get_data(&resp)["total"], 2);
|
||||||
|
|
||||||
// Tab list should show 2 tabs
|
// Tab list should show 2 tabs with distinct, incrementing tabIds
|
||||||
let resp = execute_command(&json!({ "id": "5", "action": "tab_list" }), &mut state).await;
|
let resp = execute_command(&json!({ "id": "5", "action": "tab_list" }), &mut state).await;
|
||||||
assert_success(&resp);
|
assert_success(&resp);
|
||||||
let tabs = get_data(&resp)["tabs"].as_array().unwrap();
|
let tabs = get_data(&resp)["tabs"].as_array().unwrap();
|
||||||
assert_eq!(tabs.len(), 2);
|
assert_eq!(tabs.len(), 2);
|
||||||
assert_eq!(tabs[1]["active"], true);
|
assert_eq!(tabs[1]["active"], true);
|
||||||
|
assert_eq!(tabs[0]["tabId"], 1, "First tab should keep tabId 1");
|
||||||
|
assert_eq!(tabs[1]["tabId"], 2, "Second tab should have tabId 2");
|
||||||
|
|
||||||
// Switch to first tab
|
// Switch to first tab
|
||||||
let resp = execute_command(
|
let resp = execute_command(
|
||||||
&json!({ "id": "6", "action": "tab_switch", "index": 0 }),
|
&json!({ "id": "6", "action": "tab_switch", "tabId": 1 }),
|
||||||
&mut state,
|
&mut state,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -946,7 +950,7 @@ async fn e2e_tabs() {
|
|||||||
|
|
||||||
// Close second tab
|
// Close second tab
|
||||||
let resp = execute_command(
|
let resp = execute_command(
|
||||||
&json!({ "id": "8", "action": "tab_close", "index": 1 }),
|
&json!({ "id": "8", "action": "tab_close", "tabId": 2 }),
|
||||||
&mut state,
|
&mut state,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -962,6 +966,329 @@ async fn e2e_tabs() {
|
|||||||
assert_success(&resp);
|
assert_success(&resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn e2e_tab_ids_not_reused() {
|
||||||
|
let mut state = DaemonState::new();
|
||||||
|
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "1", "action": "launch", "headless": true }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
|
||||||
|
// First tab gets tabId 1
|
||||||
|
let resp = execute_command(&json!({ "id": "2", "action": "tab_list" }), &mut state).await;
|
||||||
|
assert_success(&resp);
|
||||||
|
let tabs = get_data(&resp)["tabs"].as_array().unwrap();
|
||||||
|
assert_eq!(tabs[0]["tabId"], 1);
|
||||||
|
|
||||||
|
// Open tab 2 and tab 3
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "3", "action": "tab_new", "url": "data:text/html,<h1>Tab 2</h1>" }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
assert_eq!(get_data(&resp)["tabId"], 2);
|
||||||
|
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "4", "action": "tab_new", "url": "data:text/html,<h1>Tab 3</h1>" }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
assert_eq!(get_data(&resp)["tabId"], 3);
|
||||||
|
|
||||||
|
// Close tab 2
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "5", "action": "tab_close", "tabId": 2 }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
|
||||||
|
// Open a new tab — should get tabId 4, NOT 2
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "6", "action": "tab_new", "url": "data:text/html,<h1>Tab 4</h1>" }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
assert_eq!(
|
||||||
|
get_data(&resp)["tabId"],
|
||||||
|
4,
|
||||||
|
"Tab IDs must not be reused after closing"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify final state: tabs 1, 3, 4
|
||||||
|
let resp = execute_command(&json!({ "id": "7", "action": "tab_list" }), &mut state).await;
|
||||||
|
assert_success(&resp);
|
||||||
|
let tabs = get_data(&resp)["tabs"].as_array().unwrap();
|
||||||
|
assert_eq!(tabs.len(), 3);
|
||||||
|
let ids: Vec<i64> = tabs.iter().map(|t| t["tabId"].as_i64().unwrap()).collect();
|
||||||
|
assert_eq!(ids, vec![1, 3, 4]);
|
||||||
|
|
||||||
|
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
|
||||||
|
assert_success(&resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn e2e_tab_global_targeting() {
|
||||||
|
let mut state = DaemonState::new();
|
||||||
|
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "1", "action": "launch", "headless": true }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
|
||||||
|
// Navigate tab 1
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "2", "action": "navigate", "url": "data:text/html,<h1>Page A</h1>" }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
|
||||||
|
// Open tab 2 (becomes active)
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "3", "action": "tab_new", "url": "data:text/html,<h1>Page B</h1>" }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
assert_eq!(get_data(&resp)["tabId"], 2);
|
||||||
|
|
||||||
|
// Use tabId to evaluate on tab 1 while tab 2 is active
|
||||||
|
// (simulates --tab 1 evaluate ...)
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "4", "action": "evaluate", "tabId": 1, "script": "document.querySelector('h1').textContent" }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
assert_eq!(
|
||||||
|
get_data(&resp)["result"],
|
||||||
|
"Page A",
|
||||||
|
"tabId should target tab 1 even though tab 2 was active"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify tab 2 content is still accessible
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "5", "action": "evaluate", "tabId": 2, "script": "document.querySelector('h1').textContent" }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
assert_eq!(get_data(&resp)["result"], "Page B");
|
||||||
|
|
||||||
|
// Without tabId, should use the current active tab (now tab 1 from the switch)
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "6", "action": "evaluate", "script": "document.querySelector('h1').textContent" }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
// After targeting tab 1 then tab 2, active tab is now tab 2
|
||||||
|
assert_eq!(get_data(&resp)["result"], "Page B");
|
||||||
|
|
||||||
|
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
|
||||||
|
assert_success(&resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn e2e_tab_global_targeting_snapshot() {
|
||||||
|
let mut state = DaemonState::new();
|
||||||
|
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "1", "action": "launch", "headless": true }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
|
||||||
|
// Navigate tab 1
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "2", "action": "navigate", "url": "data:text/html,<h1>Page A</h1>" }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
|
||||||
|
// Open tab 2 (becomes active)
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "3", "action": "tab_new", "url": "data:text/html,<h1>Page B</h1>" }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
assert_eq!(get_data(&resp)["tabId"], 2);
|
||||||
|
|
||||||
|
// Snapshot tab 1 via tabId while tab 2 is active
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "4", "action": "snapshot", "tabId": 1 }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
let snapshot = get_data(&resp)["snapshot"].as_str().unwrap();
|
||||||
|
assert!(
|
||||||
|
snapshot.contains("Page A"),
|
||||||
|
"Snapshot with tabId=1 should contain 'Page A', got: {}",
|
||||||
|
snapshot
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!snapshot.contains("Page B"),
|
||||||
|
"Snapshot with tabId=1 should NOT contain 'Page B', got: {}",
|
||||||
|
snapshot
|
||||||
|
);
|
||||||
|
|
||||||
|
// Snapshot tab 2 via tabId
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "5", "action": "snapshot", "tabId": 2 }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
let snapshot = get_data(&resp)["snapshot"].as_str().unwrap();
|
||||||
|
assert!(
|
||||||
|
snapshot.contains("Page B"),
|
||||||
|
"Snapshot with tabId=2 should contain 'Page B', got: {}",
|
||||||
|
snapshot
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!snapshot.contains("Page A"),
|
||||||
|
"Snapshot with tabId=2 should NOT contain 'Page A', got: {}",
|
||||||
|
snapshot
|
||||||
|
);
|
||||||
|
|
||||||
|
// Snapshot without tabId should use the last-switched tab (tab 2)
|
||||||
|
let resp = execute_command(&json!({ "id": "6", "action": "snapshot" }), &mut state).await;
|
||||||
|
assert_success(&resp);
|
||||||
|
let snapshot = get_data(&resp)["snapshot"].as_str().unwrap();
|
||||||
|
assert!(
|
||||||
|
snapshot.contains("Page B"),
|
||||||
|
"Snapshot without tabId should use active tab (Page B), got: {}",
|
||||||
|
snapshot
|
||||||
|
);
|
||||||
|
|
||||||
|
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
|
||||||
|
assert_success(&resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn e2e_tab_global_targeting_snapshot_non_contiguous() {
|
||||||
|
// Reproduces the bug where --tab 3 snapshot shows tab 1's content
|
||||||
|
// when tab IDs are non-contiguous (e.g. tabs [1] and [3] after
|
||||||
|
// closing tab [2]).
|
||||||
|
let mut state = DaemonState::new();
|
||||||
|
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "1", "action": "launch", "headless": true }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
|
||||||
|
// Navigate tab 1 to Page A
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "2", "action": "navigate", "url": "data:text/html,<h1>Page A</h1>" }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
|
||||||
|
// Open tab 2
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "3", "action": "tab_new", "url": "data:text/html,<h1>Page B</h1>" }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
assert_eq!(get_data(&resp)["tabId"], 2);
|
||||||
|
|
||||||
|
// Open tab 3
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "4", "action": "tab_new", "url": "data:text/html,<h1>Page C</h1>" }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
assert_eq!(get_data(&resp)["tabId"], 3);
|
||||||
|
|
||||||
|
// Close tab 2 to create non-contiguous IDs: [1, 3]
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "5", "action": "tab_close", "tabId": 2 }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
|
||||||
|
// Verify tab list shows [1] and [3]
|
||||||
|
let resp = execute_command(&json!({ "id": "6", "action": "tab_list" }), &mut state).await;
|
||||||
|
assert_success(&resp);
|
||||||
|
let tabs = get_data(&resp)["tabs"].as_array().unwrap();
|
||||||
|
assert_eq!(tabs.len(), 2);
|
||||||
|
assert_eq!(tabs[0]["tabId"], 1);
|
||||||
|
assert_eq!(tabs[1]["tabId"], 3);
|
||||||
|
|
||||||
|
// Switch active tab back to tab 1
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "7", "action": "tab_switch", "tabId": 1 }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
|
||||||
|
// Snapshot tab 3 via tabId while tab 1 is active
|
||||||
|
// (simulates: --tab 3 snapshot)
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "8", "action": "snapshot", "tabId": 3 }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
let snapshot = get_data(&resp)["snapshot"].as_str().unwrap();
|
||||||
|
assert!(
|
||||||
|
snapshot.contains("Page C"),
|
||||||
|
"Snapshot with tabId=3 should contain 'Page C', got: {}",
|
||||||
|
snapshot
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!snapshot.contains("Page A"),
|
||||||
|
"Snapshot with tabId=3 should NOT contain 'Page A', got: {}",
|
||||||
|
snapshot
|
||||||
|
);
|
||||||
|
|
||||||
|
// Snapshot tab 1 via tabId
|
||||||
|
let resp = execute_command(
|
||||||
|
&json!({ "id": "9", "action": "snapshot", "tabId": 1 }),
|
||||||
|
&mut state,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_success(&resp);
|
||||||
|
let snapshot = get_data(&resp)["snapshot"].as_str().unwrap();
|
||||||
|
assert!(
|
||||||
|
snapshot.contains("Page A"),
|
||||||
|
"Snapshot with tabId=1 should contain 'Page A', got: {}",
|
||||||
|
snapshot
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!snapshot.contains("Page C"),
|
||||||
|
"Snapshot with tabId=1 should NOT contain 'Page C', got: {}",
|
||||||
|
snapshot
|
||||||
|
);
|
||||||
|
|
||||||
|
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
|
||||||
|
assert_success(&resp);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Element queries: isvisible, isenabled, gettext, getattribute
|
// Element queries: isvisible, isenabled, gettext, getattribute
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
+105
-6
@@ -406,7 +406,11 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou
|
|||||||
}
|
}
|
||||||
// Tabs
|
// Tabs
|
||||||
if let Some(tabs) = data.get("tabs").and_then(|v| v.as_array()) {
|
if let Some(tabs) = data.get("tabs").and_then(|v| v.as_array()) {
|
||||||
for (i, tab) in tabs.iter().enumerate() {
|
for tab in tabs {
|
||||||
|
let tab_id = tab
|
||||||
|
.get("tabId")
|
||||||
|
.and_then(|v| v.as_i64())
|
||||||
|
.unwrap_or_default();
|
||||||
let title = tab
|
let title = tab
|
||||||
.get("title")
|
.get("title")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
@@ -418,10 +422,47 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou
|
|||||||
} else {
|
} else {
|
||||||
" ".to_string()
|
" ".to_string()
|
||||||
};
|
};
|
||||||
println!("{} [{}] {} - {}", marker, i, title, url);
|
println!("{} [{}] {} - {}", marker, tab_id, title, url);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Tab switch
|
||||||
|
if action == Some("tab_switch") {
|
||||||
|
if let Some(tab_id) = data.get("tabId").and_then(|v| v.as_i64()) {
|
||||||
|
if let Some(url) = data.get("url").and_then(|v| v.as_str()) {
|
||||||
|
println!(
|
||||||
|
"{} Switched to tab [{}] ({})",
|
||||||
|
color::success_indicator(),
|
||||||
|
tab_id,
|
||||||
|
url
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"{} Switched to tab [{}]",
|
||||||
|
color::success_indicator(),
|
||||||
|
tab_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// New tab/window
|
||||||
|
if let Some(tab_id) = data.get("tabId").and_then(|v| v.as_i64()) {
|
||||||
|
if let Some(total) = data.get("total").and_then(|v| v.as_i64()) {
|
||||||
|
let label = match action {
|
||||||
|
Some("window_new") => "Window opened",
|
||||||
|
_ => "Tab opened",
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
"{} {} [{}] ({} total)",
|
||||||
|
color::success_indicator(),
|
||||||
|
label,
|
||||||
|
tab_id,
|
||||||
|
total
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Console logs
|
// Console logs
|
||||||
if let Some(logs) = data.get("messages").and_then(|v| v.as_array()) {
|
if let Some(logs) = data.get("messages").and_then(|v| v.as_array()) {
|
||||||
if opts.content_boundaries {
|
if opts.content_boundaries {
|
||||||
@@ -562,7 +603,13 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou
|
|||||||
// Closed (browser or tab)
|
// Closed (browser or tab)
|
||||||
if data.get("closed").is_some() {
|
if data.get("closed").is_some() {
|
||||||
let label = match action {
|
let label = match action {
|
||||||
Some("tab_close") => "Tab closed",
|
Some("tab_close") => {
|
||||||
|
if let Some(closed_id) = data.get("tabId").and_then(|v| v.as_i64()) {
|
||||||
|
println!("{} Tab [{}] closed", color::success_indicator(), closed_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
"Tab closed"
|
||||||
|
}
|
||||||
_ => "Browser closed",
|
_ => "Browser closed",
|
||||||
};
|
};
|
||||||
println!("{} {}", color::success_indicator(), label);
|
println!("{} {}", color::success_indicator(), label);
|
||||||
@@ -1010,6 +1057,7 @@ Aliases: goto, navigate
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
--headers <json> Set HTTP headers (scoped to this origin)
|
--headers <json> Set HTTP headers (scoped to this origin)
|
||||||
--headed Show browser window
|
--headed Show browser window
|
||||||
|
|
||||||
@@ -1033,6 +1081,7 @@ the browser's back button.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser back
|
agent-browser back
|
||||||
@@ -1050,6 +1099,7 @@ the browser's forward button.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser forward
|
agent-browser forward
|
||||||
@@ -1067,6 +1117,7 @@ the browser's reload button.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser reload
|
agent-browser reload
|
||||||
@@ -1090,6 +1141,7 @@ Options:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser click "#submit-button"
|
agent-browser click "#submit-button"
|
||||||
@@ -1111,6 +1163,7 @@ or triggering double-click handlers.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser dblclick "#editable-text"
|
agent-browser dblclick "#editable-text"
|
||||||
@@ -1129,6 +1182,7 @@ This replaces any existing content in the field.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser fill "#email" "user@example.com"
|
agent-browser fill "#email" "user@example.com"
|
||||||
@@ -1148,6 +1202,7 @@ Unlike fill, this does not clear existing content first.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser type "#search" "hello"
|
agent-browser type "#search" "hello"
|
||||||
@@ -1171,6 +1226,7 @@ triggering hover states or dropdown menus.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser hover "#dropdown-trigger"
|
agent-browser hover "#dropdown-trigger"
|
||||||
@@ -1188,6 +1244,7 @@ Sets keyboard focus to the specified element.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser focus "#input-field"
|
agent-browser focus "#input-field"
|
||||||
@@ -1205,6 +1262,7 @@ Checks a checkbox element. If already checked, no action is taken.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser check "#terms-checkbox"
|
agent-browser check "#terms-checkbox"
|
||||||
@@ -1222,6 +1280,7 @@ Unchecks a checkbox element. If already unchecked, no action is taken.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser uncheck "#newsletter-opt-in"
|
agent-browser uncheck "#newsletter-opt-in"
|
||||||
@@ -1239,6 +1298,7 @@ Selects one or more options in a <select> dropdown by value.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser select "#country" "US"
|
agent-browser select "#country" "US"
|
||||||
@@ -1257,6 +1317,7 @@ Drags an element from source to target location.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser drag "#draggable" "#drop-zone"
|
agent-browser drag "#draggable" "#drop-zone"
|
||||||
@@ -1274,6 +1335,7 @@ Uploads one or more files to a file input element.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser upload "#file-input" ./document.pdf
|
agent-browser upload "#file-input" ./document.pdf
|
||||||
@@ -1295,6 +1357,7 @@ Arguments:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser download "#download-btn" ./file.pdf
|
agent-browser download "#download-btn" ./file.pdf
|
||||||
@@ -1326,6 +1389,7 @@ Modifiers (combine with +):
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser press Enter
|
agent-browser press Enter
|
||||||
@@ -1347,6 +1411,7 @@ Useful for holding modifier keys.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser keydown Shift
|
agent-browser keydown Shift
|
||||||
@@ -1364,6 +1429,7 @@ Releases a key that was pressed with keydown.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser keyup Shift
|
agent-browser keyup Shift
|
||||||
@@ -1392,6 +1458,7 @@ directly — it already operates on the current focus.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser keyboard type "Hello, World!"
|
agent-browser keyboard type "Hello, World!"
|
||||||
@@ -1426,6 +1493,7 @@ Options:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser scroll
|
agent-browser scroll
|
||||||
@@ -1448,6 +1516,7 @@ Aliases: scrollinto
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser scrollintoview "#footer"
|
agent-browser scrollintoview "#footer"
|
||||||
@@ -1485,6 +1554,7 @@ Wait for text to disappear:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser wait "#loading-spinner"
|
agent-browser wait "#loading-spinner"
|
||||||
@@ -1526,6 +1596,7 @@ Options:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser screenshot
|
agent-browser screenshot
|
||||||
@@ -1549,6 +1620,7 @@ Saves the current page as a PDF file.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser pdf ./page.pdf
|
agent-browser pdf ./page.pdf
|
||||||
@@ -1577,6 +1649,7 @@ Options:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser snapshot
|
agent-browser snapshot
|
||||||
@@ -1603,6 +1676,7 @@ Options:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser eval "document.title"
|
agent-browser eval "document.title"
|
||||||
@@ -1635,6 +1709,7 @@ Options:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser close
|
agent-browser close
|
||||||
@@ -1686,6 +1761,7 @@ Subcommands:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser get text @e1
|
agent-browser get text @e1
|
||||||
@@ -1718,6 +1794,7 @@ Subcommands:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser is visible "#modal"
|
agent-browser is visible "#modal"
|
||||||
@@ -1757,6 +1834,7 @@ Options:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser find role button click --name Submit
|
agent-browser find role button click --name Submit
|
||||||
@@ -1787,6 +1865,7 @@ Subcommands:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser mouse move 100 200
|
agent-browser mouse move 100 200
|
||||||
@@ -1820,6 +1899,7 @@ Settings:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser set viewport 1920 1080
|
agent-browser set viewport 1920 1080
|
||||||
@@ -1860,6 +1940,7 @@ Subcommands:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser network route "**/api/*" --abort
|
agent-browser network route "**/api/*" --abort
|
||||||
@@ -1897,6 +1978,7 @@ Operations:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser storage local
|
agent-browser storage local
|
||||||
@@ -1936,6 +2018,7 @@ for the current page URL.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
# Simple cookie for current page
|
# Simple cookie for current page
|
||||||
@@ -1971,14 +2054,15 @@ Usage: agent-browser tab [operation] [args]
|
|||||||
Manage browser tabs in the current window.
|
Manage browser tabs in the current window.
|
||||||
|
|
||||||
Operations:
|
Operations:
|
||||||
list List all tabs (default)
|
list List all tabs with tab IDs (default)
|
||||||
new [url] Open new tab
|
new [url] Open new tab
|
||||||
close [index] Close tab (current if no index)
|
close [id] Close tab by ID (current if no ID)
|
||||||
<index> Switch to tab by index
|
<id> Switch to tab by ID
|
||||||
|
|
||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser tab
|
agent-browser tab
|
||||||
@@ -2006,6 +2090,7 @@ Operations:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser window new
|
agent-browser window new
|
||||||
@@ -2028,6 +2113,7 @@ Arguments:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser frame "#embed-iframe"
|
agent-browser frame "#embed-iframe"
|
||||||
@@ -2115,6 +2201,7 @@ Operations:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser dialog accept
|
agent-browser dialog accept
|
||||||
@@ -2140,6 +2227,7 @@ Operations:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser trace start
|
agent-browser trace start
|
||||||
@@ -2171,6 +2259,7 @@ Start Options:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
# Basic profiling
|
# Basic profiling
|
||||||
@@ -2210,6 +2299,7 @@ Operations:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
# Record from current page (preserves login state)
|
# Record from current page (preserves login state)
|
||||||
@@ -2242,6 +2332,7 @@ Options:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser console
|
agent-browser console
|
||||||
@@ -2262,6 +2353,7 @@ Options:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser errors
|
agent-browser errors
|
||||||
@@ -2281,6 +2373,7 @@ Visually highlights an element on the page for debugging.
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser highlight "#target-element"
|
agent-browser highlight "#target-element"
|
||||||
@@ -2306,6 +2399,7 @@ Operations:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser clipboard read
|
agent-browser clipboard read
|
||||||
@@ -2345,6 +2439,7 @@ State Encryption:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser state save ./auth-state.json
|
agent-browser state save ./auth-state.json
|
||||||
@@ -2377,6 +2472,7 @@ Environment:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser session
|
agent-browser session
|
||||||
@@ -2474,6 +2570,7 @@ Supported URL formats:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
# Connect to local Chrome with remote debugging
|
# Connect to local Chrome with remote debugging
|
||||||
@@ -2635,6 +2732,7 @@ URL Diff:
|
|||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
|
--tab <id> Target specific tab ID
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser diff snapshot
|
agent-browser diff snapshot
|
||||||
@@ -2929,6 +3027,7 @@ Authentication:
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
--session <name> Isolated session (or AGENT_BROWSER_SESSION env)
|
--session <name> Isolated session (or AGENT_BROWSER_SESSION env)
|
||||||
|
--tab <id> Target specific tab ID for the command
|
||||||
--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)
|
||||||
--args <args> Browser launch args, comma or newline separated (or AGENT_BROWSER_ARGS)
|
--args <args> Browser launch args, comma or newline separated (or AGENT_BROWSER_ARGS)
|
||||||
|
|||||||
Reference in New Issue
Block a user