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    #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid(), bytes_len = tracing::field::Empty))]
70    pub async fn screenshot(
71        &self,
72        options: Option<crate::protocol::ScreenshotOptions>,
73    ) -> Result<Vec<u8>> {
74        let params = if let Some(opts) = options {
75            opts.to_json()
76        } else {
77            // Default to PNG with required timeout
78            serde_json::json!({
79                "type": "png",
80                "timeout": crate::DEFAULT_TIMEOUT_MS
81            })
82        };
83
84        #[derive(Deserialize)]
85        struct ScreenshotResponse {
86            binary: String,
87        }
88
89        let response: ScreenshotResponse = self.base.channel().send("screenshot", params).await?;
90
91        // Decode base64 to bytes
92        let bytes = base64::prelude::BASE64_STANDARD
93            .decode(&response.binary)
94            .map_err(|e| {
95                crate::error::Error::ProtocolError(format!(
96                    "Failed to decode element screenshot: {}",
97                    e
98                ))
99            })?;
100
101        Ok(bytes)
102    }
103
104    /// Returns the bounding box of this element, or None if it is not visible.
105    ///
106    /// The bounding box is in pixels, relative to the top-left corner of the page.
107    ///
108    /// See: <https://playwright.dev/docs/api/class-elementhandle#element-handle-bounding-box>
109    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
110    pub async fn bounding_box(&self) -> Result<Option<BoundingBox>> {
111        #[derive(Deserialize)]
112        struct BoundingBoxResponse {
113            value: Option<BoundingBox>,
114        }
115
116        let response: BoundingBoxResponse = self
117            .base
118            .channel()
119            .send(
120                "boundingBox",
121                serde_json::json!({
122                    "timeout": crate::DEFAULT_TIMEOUT_MS
123                }),
124            )
125            .await?;
126
127        Ok(response.value)
128    }
129
130    /// Sets files on this element (which must be an `<input type="file">`).
131    ///
132    /// Called by [`FileChooser::set_files`](crate::protocol::FileChooser::set_files) to
133    /// satisfy a file chooser dialog by setting files directly on the element.
134    ///
135    /// # Arguments
136    ///
137    /// * `files` - Slice of file paths to set on the input element
138    ///
139    /// See: <https://playwright.dev/docs/api/class-filechooser#file-chooser-set-files>
140    pub(crate) async fn set_input_files(
141        &self,
142        files: &[std::path::PathBuf],
143    ) -> crate::error::Result<()> {
144        use base64::{Engine as _, engine::general_purpose};
145
146        let payloads: Vec<serde_json::Value> = files
147            .iter()
148            .map(|path| {
149                let name = path
150                    .file_name()
151                    .map(|n| n.to_string_lossy().into_owned())
152                    .unwrap_or_else(|| "file".to_string());
153                let mime_type = crate::protocol::mime::from_path(path);
154                let buffer = std::fs::read(path).unwrap_or_default();
155                let b64 = general_purpose::STANDARD.encode(&buffer);
156                serde_json::json!({
157                    "name": name,
158                    "mimeType": mime_type,
159                    "buffer": b64
160                })
161            })
162            .collect();
163
164        self.base
165            .channel()
166            .send_no_result(
167                "setInputFiles",
168                serde_json::json!({
169                    "payloads": payloads,
170                    "timeout": crate::DEFAULT_TIMEOUT_MS
171                }),
172            )
173            .await
174    }
175
176    /// Scrolls this element into the viewport if it is not already visible.
177    ///
178    /// See: <https://playwright.dev/docs/api/class-elementhandle#element-handle-scroll-into-view-if-needed>
179    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
180    pub async fn scroll_into_view_if_needed(&self) -> Result<()> {
181        self.base
182            .channel()
183            .send_no_result(
184                "scrollIntoViewIfNeeded",
185                serde_json::json!({
186                    "timeout": crate::DEFAULT_TIMEOUT_MS
187                }),
188            )
189            .await
190    }
191
192    /// Returns the `Frame` associated with this `<iframe>` element, or `None` if
193    /// the element is not an iframe.
194    ///
195    /// See: <https://playwright.dev/docs/api/class-elementhandle#element-handle-content-frame>
196    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
197    pub async fn content_frame(&self) -> Result<Option<crate::protocol::Frame>> {
198        use crate::server::connection::ConnectionExt;
199
200        #[derive(Deserialize)]
201        struct FrameRef {
202            guid: String,
203        }
204        #[derive(Deserialize)]
205        struct ContentFrameResponse {
206            frame: Option<FrameRef>,
207        }
208
209        let response: ContentFrameResponse = self
210            .base
211            .channel()
212            .send("contentFrame", serde_json::json!({}))
213            .await?;
214
215        match response.frame {
216            None => Ok(None),
217            Some(frame_ref) => {
218                let connection = self.base.connection();
219                let frame = connection
220                    .get_typed::<crate::protocol::Frame>(&frame_ref.guid)
221                    .await?;
222                Ok(Some(frame))
223            }
224        }
225    }
226
227    /// Returns the `Frame` that owns this element.
228    ///
229    /// Every element belongs to a frame (the main frame or a child iframe frame).
230    ///
231    /// See: <https://playwright.dev/docs/api/class-elementhandle#element-handle-owner-frame>
232    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
233    pub async fn owner_frame(&self) -> Result<Option<crate::protocol::Frame>> {
234        use crate::server::connection::ConnectionExt;
235
236        #[derive(Deserialize)]
237        struct FrameRef {
238            guid: String,
239        }
240        #[derive(Deserialize)]
241        struct OwnerFrameResponse {
242            frame: Option<FrameRef>,
243        }
244
245        let response: OwnerFrameResponse = self
246            .base
247            .channel()
248            .send("ownerFrame", serde_json::json!({}))
249            .await?;
250
251        match response.frame {
252            None => Ok(None),
253            Some(frame_ref) => {
254                let connection = self.base.connection();
255                let frame = connection
256                    .get_typed::<crate::protocol::Frame>(&frame_ref.guid)
257                    .await?;
258                Ok(Some(frame))
259            }
260        }
261    }
262
263    /// Waits until the element reaches the specified state.
264    ///
265    /// Valid states: `"visible"`, `"hidden"`, `"stable"`, `"enabled"`, `"disabled"`, `"editable"`.
266    ///
267    /// # Arguments
268    ///
269    /// * `state` — the element state to wait for
270    /// * `timeout` — optional timeout in milliseconds (defaults to [`DEFAULT_TIMEOUT_MS`](crate::DEFAULT_TIMEOUT_MS))
271    ///
272    /// See: <https://playwright.dev/docs/api/class-elementhandle#element-handle-wait-for-element-state>
273    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
274    pub async fn wait_for_element_state(&self, state: &str, timeout: Option<f64>) -> Result<()> {
275        let timeout_ms = timeout.unwrap_or(crate::DEFAULT_TIMEOUT_MS);
276        self.base
277            .channel()
278            .send_no_result(
279                "waitForElementState",
280                serde_json::json!({
281                    "state": state,
282                    "timeout": timeout_ms
283                }),
284            )
285            .await
286    }
287}
288
289impl ChannelOwner for ElementHandle {
290    fn guid(&self) -> &str {
291        self.base.guid()
292    }
293
294    fn type_name(&self) -> &str {
295        self.base.type_name()
296    }
297
298    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
299        self.base.parent()
300    }
301
302    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
303        self.base.connection()
304    }
305
306    fn initializer(&self) -> &Value {
307        self.base.initializer()
308    }
309
310    fn channel(&self) -> &crate::server::channel::Channel {
311        self.base.channel()
312    }
313
314    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
315        self.base.dispose(reason)
316    }
317
318    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
319        self.base.adopt(child)
320    }
321
322    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
323        self.base.add_child(guid, child)
324    }
325
326    fn remove_child(&self, guid: &str) {
327        self.base.remove_child(guid)
328    }
329
330    fn on_event(&self, _method: &str, _params: Value) {
331        // ElementHandle events will be handled in future phases if needed
332    }
333
334    fn was_collected(&self) -> bool {
335        self.base.was_collected()
336    }
337
338    fn as_any(&self) -> &dyn Any {
339        self
340    }
341}
342
343impl std::fmt::Debug for ElementHandle {
344    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
345        f.debug_struct("ElementHandle")
346            .field("guid", &self.guid())
347            .finish()
348    }
349}