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