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