1use 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#[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 pub fn object_id(&self) -> &str {
32 &self.object_id
33 }
34
35 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 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 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 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}