ui_automata/
tab_handle.rs1use crate::{AutomataError, Browser, SelectorPath, registry::AnchorDef};
2
3#[derive(Debug, Clone)]
5pub struct TabHandle {
6 pub tab_id: String,
8 pub parent_browser: String,
10 pub created: bool,
13 pub depth: usize,
15}
16
17impl TabHandle {
18 pub fn mount(def: &AnchorDef, browser: &impl Browser) -> Result<Self, AutomataError> {
25 let parent_browser = def.parent.clone().ok_or_else(|| {
26 AutomataError::Internal(format!("Tab anchor '{}' has no parent", def.name))
27 })?;
28
29 let (tab_id, created) = if def.selector.is_wildcard() {
30 let tabs = browser
33 .tabs()
34 .map_err(|e| AutomataError::Internal(format!("browser.tabs(): {e}")))?;
35 let new_tab = tabs.into_iter().find(|(_, info)| {
36 info.url == "edge://newtab/" || info.url == "about:blank" || info.url.is_empty()
37 });
38 if let Some((existing_id, _)) = new_tab {
39 (existing_id, true)
42 } else {
43 let tab_id = browser
44 .open_tab(None)
45 .map_err(|e| AutomataError::Internal(format!("open_tab(blank): {e}")))?;
46 (tab_id, true)
47 }
48 } else {
49 let selector = def.selector.clone();
51 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
52 let tab_id = loop {
53 let tabs = browser
54 .tabs()
55 .map_err(|e| AutomataError::Internal(format!("browser.tabs(): {e}")))?;
56 if let Some((id, _)) = tabs
57 .into_iter()
58 .find(|(_, info)| selector.matches_tab_info(&info.title, &info.url))
59 {
60 break id;
61 }
62 if std::time::Instant::now() >= deadline {
63 return Err(AutomataError::Internal(format!(
64 "Tab '{}': timed out waiting for tab matching '{selector}'",
65 def.name
66 )));
67 }
68 std::thread::sleep(std::time::Duration::from_millis(200));
69 };
70 (tab_id, false)
71 };
72
73 if let Err(e) = browser.activate_tab(&tab_id) {
75 log::warn!("activate_tab('{}') failed: {e}", tab_id);
76 }
77
78 log::debug!(
79 "mounted tab '{}' (tab_id={tab_id}, created={created})",
80 def.name
81 );
82 Ok(Self {
83 tab_id,
84 parent_browser,
85 created,
86 depth: def.mount_depth,
87 })
88 }
89
90 pub fn close_if_created(&self, browser: &impl Browser) {
92 if self.created {
93 if let Err(e) = browser.close_tab(&self.tab_id) {
94 log::warn!("close_tab('{}') failed: {e}", self.tab_id);
95 }
96 }
97 }
98
99 pub fn scoped_selector(
107 &self,
108 browser: &impl Browser,
109 selector: &SelectorPath,
110 ) -> Result<(String, SelectorPath), AutomataError> {
111 let visibility = browser
114 .eval(&self.tab_id, "document.visibilityState")
115 .unwrap_or_default();
116 if visibility != "visible" {
117 return Err(AutomataError::Internal(format!(
118 "tab '{}' is not the active tab (visibilityState={:?})",
119 self.tab_id, visibility
120 )));
121 }
122 let doc_sel_str = format!(">> [id=RootWebArea] >> {selector}");
123 let doc_sel = SelectorPath::parse(&doc_sel_str)
124 .map_err(|e| AutomataError::Internal(format!("tab selector build: {e}")))?;
125 Ok((self.parent_browser.clone(), doc_sel))
126 }
127}