Skip to main content

playwright_cdp/
element_handle.rs

1//! `ElementHandle` — a persistent handle to a resolved DOM element (a CDP
2//! `RemoteObjectId`).
3//!
4//! Unlike [`Locator`](crate::Locator), which resolves lazily on each action,
5//! an `ElementHandle` pins one specific element. Remote objects are GC'd by the
6//! browser when their execution context is destroyed (e.g. on navigation); call
7//! [`ElementHandle::dispose`] to release eagerly.
8
9use crate::error::{Error, Result};
10use crate::locator::{click_element, element_box};
11use crate::options::{ClickOptions, FillOptions, ScreenshotOptions};
12use crate::page::Page;
13use crate::selectors;
14use crate::types::BoundingBox;
15use base64::Engine;
16use serde_json::{json, Value};
17
18/// A handle to a resolved element.
19#[derive(Clone)]
20pub struct ElementHandle {
21    page: Page,
22    object_id: String,
23}
24
25impl ElementHandle {
26    pub(crate) fn new(page: Page, object_id: String) -> Self {
27        Self { page, object_id }
28    }
29
30    /// The underlying CDP remote object id.
31    pub fn object_id(&self) -> &str {
32        &self.object_id
33    }
34
35    /// The owning page.
36    pub fn page(&self) -> Page {
37        self.page.clone()
38    }
39
40    pub async fn click(&self, options: Option<ClickOptions>) -> Result<()> {
41        let opts = options.unwrap_or_default();
42        click_element(&self.page, &self.object_id, &opts).await
43    }
44
45    pub async fn fill(&self, text: &str, _opts: Option<FillOptions>) -> Result<()> {
46        selectors::eval_object(
47            self.page.session(),
48            &self.object_id,
49            "(el, text) => { el.focus(); el.value = text; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); }",
50            json!(text),
51        )
52        .await
53        .map(|_| ())
54    }
55
56    pub async fn focus(&self) -> Result<()> {
57        self.page
58            .session()
59            .send("DOM.focus", json!({ "objectId": self.object_id }))
60            .await
61            .map(|_: Value| ())
62    }
63
64    pub async fn blur(&self) -> Result<()> {
65        selectors::eval_object(self.page.session(), &self.object_id, "(el) => { el.blur(); }", Value::Null)
66            .await
67            .map(|_| ())
68    }
69
70    pub async fn scroll_into_view_if_needed(&self) -> Result<()> {
71        self.page
72            .session()
73            .send("DOM.scrollIntoViewIfNeeded", json!({ "objectId": self.object_id }))
74            .await
75            .map(|_: Value| ())
76    }
77
78    pub async fn dispatch_event(&self, type_: &str, init: Option<Value>) -> Result<()> {
79        let init = init.unwrap_or(Value::Null);
80        selectors::eval_object(
81            self.page.session(),
82            &self.object_id,
83            "(el, args) => { el.dispatchEvent(new Event(args.type, args.init || undefined)); }",
84            json!({ "type": type_, "init": init }),
85        )
86        .await
87        .map(|_| ())
88    }
89
90    pub async fn text_content(&self) -> Result<Option<String>> {
91        self.read_str_opt("(el) => el.textContent").await
92    }
93
94    pub async fn inner_text(&self) -> Result<String> {
95        Ok(self.read_str_opt("(el) => el.innerText").await?.unwrap_or_default())
96    }
97
98    pub async fn inner_html(&self) -> Result<String> {
99        Ok(self.read_str_opt("(el) => el.innerHTML").await?.unwrap_or_default())
100    }
101
102    pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
103        let v = selectors::eval_object(
104            self.page.session(),
105            &self.object_id,
106            "(el, name) => el.getAttribute(name)",
107            json!(name),
108        )
109        .await?;
110        Ok(v.as_str().map(String::from))
111    }
112
113    /// Evaluate `expression` with this element bound as the first argument.
114    pub async fn evaluate<R: serde::de::DeserializeOwned>(
115        &self,
116        expression: &str,
117        arg: Option<Value>,
118    ) -> Result<R> {
119        let function = format!("(el, arg) => {{ return ({expression}); }}");
120        let v = selectors::eval_object(self.page.session(), &self.object_id, &function, arg.unwrap_or(Value::Null))
121            .await?;
122        serde_json::from_value::<R>(v).map_err(Error::from)
123    }
124
125    async fn read_str_opt(&self, function: &str) -> Result<Option<String>> {
126        let v = selectors::eval_object(self.page.session(), &self.object_id, function, Value::Null).await?;
127        Ok(v.as_str().map(String::from))
128    }
129
130    async fn state(&self, field: &str) -> Result<bool> {
131        let function = format!(
132            "(el) => self.__pwcdpInjected && self.__pwcdpInjected.elementState(el).{field}"
133        );
134        let v = selectors::eval_object(self.page.session(), &self.object_id, &function, Value::Null).await?;
135        Ok(v.as_bool().unwrap_or(false))
136    }
137
138    pub async fn is_visible(&self) -> Result<bool> {
139        self.state("visible").await
140    }
141    pub async fn is_enabled(&self) -> Result<bool> {
142        self.state("enabled").await
143    }
144    pub async fn is_checked(&self) -> Result<bool> {
145        self.state("checked").await
146    }
147    pub async fn is_editable(&self) -> Result<bool> {
148        self.state("editable").await
149    }
150    pub async fn is_hidden(&self) -> Result<bool> {
151        Ok(!self.is_visible().await?)
152    }
153
154    pub async fn bounding_box(&self) -> Result<Option<BoundingBox>> {
155        match element_box(&self.page, &self.object_id).await {
156            Ok((x, y, w, h)) => Ok(Some(BoundingBox { x, y, width: w, height: h })),
157            Err(_) => Ok(None),
158        }
159    }
160
161    pub async fn screenshot(&self, opts: Option<ScreenshotOptions>) -> Result<Vec<u8>> {
162        let (x, y, w, h) = element_box(&self.page, &self.object_id).await?;
163        let opts = opts.unwrap_or_default();
164        let format = match opts.r#type.unwrap_or_default() {
165            crate::types::ScreenshotType::Png => "png",
166            crate::types::ScreenshotType::Jpeg => "jpeg",
167            crate::types::ScreenshotType::Webp => "webp",
168        };
169        let mut params = json!({
170            "format": format,
171            "clip": { "x": x, "y": y, "width": w, "height": h, "scale": 1 },
172        });
173        if opts.omit_background.unwrap_or(false) && format == "png" {
174            params["omitBackground"] = json!(true);
175        }
176        let resp = self.page.session().send("Page.captureScreenshot", params).await?;
177        let data = resp
178            .get("data")
179            .and_then(|v| v.as_str())
180            .ok_or_else(|| Error::ProtocolError("screenshot missing data".into()))?;
181        let bytes = base64::engine::general_purpose::STANDARD
182            .decode(data)
183            .map_err(|e| Error::ProtocolError(format!("base64 decode: {e}")))?;
184        if let Some(path) = &opts.path {
185            tokio::fs::write(path, &bytes).await?;
186        }
187        Ok(bytes)
188    }
189
190    /// Set files on an `<input type=file>` by server-side path(s).
191    pub async fn set_input_files(&self, files: &[&str]) -> Result<()> {
192        let desc = self
193            .page
194            .session()
195            .send("DOM.requestNode", json!({ "objectId": self.object_id }))
196            .await?;
197        let node_id = desc
198            .get("nodeId")
199            .and_then(|v| v.as_i64())
200            .ok_or_else(|| Error::ProtocolError("requestNode missing nodeId".into()))?;
201        self.page
202            .session()
203            .send("DOM.setFileInputFiles", json!({ "nodeId": node_id, "files": files }))
204            .await
205            .map(|_: Value| ())
206    }
207
208    /// Release the remote object eagerly (otherwise browser GCs it on navigation).
209    pub async fn dispose(&self) -> Result<()> {
210        let _ = self
211            .page
212            .session()
213            .send("Runtime.releaseObject", json!({ "objectId": self.object_id }))
214            .await;
215        Ok(())
216    }
217}