Skip to main content

playwright_rs/protocol/
element_handle.rs

1// ElementHandle protocol object
2//
3// Represents a DOM element in the page. Supports element-specific operations like screenshots.
4// ElementHandles are created via query_selector methods and are protocol objects with GUIDs.
5
6use crate::error::Result;
7use crate::protocol::locator::BoundingBox;
8use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
9use base64::Engine;
10use serde::Deserialize;
11use serde_json::Value;
12use std::any::Any;
13use std::sync::Arc;
14
15/// ElementHandle represents a DOM element in the page.
16///
17/// ElementHandles are created via `page.query_selector()` or `frame.query_selector()`.
18/// They are protocol objects that allow element-specific operations like taking screenshots.
19///
20/// See: <https://playwright.dev/docs/api/class-elementhandle>
21#[derive(Clone)]
22pub struct ElementHandle {
23    base: ChannelOwnerImpl,
24}
25
26impl ElementHandle {
27    /// Creates a new ElementHandle from protocol initialization
28    ///
29    /// This is called by the object factory when the server sends a `__create__` message
30    /// for an ElementHandle object.
31    pub fn new(
32        parent: Arc<dyn ChannelOwner>,
33        type_name: String,
34        guid: Arc<str>,
35        initializer: Value,
36    ) -> Result<Self> {
37        let base = ChannelOwnerImpl::new(
38            ParentOrConnection::Parent(parent),
39            type_name,
40            guid,
41            initializer,
42        );
43
44        Ok(Self { base })
45    }
46
47    /// Takes a screenshot of the element and returns the image bytes.
48    ///
49    /// The screenshot is captured as PNG by default.
50    ///
51    /// # Example
52    ///
53    /// ```ignore
54    /// # use playwright_rs::protocol::Playwright;
55    /// # #[tokio::main]
56    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
57    /// let playwright = Playwright::launch().await?;
58    /// let browser = playwright.chromium().launch().await?;
59    /// let page = browser.new_page().await?;
60    /// page.goto("https://example.com", None).await?;
61    ///
62    /// let element = page.query_selector("h1").await?.expect("h1 not found");
63    /// let screenshot_bytes = element.screenshot(None).await?;
64    /// # Ok(())
65    /// # }
66    /// ```
67    ///
68    /// See: <https://playwright.dev/docs/api/class-elementhandle#element-handle-screenshot>
69    pub async fn screenshot(
70        &self,
71        options: Option<crate::protocol::ScreenshotOptions>,
72    ) -> Result<Vec<u8>> {
73        let params = if let Some(opts) = options {
74            opts.to_json()
75        } else {
76            // Default to PNG with required timeout
77            serde_json::json!({
78                "type": "png",
79                "timeout": crate::DEFAULT_TIMEOUT_MS
80            })
81        };
82
83        #[derive(Deserialize)]
84        struct ScreenshotResponse {
85            binary: String,
86        }
87
88        let response: ScreenshotResponse = self.base.channel().send("screenshot", params).await?;
89
90        // Decode base64 to bytes
91        let bytes = base64::prelude::BASE64_STANDARD
92            .decode(&response.binary)
93            .map_err(|e| {
94                crate::error::Error::ProtocolError(format!(
95                    "Failed to decode element screenshot: {}",
96                    e
97                ))
98            })?;
99
100        Ok(bytes)
101    }
102
103    /// Returns the bounding box of this element, or None if it is not visible.
104    ///
105    /// The bounding box is in pixels, relative to the top-left corner of the page.
106    ///
107    /// See: <https://playwright.dev/docs/api/class-elementhandle#element-handle-bounding-box>
108    pub async fn bounding_box(&self) -> Result<Option<BoundingBox>> {
109        #[derive(Deserialize)]
110        struct BoundingBoxResponse {
111            value: Option<BoundingBox>,
112        }
113
114        let response: BoundingBoxResponse = self
115            .base
116            .channel()
117            .send(
118                "boundingBox",
119                serde_json::json!({
120                    "timeout": crate::DEFAULT_TIMEOUT_MS
121                }),
122            )
123            .await?;
124
125        Ok(response.value)
126    }
127
128    /// Scrolls this element into the viewport if it is not already visible.
129    ///
130    /// See: <https://playwright.dev/docs/api/class-elementhandle#element-handle-scroll-into-view-if-needed>
131    pub async fn scroll_into_view_if_needed(&self) -> Result<()> {
132        self.base
133            .channel()
134            .send_no_result(
135                "scrollIntoViewIfNeeded",
136                serde_json::json!({
137                    "timeout": crate::DEFAULT_TIMEOUT_MS
138                }),
139            )
140            .await
141    }
142}
143
144impl ChannelOwner for ElementHandle {
145    fn guid(&self) -> &str {
146        self.base.guid()
147    }
148
149    fn type_name(&self) -> &str {
150        self.base.type_name()
151    }
152
153    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
154        self.base.parent()
155    }
156
157    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
158        self.base.connection()
159    }
160
161    fn initializer(&self) -> &Value {
162        self.base.initializer()
163    }
164
165    fn channel(&self) -> &crate::server::channel::Channel {
166        self.base.channel()
167    }
168
169    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
170        self.base.dispose(reason)
171    }
172
173    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
174        self.base.adopt(child)
175    }
176
177    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
178        self.base.add_child(guid, child)
179    }
180
181    fn remove_child(&self, guid: &str) {
182        self.base.remove_child(guid)
183    }
184
185    fn on_event(&self, _method: &str, _params: Value) {
186        // ElementHandle events will be handled in future phases if needed
187    }
188
189    fn was_collected(&self) -> bool {
190        self.base.was_collected()
191    }
192
193    fn as_any(&self) -> &dyn Any {
194        self
195    }
196}
197
198impl std::fmt::Debug for ElementHandle {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        f.debug_struct("ElementHandle")
201            .field("guid", &self.guid())
202            .finish()
203    }
204}