firefox_webdriver/browser/tab/
elements.rs

1//! Element search and observation methods.
2
3use std::sync::Arc;
4use std::time::Duration;
5
6use parking_lot::Mutex as ParkingMutex;
7use tokio::sync::oneshot;
8use tokio::time::timeout;
9use tracing::debug;
10
11use crate::browser::Element;
12use crate::browser::selector::By;
13use crate::error::{Error, Result};
14use crate::identifiers::{ElementId, SubscriptionId};
15use crate::protocol::event::ParsedEvent;
16use crate::protocol::{Command, ElementCommand, Event};
17
18use super::Tab;
19
20// ============================================================================
21// Constants
22// ============================================================================
23
24/// Default timeout for wait_for_element (30 seconds).
25const DEFAULT_WAIT_TIMEOUT: Duration = Duration::from_secs(30);
26
27// ============================================================================
28// Tab - Element Search
29// ============================================================================
30
31impl Tab {
32    /// Finds a single element using a locator strategy.
33    ///
34    /// # Example
35    ///
36    /// ```ignore
37    /// use firefox_webdriver::By;
38    ///
39    /// // CSS selector
40    /// let btn = tab.find_element(By::Css("#submit")).await?;
41    ///
42    /// // By ID
43    /// let form = tab.find_element(By::Id("login-form")).await?;
44    ///
45    /// // By text content
46    /// let link = tab.find_element(By::Text("Click here")).await?;
47    ///
48    /// // By XPath
49    /// let btn = tab.find_element(By::XPath("//button[@type='submit']")).await?;
50    /// ```
51    pub async fn find_element(&self, by: By) -> Result<Element> {
52        let command = Command::Element(ElementCommand::Find {
53            strategy: by.strategy().to_string(),
54            value: by.value().to_string(),
55            parent_id: None,
56        });
57
58        let response = self.send_command(command).await?;
59
60        let element_id = response
61            .result
62            .as_ref()
63            .and_then(|v| v.get("elementId"))
64            .and_then(|v| v.as_str())
65            .ok_or_else(|| {
66                Error::element_not_found(
67                    format!("{}:{}", by.strategy(), by.value()),
68                    self.inner.tab_id,
69                    self.inner.frame_id,
70                )
71            })?;
72
73        Ok(Element::new(
74            ElementId::new(element_id),
75            self.inner.tab_id,
76            self.inner.frame_id,
77            self.inner.session_id,
78            self.inner.window.clone(),
79        ))
80    }
81
82    /// Finds all elements using a locator strategy.
83    ///
84    /// # Example
85    ///
86    /// ```ignore
87    /// use firefox_webdriver::By;
88    ///
89    /// let buttons = tab.find_elements(By::Tag("button")).await?;
90    /// let links = tab.find_elements(By::PartialText("Read")).await?;
91    /// ```
92    pub async fn find_elements(&self, by: By) -> Result<Vec<Element>> {
93        let command = Command::Element(ElementCommand::FindAll {
94            strategy: by.strategy().to_string(),
95            value: by.value().to_string(),
96            parent_id: None,
97        });
98
99        let response = self.send_command(command).await?;
100
101        let elements = response
102            .result
103            .as_ref()
104            .and_then(|v| v.get("elementIds"))
105            .and_then(|v| v.as_array())
106            .map(|arr| {
107                arr.iter()
108                    .filter_map(|v| v.as_str())
109                    .map(|id| {
110                        Element::new(
111                            ElementId::new(id),
112                            self.inner.tab_id,
113                            self.inner.frame_id,
114                            self.inner.session_id,
115                            self.inner.window.clone(),
116                        )
117                    })
118                    .collect()
119            })
120            .unwrap_or_default();
121
122        Ok(elements)
123    }
124}
125
126// ============================================================================
127// Tab - Element Observation
128// ============================================================================
129
130impl Tab {
131    /// Waits for an element using a locator strategy.
132    ///
133    /// Uses MutationObserver (no polling). Times out after 30 seconds.
134    ///
135    /// # Example
136    ///
137    /// ```ignore
138    /// use firefox_webdriver::By;
139    ///
140    /// let btn = tab.wait_for_element(By::Id("submit")).await?;
141    /// let link = tab.wait_for_element(By::Css("a.login")).await?;
142    /// let el = tab.wait_for_element(By::XPath("//button")).await?;
143    /// ```
144    pub async fn wait_for_element(&self, by: By) -> Result<Element> {
145        self.wait_for_element_timeout(by, DEFAULT_WAIT_TIMEOUT)
146            .await
147    }
148
149    /// Waits for an element using a locator strategy with custom timeout.
150    pub async fn wait_for_element_timeout(
151        &self,
152        by: By,
153        timeout_duration: Duration,
154    ) -> Result<Element> {
155        debug!(
156            tab_id = %self.inner.tab_id,
157            strategy = by.strategy(),
158            value = by.value(),
159            timeout_ms = timeout_duration.as_millis(),
160            "Waiting for element"
161        );
162
163        let window = self.get_window()?;
164
165        let (tx, rx) = oneshot::channel::<Result<Element>>();
166        let tx = Arc::new(ParkingMutex::new(Some(tx)));
167        let expected_strategy = by.strategy().to_string();
168        let expected_value = by.value().to_string();
169        let tab_id = self.inner.tab_id;
170        let frame_id = self.inner.frame_id;
171        let session_id = self.inner.session_id;
172        let window_clone = self.inner.window.clone();
173        let tx_clone = Arc::clone(&tx);
174
175        window.inner.pool.set_event_handler(
176            window.inner.session_id,
177            Box::new(move |event: Event| {
178                if event.method.as_str() != "element.added" {
179                    return None;
180                }
181
182                let parsed = event.parse();
183                if let ParsedEvent::ElementAdded {
184                    strategy,
185                    value,
186                    element_id,
187                    ..
188                } = parsed
189                    && strategy == expected_strategy
190                    && value == expected_value
191                {
192                    let element = Element::new(
193                        ElementId::new(&element_id),
194                        tab_id,
195                        frame_id,
196                        session_id,
197                        window_clone.clone(),
198                    );
199
200                    if let Some(tx) = tx_clone.lock().take() {
201                        let _ = tx.send(Ok(element));
202                    }
203                }
204
205                None
206            }),
207        );
208
209        let command = Command::Element(ElementCommand::Subscribe {
210            strategy: by.strategy().to_string(),
211            value: by.value().to_string(),
212            one_shot: true,
213            timeout: Some(timeout_duration.as_millis() as u64),
214        });
215        let response = self.send_command(command).await?;
216
217        // Check if element already exists
218        if let Some(element_id) = response
219            .result
220            .as_ref()
221            .and_then(|v| v.get("elementId"))
222            .and_then(|v| v.as_str())
223        {
224            window
225                .inner
226                .pool
227                .clear_event_handler(window.inner.session_id);
228
229            return Ok(Element::new(
230                ElementId::new(element_id),
231                self.inner.tab_id,
232                self.inner.frame_id,
233                self.inner.session_id,
234                self.inner.window.clone(),
235            ));
236        }
237
238        let result = timeout(timeout_duration, rx).await;
239
240        window
241            .inner
242            .pool
243            .clear_event_handler(window.inner.session_id);
244
245        match result {
246            Ok(Ok(element)) => element,
247            Ok(Err(_)) => Err(Error::protocol("Channel closed unexpectedly")),
248            Err(_) => Err(Error::Timeout {
249                operation: format!("wait_for({}:{})", by.strategy(), by.value()),
250                timeout_ms: timeout_duration.as_millis() as u64,
251            }),
252        }
253    }
254
255    /// Registers a callback for when elements matching the selector appear.
256    ///
257    /// # Returns
258    ///
259    /// Subscription ID for later unsubscription.
260    pub async fn on_element_added<F>(&self, by: By, callback: F) -> Result<SubscriptionId>
261    where
262        F: Fn(Element) + Send + Sync + 'static,
263    {
264        debug!(
265            tab_id = %self.inner.tab_id,
266            strategy = by.strategy(),
267            value = by.value(),
268            "Subscribing to element.added"
269        );
270
271        let window = self.get_window()?;
272
273        let expected_strategy = by.strategy().to_string();
274        let expected_value = by.value().to_string();
275        let tab_id = self.inner.tab_id;
276        let frame_id = self.inner.frame_id;
277        let session_id = self.inner.session_id;
278        let window_clone = self.inner.window.clone();
279        let callback = Arc::new(callback);
280
281        window.inner.pool.set_event_handler(
282            window.inner.session_id,
283            Box::new(move |event: Event| {
284                if event.method.as_str() != "element.added" {
285                    return None;
286                }
287
288                let parsed = event.parse();
289                if let ParsedEvent::ElementAdded {
290                    strategy,
291                    value,
292                    element_id,
293                    ..
294                } = parsed
295                    && strategy == expected_strategy
296                    && value == expected_value
297                {
298                    let element = Element::new(
299                        ElementId::new(&element_id),
300                        tab_id,
301                        frame_id,
302                        session_id,
303                        window_clone.clone(),
304                    );
305                    callback(element);
306                }
307
308                None
309            }),
310        );
311
312        let command = Command::Element(ElementCommand::Subscribe {
313            strategy: by.strategy().to_string(),
314            value: by.value().to_string(),
315            one_shot: false,
316            timeout: None,
317        });
318
319        let response = self.send_command(command).await?;
320
321        let subscription_id = response
322            .result
323            .as_ref()
324            .and_then(|v| v.get("subscriptionId"))
325            .and_then(|v| v.as_str())
326            .ok_or_else(|| Error::protocol("No subscriptionId in response"))?;
327
328        Ok(SubscriptionId::new(subscription_id))
329    }
330
331    /// Registers a callback for when a specific element is removed.
332    pub async fn on_element_removed<F>(&self, element_id: &ElementId, callback: F) -> Result<()>
333    where
334        F: Fn() + Send + Sync + 'static,
335    {
336        debug!(tab_id = %self.inner.tab_id, %element_id, "Watching for element removal");
337
338        let window = self.get_window()?;
339
340        let element_id_clone = element_id.as_str().to_string();
341        let callback = Arc::new(callback);
342
343        window.inner.pool.set_event_handler(
344            window.inner.session_id,
345            Box::new(move |event: Event| {
346                if event.method.as_str() != "element.removed" {
347                    return None;
348                }
349
350                let parsed = event.parse();
351                if let ParsedEvent::ElementRemoved {
352                    element_id: removed_id,
353                    ..
354                } = parsed
355                    && removed_id == element_id_clone
356                {
357                    callback();
358                }
359
360                None
361            }),
362        );
363
364        let command = Command::Element(ElementCommand::WatchRemoval {
365            element_id: element_id.clone(),
366        });
367
368        self.send_command(command).await?;
369        Ok(())
370    }
371
372    /// Unsubscribes from element observation.
373    pub async fn unsubscribe(&self, subscription_id: &SubscriptionId) -> Result<()> {
374        let command = Command::Element(ElementCommand::Unsubscribe {
375            subscription_id: subscription_id.as_str().to_string(),
376        });
377
378        self.send_command(command).await?;
379
380        if let Some(window) = &self.inner.window {
381            window
382                .inner
383                .pool
384                .clear_event_handler(window.inner.session_id);
385        }
386
387        Ok(())
388    }
389}