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};
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    /// Returns `false` if `offsetParent` is null (element is hidden).
236    pub async fn is_displayed(&self) -> Result<bool> {
237        let offset_parent = self.get_property("offsetParent").await?;
238        Ok(!offset_parent.is_null())
239    }
240
241    /// Checks if the element is enabled.
242    ///
243    /// Returns `true` if `disabled` property is false or absent.
244    pub async fn is_enabled(&self) -> Result<bool> {
245        let disabled = self.get_property("disabled").await?;
246        Ok(!disabled.as_bool().unwrap_or(false))
247    }
248}
249
250// ============================================================================
251// Element - Keyboard Input
252// ============================================================================
253
254impl Element {
255    /// Presses a navigation/control key.
256    ///
257    /// For typing text, use [`type_text`](Self::type_text) instead.
258    ///
259    /// # Example
260    ///
261    /// ```ignore
262    /// use firefox_webdriver::Key;
263    ///
264    /// element.press(Key::Enter).await?;
265    /// element.press(Key::Tab).await?;
266    /// element.press(Key::Backspace).await?;
267    /// ```
268    pub async fn press(&self, key: Key) -> Result<()> {
269        let (key_str, code, key_code, printable) = key.properties();
270        self.type_key(
271            key_str, code, key_code, printable, false, false, false, false,
272        )
273        .await
274    }
275
276    /// Types a single key with optional modifiers (low-level API).
277    ///
278    /// Prefer using [`press`](Self::press) for common keys or [`type_text`](Self::type_text) for text.
279    ///
280    /// Dispatches full keyboard event sequence: keydown → input → keypress → keyup.
281    ///
282    /// # Arguments
283    ///
284    /// * `key` - Key value (e.g., "a", "Enter")
285    /// * `code` - Key code (e.g., "KeyA", "Enter")
286    /// * `key_code` - Legacy keyCode number
287    /// * `printable` - Whether key produces visible output
288    /// * `ctrl` - Ctrl modifier
289    /// * `shift` - Shift modifier
290    /// * `alt` - Alt modifier
291    /// * `meta` - Meta modifier
292    #[allow(clippy::too_many_arguments)]
293    pub async fn type_key(
294        &self,
295        key: &str,
296        code: &str,
297        key_code: u32,
298        printable: bool,
299        ctrl: bool,
300        shift: bool,
301        alt: bool,
302        meta: bool,
303    ) -> Result<()> {
304        let command = Command::Input(InputCommand::TypeKey {
305            element_id: self.inner.id.clone(),
306            key: key.to_string(),
307            code: code.to_string(),
308            key_code,
309            printable,
310            ctrl,
311            shift,
312            alt,
313            meta,
314        });
315
316        self.send_command(command).await?;
317        Ok(())
318    }
319
320    /// Types a character with default key properties.
321    ///
322    /// Convenience method that uses `type_text` internally for reliability.
323    pub async fn type_char(&self, c: char) -> Result<()> {
324        self.type_text(&c.to_string()).await
325    }
326
327    /// Types a text string character by character.
328    ///
329    /// Each character goes through full keyboard event sequence.
330    /// This is slower but more realistic than `set_value`.
331    ///
332    /// # Example
333    ///
334    /// ```ignore
335    /// element.type_text("Hello, World!").await?;
336    /// ```
337    pub async fn type_text(&self, text: &str) -> Result<()> {
338        debug!(element_id = %self.inner.id, text_len = text.len(), "Typing text");
339
340        let command = Command::Input(InputCommand::TypeText {
341            element_id: self.inner.id.clone(),
342            text: text.to_string(),
343        });
344
345        self.send_command(command).await?;
346        Ok(())
347    }
348}
349
350// ============================================================================
351// Element - Mouse Input
352// ============================================================================
353
354impl Element {
355    /// Clicks the element using mouse events.
356    ///
357    /// Dispatches: mousemove → mousedown → mouseup → click.
358    /// This is more realistic than `click()` which uses `element.click()`.
359    ///
360    /// # Arguments
361    ///
362    /// * `button` - Mouse button (0=left, 1=middle, 2=right)
363    pub async fn mouse_click(&self, button: u8) -> Result<()> {
364        debug!(element_id = %self.inner.id, button = button, "Mouse clicking element");
365
366        let command = Command::Input(InputCommand::MouseClick {
367            element_id: Some(self.inner.id.clone()),
368            x: None,
369            y: None,
370            button,
371        });
372
373        self.send_command(command).await?;
374        Ok(())
375    }
376
377    /// Double-clicks the element.
378    ///
379    /// Dispatches two click sequences followed by dblclick event.
380    pub async fn double_click(&self) -> Result<()> {
381        debug!(element_id = %self.inner.id, "Double clicking element");
382
383        self.call_method(
384            "dispatchEvent",
385            vec![serde_json::json!({"type": "dblclick", "bubbles": true, "cancelable": true})],
386        )
387        .await?;
388        Ok(())
389    }
390
391    /// Right-clicks the element (context menu click).
392    ///
393    /// Dispatches contextmenu event.
394    pub async fn context_click(&self) -> Result<()> {
395        debug!(element_id = %self.inner.id, "Context clicking element");
396        self.mouse_click(2).await
397    }
398
399    /// Hovers over the element.
400    ///
401    /// Moves mouse to element center and dispatches mouseenter/mouseover events.
402    pub async fn hover(&self) -> Result<()> {
403        debug!(element_id = %self.inner.id, "Hovering over element");
404        self.mouse_move().await
405    }
406
407    /// Moves mouse to the element center.
408    pub async fn mouse_move(&self) -> Result<()> {
409        debug!(element_id = %self.inner.id, "Moving mouse to element");
410
411        let command = Command::Input(InputCommand::MouseMove {
412            element_id: Some(self.inner.id.clone()),
413            x: None,
414            y: None,
415        });
416
417        self.send_command(command).await?;
418        Ok(())
419    }
420
421    /// Presses mouse button down on the element (without release).
422    ///
423    /// Dispatches only mousedown event.
424    /// Use with `mouse_up()` for drag operations.
425    ///
426    /// # Arguments
427    ///
428    /// * `button` - Mouse button (0=left, 1=middle, 2=right)
429    pub async fn mouse_down(&self, button: u8) -> Result<()> {
430        debug!(element_id = %self.inner.id, button = button, "Mouse down on element");
431
432        let command = Command::Input(InputCommand::MouseDown {
433            element_id: Some(self.inner.id.clone()),
434            x: None,
435            y: None,
436            button,
437        });
438
439        self.send_command(command).await?;
440        Ok(())
441    }
442
443    /// Releases mouse button on the element.
444    ///
445    /// Dispatches only mouseup event.
446    /// Use with `mouse_down()` for drag operations.
447    ///
448    /// # Arguments
449    ///
450    /// * `button` - Mouse button (0=left, 1=middle, 2=right)
451    pub async fn mouse_up(&self, button: u8) -> Result<()> {
452        debug!(element_id = %self.inner.id, button = button, "Mouse up on element");
453
454        let command = Command::Input(InputCommand::MouseUp {
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
466// ============================================================================
467// Element - Scroll
468// ============================================================================
469
470impl Element {
471    /// Scrolls the element into view.
472    ///
473    /// Uses `element.scrollIntoView()` with smooth behavior.
474    pub async fn scroll_into_view(&self) -> Result<()> {
475        debug!(element_id = %self.inner.id, "Scrolling element into view");
476
477        self.call_method(
478            "scrollIntoView",
479            vec![serde_json::json!({"behavior": "smooth", "block": "center"})],
480        )
481        .await?;
482        Ok(())
483    }
484
485    /// Scrolls the element into view immediately (no smooth animation).
486    pub async fn scroll_into_view_instant(&self) -> Result<()> {
487        debug!(element_id = %self.inner.id, "Scrolling element into view (instant)");
488
489        self.call_method(
490            "scrollIntoView",
491            vec![serde_json::json!({"behavior": "instant", "block": "center"})],
492        )
493        .await?;
494        Ok(())
495    }
496
497    /// Gets the element's bounding rectangle.
498    ///
499    /// # Returns
500    ///
501    /// Tuple of (x, y, width, height) in pixels.
502    pub async fn get_bounding_rect(&self) -> Result<(f64, f64, f64, f64)> {
503        let result = self.call_method("getBoundingClientRect", vec![]).await?;
504
505        let x = result.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0);
506        let y = result.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0);
507        let width = result.get("width").and_then(|v| v.as_f64()).unwrap_or(0.0);
508        let height = result.get("height").and_then(|v| v.as_f64()).unwrap_or(0.0);
509
510        debug!(element_id = %self.inner.id, x = x, y = y, width = width, height = height, "Got bounding rect");
511        Ok((x, y, width, height))
512    }
513}
514
515// ============================================================================
516// Element - Checkbox/Radio
517// ============================================================================
518
519impl Element {
520    /// Checks if the element is checked (for checkboxes/radio buttons).
521    pub async fn is_checked(&self) -> Result<bool> {
522        let value = self.get_property("checked").await?;
523        Ok(value.as_bool().unwrap_or(false))
524    }
525
526    /// Checks the checkbox/radio button.
527    ///
528    /// Does nothing if already checked.
529    pub async fn check(&self) -> Result<()> {
530        if !self.is_checked().await? {
531            self.click().await?;
532        }
533        Ok(())
534    }
535
536    /// Unchecks the checkbox.
537    ///
538    /// Does nothing if already unchecked.
539    pub async fn uncheck(&self) -> Result<()> {
540        if self.is_checked().await? {
541            self.click().await?;
542        }
543        Ok(())
544    }
545
546    /// Toggles the checkbox state.
547    pub async fn toggle(&self) -> Result<()> {
548        self.click().await
549    }
550
551    /// Sets the checked state.
552    pub async fn set_checked(&self, checked: bool) -> Result<()> {
553        if checked {
554            self.check().await
555        } else {
556            self.uncheck().await
557        }
558    }
559}
560
561// ============================================================================
562// Element - Select/Dropdown
563// ============================================================================
564
565impl Element {
566    /// Selects an option by visible text (for `<select>` elements).
567    ///
568    /// # Example
569    ///
570    /// ```ignore
571    /// let select = tab.find_element(By::css("select#country")).await?;
572    /// select.select_by_text("United States").await?;
573    /// ```
574    pub async fn select_by_text(&self, text: &str) -> Result<()> {
575        // Find and click the option
576        if let Ok(options) = self.find_elements(By::tag("option")).await {
577            for option in options {
578                if let Ok(option_text) = option.get_text().await
579                    && option_text.trim() == text
580                {
581                    option.set_property("selected", Value::Bool(true)).await?;
582                    // Trigger change event
583                    self.call_method(
584                        "dispatchEvent",
585                        vec![serde_json::json!({"type": "change", "bubbles": true})],
586                    )
587                    .await?;
588                    return Ok(());
589                }
590            }
591        }
592
593        Err(Error::invalid_argument(format!(
594            "Option with text '{}' not found",
595            text
596        )))
597    }
598
599    /// Selects an option by value attribute (for `<select>` elements).
600    pub async fn select_by_value(&self, value: &str) -> Result<()> {
601        self.set_property("value", Value::String(value.to_string()))
602            .await?;
603        self.call_method(
604            "dispatchEvent",
605            vec![serde_json::json!({"type": "change", "bubbles": true})],
606        )
607        .await?;
608        Ok(())
609    }
610
611    /// Selects an option by index (for `<select>` elements).
612    pub async fn select_by_index(&self, index: usize) -> Result<()> {
613        self.set_property("selectedIndex", Value::Number(index.into()))
614            .await?;
615        self.call_method(
616            "dispatchEvent",
617            vec![serde_json::json!({"type": "change", "bubbles": true})],
618        )
619        .await?;
620        Ok(())
621    }
622
623    /// Gets the selected option's value (for `<select>` elements).
624    pub async fn get_selected_value(&self) -> Result<Option<String>> {
625        let value = self.get_property("value").await?;
626        Ok(value.as_str().map(|s| s.to_string()))
627    }
628
629    /// Gets the selected option's index (for `<select>` elements).
630    pub async fn get_selected_index(&self) -> Result<i64> {
631        let value = self.get_property("selectedIndex").await?;
632        Ok(value.as_i64().unwrap_or(-1))
633    }
634
635    /// Gets the selected option's text (for `<select>` elements).
636    pub async fn get_selected_text(&self) -> Result<Option<String>> {
637        let options = self.find_elements(By::css("option:checked")).await?;
638        if let Some(option) = options.first() {
639            let text = option.get_text().await?;
640            return Ok(Some(text));
641        }
642        Ok(None)
643    }
644
645    /// Checks if this is a multi-select element.
646    pub async fn is_multiple(&self) -> Result<bool> {
647        let value = self.get_property("multiple").await?;
648        Ok(value.as_bool().unwrap_or(false))
649    }
650}
651
652// ============================================================================
653// Element - Nested Search
654// ============================================================================
655
656impl Element {
657    /// Finds a child element using a locator strategy.
658    ///
659    /// # Example
660    ///
661    /// ```ignore
662    /// use firefox_webdriver::By;
663    ///
664    /// let form = tab.find_element(By::Id("login-form")).await?;
665    /// let btn = form.find_element(By::Css("button[type='submit']")).await?;
666    /// ```
667    pub async fn find_element(&self, by: By) -> Result<Element> {
668        let command = Command::Element(ElementCommand::Find {
669            strategy: by.strategy().to_string(),
670            value: by.value().to_string(),
671            parent_id: Some(self.inner.id.clone()),
672        });
673
674        let response = self.send_command(command).await?;
675
676        let element_id = response
677            .result
678            .as_ref()
679            .and_then(|v| v.get("elementId"))
680            .and_then(|v| v.as_str())
681            .ok_or_else(|| {
682                Error::element_not_found(
683                    format!("{}:{}", by.strategy(), by.value()),
684                    self.inner.tab_id,
685                    self.inner.frame_id,
686                )
687            })?;
688
689        Ok(Element::new(
690            ElementId::new(element_id),
691            self.inner.tab_id,
692            self.inner.frame_id,
693            self.inner.session_id,
694            self.inner.window.clone(),
695        ))
696    }
697
698    /// Finds all child elements using a locator strategy.
699    ///
700    /// # Example
701    ///
702    /// ```ignore
703    /// use firefox_webdriver::By;
704    ///
705    /// let form = tab.find_element(By::Id("login-form")).await?;
706    /// let inputs = form.find_elements(By::Tag("input")).await?;
707    /// ```
708    pub async fn find_elements(&self, by: By) -> Result<Vec<Element>> {
709        let command = Command::Element(ElementCommand::FindAll {
710            strategy: by.strategy().to_string(),
711            value: by.value().to_string(),
712            parent_id: Some(self.inner.id.clone()),
713        });
714
715        let response = self.send_command(command).await?;
716
717        let element_ids = response
718            .result
719            .as_ref()
720            .and_then(|v| v.get("elementIds"))
721            .and_then(|v| v.as_array())
722            .map(|arr| {
723                arr.iter()
724                    .filter_map(|v| v.as_str())
725                    .map(|id| {
726                        Element::new(
727                            ElementId::new(id),
728                            self.inner.tab_id,
729                            self.inner.frame_id,
730                            self.inner.session_id,
731                            self.inner.window.clone(),
732                        )
733                    })
734                    .collect()
735            })
736            .unwrap_or_default();
737
738        Ok(element_ids)
739    }
740}
741
742// ============================================================================
743// Element - Generic Property Access
744// ============================================================================
745
746impl Element {
747    /// Gets a property value via `element[name]`.
748    ///
749    /// # Arguments
750    ///
751    /// * `name` - Property name (e.g., "value", "textContent")
752    pub async fn get_property(&self, name: &str) -> Result<Value> {
753        let command = Command::Element(ElementCommand::GetProperty {
754            element_id: self.inner.id.clone(),
755            name: name.to_string(),
756        });
757
758        let response = self.send_command(command).await?;
759
760        Ok(response
761            .result
762            .and_then(|v| v.get("value").cloned())
763            .unwrap_or(Value::Null))
764    }
765
766    /// Sets a property value via `element[name] = value`.
767    ///
768    /// # Arguments
769    ///
770    /// * `name` - Property name
771    /// * `value` - Value to set
772    pub async fn set_property(&self, name: &str, value: Value) -> Result<()> {
773        let command = Command::Element(ElementCommand::SetProperty {
774            element_id: self.inner.id.clone(),
775            name: name.to_string(),
776            value,
777        });
778
779        self.send_command(command).await?;
780        Ok(())
781    }
782
783    /// Calls a method via `element[name](...args)`.
784    ///
785    /// # Arguments
786    ///
787    /// * `name` - Method name
788    /// * `args` - Method arguments
789    pub async fn call_method(&self, name: &str, args: Vec<Value>) -> Result<Value> {
790        let command = Command::Element(ElementCommand::CallMethod {
791            element_id: self.inner.id.clone(),
792            name: name.to_string(),
793            args,
794        });
795
796        let response = self.send_command(command).await?;
797
798        Ok(response
799            .result
800            .and_then(|v| v.get("value").cloned())
801            .unwrap_or(Value::Null))
802    }
803}
804
805// ============================================================================
806// Element - Screenshot
807// ============================================================================
808
809impl Element {
810    /// Captures a PNG screenshot of this element.
811    ///
812    /// Returns base64-encoded image data.
813    ///
814    /// # Example
815    ///
816    /// ```ignore
817    /// let element = tab.find_element("#chart").await?;
818    /// let screenshot = element.screenshot().await?;
819    /// ```
820    pub async fn screenshot(&self) -> Result<String> {
821        self.screenshot_with_format("png", None).await
822    }
823
824    /// Captures a JPEG screenshot of this element with specified quality.
825    ///
826    /// # Arguments
827    ///
828    /// * `quality` - JPEG quality (0-100)
829    pub async fn screenshot_jpeg(&self, quality: u8) -> Result<String> {
830        self.screenshot_with_format("jpeg", Some(quality.min(100)))
831            .await
832    }
833
834    /// Captures a screenshot with specified format.
835    ///
836    /// The extension returns full page screenshot + clip info.
837    /// Rust handles the cropping to avoid canvas security issues.
838    async fn screenshot_with_format(&self, format: &str, quality: Option<u8>) -> Result<String> {
839        use base64::Engine;
840        use base64::engine::general_purpose::STANDARD as Base64Standard;
841        use image::GenericImageView;
842
843        let command = Command::Element(ElementCommand::CaptureScreenshot {
844            element_id: self.inner.id.clone(),
845            format: format.to_string(),
846            quality,
847        });
848
849        let response = self.send_command(command).await?;
850
851        tracing::debug!(response = ?response, "Element screenshot response");
852
853        let result = response.result.as_ref().ok_or_else(|| {
854            let error_str = response.error.as_deref().unwrap_or("none");
855            let msg_str = response.message.as_deref().unwrap_or("none");
856            Error::script_error(format!(
857                "Element screenshot failed. error={}, message={}",
858                error_str, msg_str
859            ))
860        })?;
861
862        let data = result
863            .get("data")
864            .and_then(|v| v.as_str())
865            .ok_or_else(|| Error::script_error("Screenshot response missing data field"))?;
866
867        // Check if clip info is provided (new format)
868        if let Some(clip) = result.get("clip") {
869            let x = clip.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32;
870            let y = clip.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32;
871            let width = clip.get("width").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32;
872            let height = clip.get("height").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32;
873            let scale = clip.get("scale").and_then(|v| v.as_f64()).unwrap_or(1.0);
874
875            // Apply scale to coordinates
876            let x = (x as f64 * scale) as u32;
877            let y = (y as f64 * scale) as u32;
878            let width = (width as f64 * scale) as u32;
879            let height = (height as f64 * scale) as u32;
880
881            if width == 0 || height == 0 {
882                return Err(Error::script_error("Element has zero dimensions"));
883            }
884
885            // Decode full page image
886            let image_bytes = Base64Standard
887                .decode(data)
888                .map_err(|e| Error::script_error(format!("Failed to decode base64: {}", e)))?;
889
890            let img = image::load_from_memory(&image_bytes)
891                .map_err(|e| Error::script_error(format!("Failed to load image: {}", e)))?;
892
893            // Clamp crop region to image bounds
894            let (img_width, img_height) = img.dimensions();
895            let x = x.min(img_width.saturating_sub(1));
896            let y = y.min(img_height.saturating_sub(1));
897            let width = width.min(img_width.saturating_sub(x));
898            let height = height.min(img_height.saturating_sub(y));
899
900            // Crop
901            let cropped = img.crop_imm(x, y, width, height);
902
903            // Encode back to base64
904            let mut output = std::io::Cursor::new(Vec::new());
905            match format {
906                "jpeg" => {
907                    let q = quality.unwrap_or(85);
908                    cropped
909                        .write_to(&mut output, image::ImageFormat::Jpeg)
910                        .map_err(|e| {
911                            Error::script_error(format!("Failed to encode JPEG: {}", e))
912                        })?;
913                    // Note: image crate doesn't support quality param directly in write_to
914                    // For proper quality control, would need jpeg encoder directly
915                    let _ = q; // suppress unused warning
916                }
917                _ => {
918                    cropped
919                        .write_to(&mut output, image::ImageFormat::Png)
920                        .map_err(|e| Error::script_error(format!("Failed to encode PNG: {}", e)))?;
921                }
922            }
923
924            Ok(Base64Standard.encode(output.into_inner()))
925        } else {
926            // Old format: data is already cropped
927            Ok(data.to_string())
928        }
929    }
930
931    /// Captures a screenshot and returns raw bytes.
932    pub async fn screenshot_bytes(&self) -> Result<Vec<u8>> {
933        use base64::Engine;
934        use base64::engine::general_purpose::STANDARD as Base64Standard;
935
936        let base64_data = self.screenshot().await?;
937        Base64Standard
938            .decode(&base64_data)
939            .map_err(|e| Error::script_error(format!("Failed to decode base64: {}", e)))
940    }
941
942    /// Captures a screenshot and saves to a file.
943    ///
944    /// Format is determined by file extension (.png or .jpg/.jpeg).
945    pub async fn save_screenshot(&self, path: impl AsRef<std::path::Path>) -> Result<()> {
946        use base64::Engine;
947        use base64::engine::general_purpose::STANDARD as Base64Standard;
948
949        let path = path.as_ref();
950        let ext = path
951            .extension()
952            .and_then(|e| e.to_str())
953            .unwrap_or("png")
954            .to_lowercase();
955
956        let base64_data = match ext.as_str() {
957            "jpg" | "jpeg" => self.screenshot_jpeg(85).await?,
958            _ => self.screenshot().await?,
959        };
960
961        let bytes = Base64Standard
962            .decode(&base64_data)
963            .map_err(|e| Error::script_error(format!("Failed to decode base64: {}", e)))?;
964
965        std::fs::write(path, bytes).map_err(Error::Io)?;
966        Ok(())
967    }
968}
969
970// ============================================================================
971// Element - Internal
972// ============================================================================
973
974impl Element {
975    /// Sends a command and returns the response.
976    async fn send_command(&self, command: Command) -> Result<Response> {
977        let window = self
978            .inner
979            .window
980            .as_ref()
981            .ok_or_else(|| Error::protocol("Element has no associated window"))?;
982
983        let request = Request::new(self.inner.tab_id, self.inner.frame_id, command);
984
985        window
986            .inner
987            .pool
988            .send(window.inner.session_id, request)
989            .await
990    }
991}
992
993// ============================================================================
994// Tests
995// ============================================================================
996
997#[cfg(test)]
998mod tests {
999    use super::Element;
1000
1001    #[test]
1002    fn test_element_is_clone() {
1003        fn assert_clone<T: Clone>() {}
1004        assert_clone::<Element>();
1005    }
1006
1007    #[test]
1008    fn test_element_is_debug() {
1009        fn assert_debug<T: std::fmt::Debug>() {}
1010        assert_debug::<Element>();
1011    }
1012}