Skip to main content

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 strategy_str = by.strategy().to_string();
168        let value_str = 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        let handler_key = format!("wait_for_element_{}_{}", strategy_str, value_str);
176        let handler_key_clone = handler_key.clone();
177        let expected_strategy = strategy_str.clone();
178        let expected_value = value_str.clone();
179
180        window.inner.pool.add_event_handler(
181            window.inner.session_id,
182            handler_key.clone(),
183            Box::new(move |event: Event| {
184                if event.method.as_str() != "element.added" {
185                    return None;
186                }
187
188                let parsed = event.parse();
189                if let ParsedEvent::ElementAdded {
190                    strategy,
191                    value,
192                    element_id,
193                    ..
194                } = parsed
195                    && strategy == expected_strategy
196                    && value == expected_value
197                {
198                    let element = Element::new(
199                        ElementId::new(&*element_id),
200                        tab_id,
201                        frame_id,
202                        session_id,
203                        window_clone.clone(),
204                    );
205
206                    if let Some(tx) = tx_clone.lock().take() {
207                        let _ = tx.send(Ok(element));
208                    }
209                }
210
211                None
212            }),
213        );
214
215        let command = Command::Element(ElementCommand::Subscribe {
216            strategy: strategy_str,
217            value: value_str,
218            one_shot: true,
219            timeout: Some(timeout_duration.as_millis() as u64),
220        });
221        let response = self.send_command(command).await?;
222
223        // Check if element already exists
224        if let Some(element_id) = response
225            .result
226            .as_ref()
227            .and_then(|v| v.get("elementId"))
228            .and_then(|v| v.as_str())
229        {
230            window
231                .inner
232                .pool
233                .remove_event_handler(window.inner.session_id, &handler_key_clone);
234
235            return Ok(Element::new(
236                ElementId::new(element_id),
237                self.inner.tab_id,
238                self.inner.frame_id,
239                self.inner.session_id,
240                self.inner.window.clone(),
241            ));
242        }
243
244        let result = timeout(timeout_duration, rx).await;
245
246        window
247            .inner
248            .pool
249            .remove_event_handler(window.inner.session_id, &handler_key_clone);
250
251        match result {
252            Ok(Ok(element)) => element,
253            Ok(Err(_)) => Err(Error::protocol("Channel closed unexpectedly")),
254            Err(_) => Err(Error::Timeout {
255                operation: format!("wait_for({}:{})", by.strategy(), by.value()),
256                timeout_ms: timeout_duration.as_millis() as u64,
257            }),
258        }
259    }
260
261    /// Registers a callback for when elements matching the selector appear.
262    ///
263    /// # Returns
264    ///
265    /// Subscription ID for later unsubscription.
266    pub async fn on_element_added<F>(&self, by: By, callback: F) -> Result<SubscriptionId>
267    where
268        F: Fn(Element) + Send + Sync + 'static,
269    {
270        debug!(
271            tab_id = %self.inner.tab_id,
272            strategy = by.strategy(),
273            value = by.value(),
274            "Subscribing to element.added"
275        );
276
277        let window = self.get_window()?;
278
279        let strategy_str = by.strategy().to_string();
280        let value_str = by.value().to_string();
281        let tab_id = self.inner.tab_id;
282        let frame_id = self.inner.frame_id;
283        let session_id = self.inner.session_id;
284        let window_clone = self.inner.window.clone();
285        let callback = Arc::new(callback);
286
287        let handler_key = format!("on_element_added_{}_{}", strategy_str, value_str);
288        let expected_strategy = strategy_str.clone();
289        let expected_value = value_str.clone();
290
291        window.inner.pool.add_event_handler(
292            window.inner.session_id,
293            handler_key,
294            Box::new(move |event: Event| {
295                if event.method.as_str() != "element.added" {
296                    return None;
297                }
298
299                let parsed = event.parse();
300                if let ParsedEvent::ElementAdded {
301                    strategy,
302                    value,
303                    element_id,
304                    ..
305                } = parsed
306                    && strategy == expected_strategy
307                    && value == expected_value
308                {
309                    let element = Element::new(
310                        ElementId::new(&*element_id),
311                        tab_id,
312                        frame_id,
313                        session_id,
314                        window_clone.clone(),
315                    );
316                    callback(element);
317                }
318
319                None
320            }),
321        );
322
323        let command = Command::Element(ElementCommand::Subscribe {
324            strategy: strategy_str,
325            value: value_str,
326            one_shot: false,
327            timeout: None,
328        });
329
330        let response = self.send_command(command).await?;
331
332        let subscription_id = response
333            .result
334            .as_ref()
335            .and_then(|v| v.get("subscriptionId"))
336            .and_then(|v| v.as_str())
337            .ok_or_else(|| Error::protocol("No subscriptionId in response"))?;
338
339        Ok(SubscriptionId::new(subscription_id))
340    }
341
342    /// Registers a callback for when a specific element is removed.
343    pub async fn on_element_removed<F>(&self, element_id: &ElementId, callback: F) -> Result<()>
344    where
345        F: Fn() + Send + Sync + 'static,
346    {
347        debug!(tab_id = %self.inner.tab_id, %element_id, "Watching for element removal");
348
349        let window = self.get_window()?;
350
351        let element_id_clone = element_id.as_str().to_string();
352        let callback = Arc::new(callback);
353
354        let handler_key = format!("on_element_removed_{}", element_id_clone);
355
356        window.inner.pool.add_event_handler(
357            window.inner.session_id,
358            handler_key,
359            Box::new(move |event: Event| {
360                if event.method.as_str() != "element.removed" {
361                    return None;
362                }
363
364                let parsed = event.parse();
365                if let ParsedEvent::ElementRemoved {
366                    element_id: removed_id,
367                    ..
368                } = parsed
369                    && removed_id == element_id_clone
370                {
371                    callback();
372                }
373
374                None
375            }),
376        );
377
378        let command = Command::Element(ElementCommand::WatchRemoval {
379            element_id: element_id.clone(),
380        });
381
382        self.send_command(command).await?;
383        Ok(())
384    }
385
386    /// Unsubscribes from element observation.
387    pub async fn unsubscribe(&self, subscription_id: &SubscriptionId) -> Result<()> {
388        let command = Command::Element(ElementCommand::Unsubscribe {
389            subscription_id: subscription_id.as_str().to_string(),
390        });
391
392        self.send_command(command).await?;
393
394        if let Some(window) = &self.inner.window {
395            // Remove handlers associated with this subscription
396            let key = format!("on_element_added_{}", subscription_id.as_str());
397            window
398                .inner
399                .pool
400                .remove_event_handler(window.inner.session_id, &key);
401        }
402
403        Ok(())
404    }
405}