Skip to main content

firefox_webdriver/browser/
element.rs

1//! DOM element interaction and manipulation.
2//!
3//! Elements are identified by UUID and stored in the content script's
4//! internal `Map<UUID, Element>`.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use firefox_webdriver::Key;
10//!
11//! let element = tab.find_element("#submit-button").await?;
12//!
13//! // Get properties
14//! let text = element.get_text().await?;
15//! let value = element.get_value().await?;
16//!
17//! // Interact
18//! element.click().await?;
19//! element.type_text("Hello, World!").await?;
20//!
21//! // Press navigation keys
22//! element.press(Key::Enter).await?;
23//! element.press(Key::Tab).await?;
24//! ```
25
26// ============================================================================
27// Imports
28// ============================================================================
29
30use std::fmt;
31use std::sync::Arc;
32
33use serde_json::Value;
34use tracing::debug;
35
36use crate::error::{Error, Result};
37use crate::identifiers::{ElementId, FrameId, SessionId, TabId};
38use crate::protocol::{Command, ElementCommand, InputCommand, Request, Response, ScriptCommand};
39
40use super::Window;
41use super::keyboard::Key;
42use super::selector::By;
43
44// ============================================================================
45// Types
46// ============================================================================
47
48/// Internal shared state for an element.
49pub(crate) struct ElementInner {
50    /// This element's unique ID.
51    pub id: ElementId,
52
53    /// Tab ID where this element exists.
54    pub tab_id: TabId,
55
56    /// Frame ID where this element exists.
57    pub frame_id: FrameId,
58
59    /// Session ID.
60    pub session_id: SessionId,
61
62    /// Parent window.
63    pub window: Option<Window>,
64}
65
66// ============================================================================
67// Element
68// ============================================================================
69
70/// A handle to a DOM element in a browser tab.
71///
72/// Elements are identified by a UUID stored in the extension's content script.
73/// Operations use generic dynamic property access (`element[method]()`).
74///
75/// # Example
76///
77/// ```ignore
78/// let element = tab.find_element("input[name='email']").await?;
79///
80/// // Set value and submit
81/// element.set_value("user@example.com").await?;
82/// element.type_text("\n").await?; // Press Enter
83/// ```
84#[derive(Clone)]
85pub struct Element {
86    /// Shared inner state.
87    pub(crate) inner: Arc<ElementInner>,
88}
89
90// ============================================================================
91// Element - Display
92// ============================================================================
93
94impl fmt::Debug for Element {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        f.debug_struct("Element")
97            .field("id", &self.inner.id)
98            .field("tab_id", &self.inner.tab_id)
99            .field("frame_id", &self.inner.frame_id)
100            .finish_non_exhaustive()
101    }
102}
103
104// ============================================================================
105// Element - Constructor
106// ============================================================================
107
108impl Element {
109    /// Creates a new element handle.
110    pub(crate) fn new(
111        id: ElementId,
112        tab_id: TabId,
113        frame_id: FrameId,
114        session_id: SessionId,
115        window: Option<Window>,
116    ) -> Self {
117        Self {
118            inner: Arc::new(ElementInner {
119                id,
120                tab_id,
121                frame_id,
122                session_id,
123                window,
124            }),
125        }
126    }
127}
128
129// ============================================================================
130// Element - Accessors
131// ============================================================================
132
133impl Element {
134    /// Returns this element's ID.
135    #[inline]
136    #[must_use]
137    pub fn id(&self) -> &ElementId {
138        &self.inner.id
139    }
140
141    /// Returns the tab ID where this element exists.
142    #[inline]
143    #[must_use]
144    pub fn tab_id(&self) -> TabId {
145        self.inner.tab_id
146    }
147
148    /// Returns the frame ID where this element exists.
149    #[inline]
150    #[must_use]
151    pub fn frame_id(&self) -> FrameId {
152        self.inner.frame_id
153    }
154}
155
156// ============================================================================
157// Element - Actions
158// ============================================================================
159
160impl Element {
161    /// Clicks the element.
162    ///
163    /// Uses `element.click()` internally.
164    pub async fn click(&self) -> Result<()> {
165        debug!(element_id = %self.inner.id, "Clicking element");
166        self.call_method("click", vec![]).await?;
167        Ok(())
168    }
169
170    /// Focuses the element.
171    pub async fn focus(&self) -> Result<()> {
172        debug!(element_id = %self.inner.id, "Focusing element");
173        self.call_method("focus", vec![]).await?;
174        Ok(())
175    }
176
177    /// Blurs (unfocuses) the element.
178    pub async fn blur(&self) -> Result<()> {
179        debug!(element_id = %self.inner.id, "Blurring element");
180        self.call_method("blur", vec![]).await?;
181        Ok(())
182    }
183
184    /// Clears the element's value.
185    ///
186    /// Sets `element.value = ""`.
187    pub async fn clear(&self) -> Result<()> {
188        debug!(element_id = %self.inner.id, "Clearing element");
189        self.set_property("value", Value::String(String::new()))
190            .await
191    }
192}
193
194// ============================================================================
195// Element - Properties
196// ============================================================================
197
198impl Element {
199    /// Gets the element's text content.
200    pub async fn get_text(&self) -> Result<String> {
201        let value = self.get_property("textContent").await?;
202        Ok(value.as_str().unwrap_or("").to_string())
203    }
204
205    /// Gets the element's inner HTML.
206    pub async fn get_inner_html(&self) -> Result<String> {
207        let value = self.get_property("innerHTML").await?;
208        Ok(value.as_str().unwrap_or("").to_string())
209    }
210
211    /// Gets the element's value (for input elements).
212    pub async fn get_value(&self) -> Result<String> {
213        let value = self.get_property("value").await?;
214        Ok(value.as_str().unwrap_or("").to_string())
215    }
216
217    /// Sets the element's value (for input elements).
218    pub async fn set_value(&self, value: &str) -> Result<()> {
219        self.set_property("value", Value::String(value.to_string()))
220            .await
221    }
222
223    /// Gets an attribute value.
224    ///
225    /// Returns `None` if the attribute doesn't exist.
226    pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
227        let result = self
228            .call_method("getAttribute", vec![Value::String(name.to_string())])
229            .await?;
230        Ok(result.as_str().map(|s| s.to_string()))
231    }
232
233    /// Checks if the element is displayed.
234    ///
235    /// Uses computed style and bounding rect for reliable visibility detection.
236    /// Checks `display`, `visibility`, `opacity`, and element dimensions.
237    pub async fn is_displayed(&self) -> Result<bool> {
238        let script = r#"
239            const el = arguments[0];
240            const style = window.getComputedStyle(el);
241            if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
242            const rect = el.getBoundingClientRect();
243            return rect.width > 0 && rect.height > 0;
244        "#;
245
246        let command = Command::Script(ScriptCommand::Evaluate {
247            script: script.to_string(),
248            args: vec![serde_json::json!({"elementId": self.inner.id.as_str()})],
249        });
250
251        let response = self.send_command(command).await?;
252
253        let displayed = response
254            .result
255            .as_ref()
256            .and_then(|v| v.get("value"))
257            .and_then(|v| v.as_bool())
258            .unwrap_or(false);
259
260        Ok(displayed)
261    }
262
263    /// Checks if the element is enabled.
264    ///
265    /// Returns `true` if `disabled` property is false or absent.
266    pub async fn is_enabled(&self) -> Result<bool> {
267        let disabled = self.get_property("disabled").await?;
268        Ok(!disabled.as_bool().unwrap_or(false))
269    }
270}
271
272// ============================================================================
273// Element - Keyboard Input
274// ============================================================================
275
276impl Element {
277    /// Presses a navigation/control key.
278    ///
279    /// For typing text, use [`type_text`](Self::type_text) instead.
280    ///
281    /// # Example
282    ///
283    /// ```ignore
284    /// use firefox_webdriver::Key;
285    ///
286    /// element.press(Key::Enter).await?;
287    /// element.press(Key::Tab).await?;
288    /// element.press(Key::Backspace).await?;
289    /// ```
290    pub async fn press(&self, key: Key) -> Result<()> {
291        let (key_str, code, key_code, printable) = key.properties();
292        self.type_key(
293            key_str, code, key_code, printable, false, false, false, false,
294        )
295        .await
296    }
297
298    /// Types a single key with optional modifiers (low-level API).
299    ///
300    /// Prefer using [`press`](Self::press) for common keys or [`type_text`](Self::type_text) for text.
301    ///
302    /// Dispatches full keyboard event sequence: keydown → input → keypress → keyup.
303    ///
304    /// # Arguments
305    ///
306    /// * `key` - Key value (e.g., "a", "Enter")
307    /// * `code` - Key code (e.g., "KeyA", "Enter")
308    /// * `key_code` - Legacy keyCode number
309    /// * `printable` - Whether key produces visible output
310    /// * `ctrl` - Ctrl modifier
311    /// * `shift` - Shift modifier
312    /// * `alt` - Alt modifier
313    /// * `meta` - Meta modifier
314    #[allow(clippy::too_many_arguments)]
315    pub async fn type_key(
316        &self,
317        key: &str,
318        code: &str,
319        key_code: u32,
320        printable: bool,
321        ctrl: bool,
322        shift: bool,
323        alt: bool,
324        meta: bool,
325    ) -> Result<()> {
326        let command = Command::Input(InputCommand::TypeKey {
327            element_id: self.inner.id.clone(),
328            key: key.to_string(),
329            code: code.to_string(),
330            key_code,
331            printable,
332            ctrl,
333            shift,
334            alt,
335            meta,
336        });
337
338        self.send_command(command).await?;
339        Ok(())
340    }
341
342    /// Types a character with default key properties.
343    ///
344    /// Convenience method that uses `type_text` internally for reliability.
345    pub async fn type_char(&self, c: char) -> Result<()> {
346        self.type_text(&c.to_string()).await
347    }
348
349    /// Types a text string character by character.
350    ///
351    /// Each character goes through full keyboard event sequence.
352    /// This is slower but more realistic than `set_value`.
353    ///
354    /// # Example
355    ///
356    /// ```ignore
357    /// element.type_text("Hello, World!").await?;
358    /// ```
359    pub async fn type_text(&self, text: &str) -> Result<()> {
360        debug!(element_id = %self.inner.id, text_len = text.len(), "Typing text");
361
362        let command = Command::Input(InputCommand::TypeText {
363            element_id: self.inner.id.clone(),
364            text: text.to_string(),
365        });
366
367        self.send_command(command).await?;
368        Ok(())
369    }
370}
371
372// ============================================================================
373// Element - Mouse Input
374// ============================================================================
375
376impl Element {
377    /// Clicks the element using mouse events.
378    ///
379    /// Dispatches: mousemove → mousedown → mouseup → click.
380    /// This is more realistic than `click()` which uses `element.click()`.
381    ///
382    /// # Arguments
383    ///
384    /// * `button` - Mouse button (0=left, 1=middle, 2=right)
385    pub async fn mouse_click(&self, button: u8) -> Result<()> {
386        debug!(element_id = %self.inner.id, button = button, "Mouse clicking element");
387
388        let command = Command::Input(InputCommand::MouseClick {
389            element_id: Some(self.inner.id.clone()),
390            x: None,
391            y: None,
392            button,
393        });
394
395        self.send_command(command).await?;
396        Ok(())
397    }
398
399    /// Double-clicks the element.
400    ///
401    /// Dispatches two click sequences followed by dblclick event.
402    pub async fn double_click(&self) -> Result<()> {
403        debug!(element_id = %self.inner.id, "Double clicking element");
404
405        self.call_method(
406            "dispatchEvent",
407            vec![serde_json::json!({"type": "dblclick", "bubbles": true, "cancelable": true})],
408        )
409        .await?;
410        Ok(())
411    }
412
413    /// Right-clicks the element (context menu click).
414    ///
415    /// Dispatches contextmenu event.
416    pub async fn context_click(&self) -> Result<()> {
417        debug!(element_id = %self.inner.id, "Context clicking element");
418        self.mouse_click(2).await
419    }
420
421    /// Hovers over the element.
422    ///
423    /// Moves mouse to element center and dispatches mouseenter/mouseover events.
424    pub async fn hover(&self) -> Result<()> {
425        debug!(element_id = %self.inner.id, "Hovering over element");
426        self.mouse_move().await
427    }
428
429    /// Moves mouse to the element center.
430    pub async fn mouse_move(&self) -> Result<()> {
431        debug!(element_id = %self.inner.id, "Moving mouse to element");
432
433        let command = Command::Input(InputCommand::MouseMove {
434            element_id: Some(self.inner.id.clone()),
435            x: None,
436            y: None,
437        });
438
439        self.send_command(command).await?;
440        Ok(())
441    }
442
443    /// Presses mouse button down on the element (without release).
444    ///
445    /// Dispatches only mousedown event.
446    /// Use with `mouse_up()` for drag operations.
447    ///
448    /// # Arguments
449    ///
450    /// * `button` - Mouse button (0=left, 1=middle, 2=right)
451    pub async fn mouse_down(&self, button: u8) -> Result<()> {
452        debug!(element_id = %self.inner.id, button = button, "Mouse down on element");
453
454        let command = Command::Input(InputCommand::MouseDown {
455            element_id: Some(self.inner.id.clone()),
456            x: None,
457            y: None,
458            button,
459        });
460
461        self.send_command(command).await?;
462        Ok(())
463    }
464
465    /// Releases mouse button on the element.
466    ///
467    /// Dispatches only mouseup event.
468    /// Use with `mouse_down()` for drag operations.
469    ///
470    /// # Arguments
471    ///
472    /// * `button` - Mouse button (0=left, 1=middle, 2=right)
473    pub async fn mouse_up(&self, button: u8) -> Result<()> {
474        debug!(element_id = %self.inner.id, button = button, "Mouse up on element");
475
476        let command = Command::Input(InputCommand::MouseUp {
477            element_id: Some(self.inner.id.clone()),
478            x: None,
479            y: None,
480            button,
481        });
482
483        self.send_command(command).await?;
484        Ok(())
485    }
486}
487
488// ============================================================================
489// Element - Scroll
490// ============================================================================
491
492impl Element {
493    /// Scrolls the element into view.
494    ///
495    /// Uses `element.scrollIntoView()` with smooth behavior.
496    pub async fn scroll_into_view(&self) -> Result<()> {
497        debug!(element_id = %self.inner.id, "Scrolling element into view");
498
499        self.call_method(
500            "scrollIntoView",
501            vec![serde_json::json!({"behavior": "smooth", "block": "center"})],
502        )
503        .await?;
504        Ok(())
505    }
506
507    /// Scrolls the element into view immediately (no smooth animation).
508    pub async fn scroll_into_view_instant(&self) -> Result<()> {
509        debug!(element_id = %self.inner.id, "Scrolling element into view (instant)");
510
511        self.call_method(
512            "scrollIntoView",
513            vec![serde_json::json!({"behavior": "instant", "block": "center"})],
514        )
515        .await?;
516        Ok(())
517    }
518
519    /// Gets the element's bounding rectangle.
520    ///
521    /// # Returns
522    ///
523    /// Tuple of (x, y, width, height) in pixels.
524    pub async fn get_bounding_rect(&self) -> Result<(f64, f64, f64, f64)> {
525        let result = self.call_method("getBoundingClientRect", vec![]).await?;
526
527        let x = result.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0);
528        let y = result.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0);
529        let width = result.get("width").and_then(|v| v.as_f64()).unwrap_or(0.0);
530        let height = result.get("height").and_then(|v| v.as_f64()).unwrap_or(0.0);
531
532        debug!(element_id = %self.inner.id, x = x, y = y, width = width, height = height, "Got bounding rect");
533        Ok((x, y, width, height))
534    }
535}
536
537// ============================================================================
538// Element - Checkbox/Radio
539// ============================================================================
540
541impl Element {
542    /// Checks if the element is checked (for checkboxes/radio buttons).
543    pub async fn is_checked(&self) -> Result<bool> {
544        let value = self.get_property("checked").await?;
545        Ok(value.as_bool().unwrap_or(false))
546    }
547
548    /// Checks the checkbox/radio button.
549    ///
550    /// Does nothing if already checked.
551    pub async fn check(&self) -> Result<()> {
552        if !self.is_checked().await? {
553            self.click().await?;
554        }
555        Ok(())
556    }
557
558    /// Unchecks the checkbox.
559    ///
560    /// Does nothing if already unchecked.
561    pub async fn uncheck(&self) -> Result<()> {
562        if self.is_checked().await? {
563            self.click().await?;
564        }
565        Ok(())
566    }
567
568    /// Toggles the checkbox state.
569    pub async fn toggle(&self) -> Result<()> {
570        self.click().await
571    }
572
573    /// Sets the checked state.
574    pub async fn set_checked(&self, checked: bool) -> Result<()> {
575        if checked {
576            self.check().await
577        } else {
578            self.uncheck().await
579        }
580    }
581}
582
583// ============================================================================
584// Element - Select/Dropdown
585// ============================================================================
586
587impl Element {
588    /// Selects an option by visible text (for `<select>` elements).
589    ///
590    /// # Example
591    ///
592    /// ```ignore
593    /// let select = tab.find_element(By::css("select#country")).await?;
594    /// select.select_by_text("United States").await?;
595    /// ```
596    pub async fn select_by_text(&self, text: &str) -> Result<()> {
597        let escaped = serde_json::to_string(text).unwrap_or_else(|_| format!("\"{}\"", text));
598        let script = format!(
599            r#"const select = arguments[0];
600            for (const opt of select.options) {{
601                if (opt.textContent.trim() === {escaped}) {{
602                    opt.selected = true;
603                    select.dispatchEvent(new Event('change', {{bubbles: true}}));
604                    return true;
605                }}
606            }}
607            return false;"#
608        );
609
610        let command = Command::Script(ScriptCommand::Evaluate {
611            script,
612            args: vec![serde_json::json!({"elementId": self.inner.id.as_str()})],
613        });
614
615        let response = self.send_command(command).await?;
616
617        let found = response
618            .result
619            .as_ref()
620            .and_then(|v| v.get("value"))
621            .and_then(|v| v.as_bool())
622            .unwrap_or(false);
623
624        if !found {
625            return Err(Error::invalid_argument(format!(
626                "Option with text '{}' not found",
627                text
628            )));
629        }
630
631        Ok(())
632    }
633
634    /// Selects an option by value attribute (for `<select>` elements).
635    pub async fn select_by_value(&self, value: &str) -> Result<()> {
636        self.set_property("value", Value::String(value.to_string()))
637            .await?;
638        self.call_method(
639            "dispatchEvent",
640            vec![serde_json::json!({"type": "change", "bubbles": true})],
641        )
642        .await?;
643        Ok(())
644    }
645
646    /// Selects an option by index (for `<select>` elements).
647    pub async fn select_by_index(&self, index: usize) -> Result<()> {
648        self.set_property("selectedIndex", Value::Number(index.into()))
649            .await?;
650        self.call_method(
651            "dispatchEvent",
652            vec![serde_json::json!({"type": "change", "bubbles": true})],
653        )
654        .await?;
655        Ok(())
656    }
657
658    /// Gets the selected option's value (for `<select>` elements).
659    pub async fn get_selected_value(&self) -> Result<Option<String>> {
660        let value = self.get_property("value").await?;
661        Ok(value.as_str().map(|s| s.to_string()))
662    }
663
664    /// Gets the selected option's index (for `<select>` elements).
665    pub async fn get_selected_index(&self) -> Result<i64> {
666        let value = self.get_property("selectedIndex").await?;
667        Ok(value.as_i64().unwrap_or(-1))
668    }
669
670    /// Gets the selected option's text (for `<select>` elements).
671    pub async fn get_selected_text(&self) -> Result<Option<String>> {
672        let options = self.find_elements(By::css("option:checked")).await?;
673        if let Some(option) = options.first() {
674            let text = option.get_text().await?;
675            return Ok(Some(text));
676        }
677        Ok(None)
678    }
679
680    /// Checks if this is a multi-select element.
681    pub async fn is_multiple(&self) -> Result<bool> {
682        let value = self.get_property("multiple").await?;
683        Ok(value.as_bool().unwrap_or(false))
684    }
685}
686
687// ============================================================================
688// Element - Nested Search
689// ============================================================================
690
691impl Element {
692    /// Finds a child element using a locator strategy.
693    ///
694    /// # Example
695    ///
696    /// ```ignore
697    /// use firefox_webdriver::By;
698    ///
699    /// let form = tab.find_element(By::Id("login-form")).await?;
700    /// let btn = form.find_element(By::Css("button[type='submit']")).await?;
701    /// ```
702    pub async fn find_element(&self, by: By) -> Result<Element> {
703        let command = Command::Element(ElementCommand::Find {
704            strategy: by.strategy().to_string(),
705            value: by.value().to_string(),
706            parent_id: Some(self.inner.id.clone()),
707        });
708
709        let response = self.send_command(command).await?;
710
711        let element_id = response
712            .result
713            .as_ref()
714            .and_then(|v| v.get("elementId"))
715            .and_then(|v| v.as_str())
716            .ok_or_else(|| {
717                Error::element_not_found(
718                    format!("{}:{}", by.strategy(), by.value()),
719                    self.inner.tab_id,
720                    self.inner.frame_id,
721                )
722            })?;
723
724        Ok(Element::new(
725            ElementId::new(element_id),
726            self.inner.tab_id,
727            self.inner.frame_id,
728            self.inner.session_id,
729            self.inner.window.clone(),
730        ))
731    }
732
733    /// Finds all child elements using a locator strategy.
734    ///
735    /// # Example
736    ///
737    /// ```ignore
738    /// use firefox_webdriver::By;
739    ///
740    /// let form = tab.find_element(By::Id("login-form")).await?;
741    /// let inputs = form.find_elements(By::Tag("input")).await?;
742    /// ```
743    pub async fn find_elements(&self, by: By) -> Result<Vec<Element>> {
744        let command = Command::Element(ElementCommand::FindAll {
745            strategy: by.strategy().to_string(),
746            value: by.value().to_string(),
747            parent_id: Some(self.inner.id.clone()),
748        });
749
750        let response = self.send_command(command).await?;
751
752        let element_ids = response
753            .result
754            .as_ref()
755            .and_then(|v| v.get("elementIds"))
756            .and_then(|v| v.as_array())
757            .map(|arr| {
758                arr.iter()
759                    .filter_map(|v| v.as_str())
760                    .map(|id| {
761                        Element::new(
762                            ElementId::new(id),
763                            self.inner.tab_id,
764                            self.inner.frame_id,
765                            self.inner.session_id,
766                            self.inner.window.clone(),
767                        )
768                    })
769                    .collect()
770            })
771            .unwrap_or_default();
772
773        Ok(element_ids)
774    }
775}
776
777// ============================================================================
778// Element - Generic Property Access
779// ============================================================================
780
781impl Element {
782    /// Gets a property value via `element[name]`.
783    ///
784    /// # Arguments
785    ///
786    /// * `name` - Property name (e.g., "value", "textContent")
787    pub async fn get_property(&self, name: &str) -> Result<Value> {
788        let command = Command::Element(ElementCommand::GetProperty {
789            element_id: self.inner.id.clone(),
790            name: name.to_string(),
791        });
792
793        let response = self.send_command(command).await?;
794
795        Ok(response
796            .result
797            .and_then(|v| v.get("value").cloned())
798            .unwrap_or(Value::Null))
799    }
800
801    /// Sets a property value via `element[name] = value`.
802    ///
803    /// # Arguments
804    ///
805    /// * `name` - Property name
806    /// * `value` - Value to set
807    pub async fn set_property(&self, name: &str, value: Value) -> Result<()> {
808        let command = Command::Element(ElementCommand::SetProperty {
809            element_id: self.inner.id.clone(),
810            name: name.to_string(),
811            value,
812        });
813
814        self.send_command(command).await?;
815        Ok(())
816    }
817
818    /// Calls a method via `element[name](...args)`.
819    ///
820    /// # Arguments
821    ///
822    /// * `name` - Method name
823    /// * `args` - Method arguments
824    pub async fn call_method(&self, name: &str, args: Vec<Value>) -> Result<Value> {
825        let command = Command::Element(ElementCommand::CallMethod {
826            element_id: self.inner.id.clone(),
827            name: name.to_string(),
828            args,
829        });
830
831        let response = self.send_command(command).await?;
832
833        Ok(response
834            .result
835            .and_then(|v| v.get("value").cloned())
836            .unwrap_or(Value::Null))
837    }
838}
839
840// ============================================================================
841// Element - Screenshot
842// ============================================================================
843
844impl Element {
845    /// Captures a PNG screenshot of this element.
846    ///
847    /// Returns base64-encoded image data.
848    ///
849    /// # Example
850    ///
851    /// ```ignore
852    /// let element = tab.find_element("#chart").await?;
853    /// let screenshot = element.screenshot().await?;
854    /// ```
855    pub async fn screenshot(&self) -> Result<String> {
856        self.screenshot_with_format("png", None).await
857    }
858
859    /// Captures a JPEG screenshot of this element with specified quality.
860    ///
861    /// # Arguments
862    ///
863    /// * `quality` - JPEG quality (0-100)
864    pub async fn screenshot_jpeg(&self, quality: u8) -> Result<String> {
865        self.screenshot_with_format("jpeg", Some(quality.min(100)))
866            .await
867    }
868
869    /// Captures a screenshot with specified format.
870    ///
871    /// The extension returns full page screenshot + clip info.
872    /// Rust handles the cropping to avoid canvas security issues.
873    async fn screenshot_with_format(&self, format: &str, quality: Option<u8>) -> Result<String> {
874        use base64::Engine;
875        use base64::engine::general_purpose::STANDARD as Base64Standard;
876        use image::GenericImageView;
877
878        let command = Command::Element(ElementCommand::CaptureScreenshot {
879            element_id: self.inner.id.clone(),
880            format: format.to_string(),
881            quality,
882        });
883
884        let response = self.send_command(command).await?;
885
886        tracing::debug!(response = ?response, "Element screenshot response");
887
888        let result = response.result.as_ref().ok_or_else(|| {
889            let error_str = response.error.as_deref().unwrap_or("none");
890            let msg_str = response.message.as_deref().unwrap_or("none");
891            Error::script_error(format!(
892                "Element screenshot failed. error={}, message={}",
893                error_str, msg_str
894            ))
895        })?;
896
897        let data = result
898            .get("data")
899            .and_then(|v| v.as_str())
900            .ok_or_else(|| Error::script_error("Screenshot response missing data field"))?;
901
902        // Check if clip info is provided (new format)
903        if let Some(clip) = result.get("clip") {
904            let x = clip.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32;
905            let y = clip.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32;
906            let width = clip.get("width").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32;
907            let height = clip.get("height").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32;
908            let scale = clip.get("scale").and_then(|v| v.as_f64()).unwrap_or(1.0);
909
910            // Apply scale to coordinates
911            let x = (x as f64 * scale) as u32;
912            let y = (y as f64 * scale) as u32;
913            let width = (width as f64 * scale) as u32;
914            let height = (height as f64 * scale) as u32;
915
916            if width == 0 || height == 0 {
917                return Err(Error::script_error("Element has zero dimensions"));
918            }
919
920            // Decode full page image
921            let image_bytes = Base64Standard
922                .decode(data)
923                .map_err(|e| Error::script_error(format!("Failed to decode base64: {}", e)))?;
924
925            let img = image::load_from_memory(&image_bytes)
926                .map_err(|e| Error::script_error(format!("Failed to load image: {}", e)))?;
927
928            // Clamp crop region to image bounds
929            let (img_width, img_height) = img.dimensions();
930            let x = x.min(img_width.saturating_sub(1));
931            let y = y.min(img_height.saturating_sub(1));
932            let width = width.min(img_width.saturating_sub(x));
933            let height = height.min(img_height.saturating_sub(y));
934
935            // Crop
936            let cropped = img.crop_imm(x, y, width, height);
937
938            // Encode back to base64
939            let mut output = std::io::Cursor::new(Vec::new());
940            match format {
941                "jpeg" => {
942                    use image::codecs::jpeg::JpegEncoder;
943                    let q = quality.unwrap_or(85);
944                    let encoder = JpegEncoder::new_with_quality(&mut output, q);
945                    cropped.write_with_encoder(encoder).map_err(|e| {
946                        Error::script_error(format!("Failed to encode JPEG: {}", e))
947                    })?;
948                }
949                _ => {
950                    cropped
951                        .write_to(&mut output, image::ImageFormat::Png)
952                        .map_err(|e| Error::script_error(format!("Failed to encode PNG: {}", e)))?;
953                }
954            }
955
956            Ok(Base64Standard.encode(output.into_inner()))
957        } else {
958            // Old format: data is already cropped
959            Ok(data.to_string())
960        }
961    }
962
963    /// Captures a screenshot and returns raw bytes.
964    pub async fn screenshot_bytes(&self) -> Result<Vec<u8>> {
965        use base64::Engine;
966        use base64::engine::general_purpose::STANDARD as Base64Standard;
967
968        let base64_data = self.screenshot().await?;
969        Base64Standard
970            .decode(&base64_data)
971            .map_err(|e| Error::script_error(format!("Failed to decode base64: {}", e)))
972    }
973
974    /// Captures a screenshot and saves to a file.
975    ///
976    /// Format is determined by file extension (.png or .jpg/.jpeg).
977    pub async fn save_screenshot(&self, path: impl AsRef<std::path::Path>) -> Result<()> {
978        use base64::Engine;
979        use base64::engine::general_purpose::STANDARD as Base64Standard;
980
981        let path = path.as_ref();
982        let ext = path
983            .extension()
984            .and_then(|e| e.to_str())
985            .unwrap_or("png")
986            .to_lowercase();
987
988        let base64_data = match ext.as_str() {
989            "jpg" | "jpeg" => self.screenshot_jpeg(85).await?,
990            _ => self.screenshot().await?,
991        };
992
993        let bytes = Base64Standard
994            .decode(&base64_data)
995            .map_err(|e| Error::script_error(format!("Failed to decode base64: {}", e)))?;
996
997        tokio::fs::write(path, bytes).await.map_err(Error::Io)?;
998        Ok(())
999    }
1000}
1001
1002// ============================================================================
1003// Element - Internal
1004// ============================================================================
1005
1006impl Element {
1007    /// Sends a command and returns the response.
1008    async fn send_command(&self, command: Command) -> Result<Response> {
1009        let window = self
1010            .inner
1011            .window
1012            .as_ref()
1013            .ok_or_else(|| Error::protocol("Element has no associated window"))?;
1014
1015        let request = Request::new(self.inner.tab_id, self.inner.frame_id, command);
1016
1017        window
1018            .inner
1019            .pool
1020            .send(window.inner.session_id, request)
1021            .await
1022    }
1023}
1024
1025// ============================================================================
1026// Tests
1027// ============================================================================
1028
1029#[cfg(test)]
1030mod tests {
1031    use super::Element;
1032
1033    #[test]
1034    fn test_element_is_clone() {
1035        fn assert_clone<T: Clone>() {}
1036        assert_clone::<Element>();
1037    }
1038
1039    #[test]
1040    fn test_element_is_debug() {
1041        fn assert_debug<T: std::fmt::Debug>() {}
1042        assert_debug::<Element>();
1043    }
1044}