1use crate::error::{Error, Result};
10use crate::frame::Frame;
11use crate::locator::{
12 click_element, content_frame_id, drag_mouse, element_box, element_center, hover_element,
13 press_key, press_text, select_option_element, set_checked_element,
14};
15use crate::options::{
16 CheckOptions, ClickOptions, DragToOptions, FillOptions, HoverOptions, PressOptions,
17 PressSequentiallyOptions, ScreenshotOptions, SelectOption, SelectOptions,
18};
19use crate::page::Page;
20use crate::selectors;
21use crate::types::BoundingBox;
22use base64::Engine;
23use serde_json::{json, Value};
24use std::time::Duration;
25
26#[derive(Clone)]
28pub struct ElementHandle {
29 page: Page,
30 object_id: String,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum ElementState {
39 Visible,
41 Hidden,
43 Enabled,
45 Editable,
47 Stable,
49}
50
51impl ElementHandle {
52 pub(crate) fn new(page: Page, object_id: String) -> Self {
53 Self { page, object_id }
54 }
55
56 pub fn object_id(&self) -> &str {
58 &self.object_id
59 }
60
61 pub fn page(&self) -> Page {
63 self.page.clone()
64 }
65
66 pub async fn click(&self, options: Option<ClickOptions>) -> Result<()> {
67 let opts = options.unwrap_or_default();
68 click_element(&self.page, &self.object_id, &opts).await
69 }
70
71 pub async fn fill(&self, text: &str, _opts: Option<FillOptions>) -> Result<()> {
72 selectors::eval_object(
73 self.page.session(),
74 &self.object_id,
75 "(el, text) => { el.focus(); el.value = text; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); }",
76 json!(text),
77 )
78 .await
79 .map(|_| ())
80 }
81
82 pub async fn clear(&self, options: Option<FillOptions>) -> Result<()> {
84 self.fill("", options).await
85 }
86
87 pub async fn type_(&self, text: &str, opts: Option<PressSequentiallyOptions>) -> Result<()> {
91 let delay = opts
92 .and_then(|o| o.delay)
93 .map(|ms| Duration::from_millis(ms as u64));
94 let _ = self
95 .page
96 .session()
97 .send("DOM.focus", json!({ "objectId": self.object_id }))
98 .await;
99 press_text(&self.page, &self.object_id, text, delay).await
100 }
101
102 pub async fn press(&self, key: &str, options: Option<PressOptions>) -> Result<()> {
104 let delay = options
105 .and_then(|o| o.delay)
106 .map(|ms| Duration::from_millis(ms as u64));
107 press_key(&self.page, &self.object_id, key, delay).await
108 }
109
110 pub async fn dblclick(&self, options: Option<ClickOptions>) -> Result<()> {
112 let mut opts = options.unwrap_or_default();
113 opts.click_count = Some(opts.click_count.unwrap_or(2));
114 click_element(&self.page, &self.object_id, &opts).await
115 }
116
117 pub async fn hover(&self, options: Option<HoverOptions>) -> Result<()> {
119 let opts = options.unwrap_or_default();
120 hover_element(&self.page, &self.object_id, &opts).await
121 }
122
123 pub async fn tap(&self, options: Option<ClickOptions>) -> Result<()> {
125 self.click(options).await
126 }
127
128 pub async fn check(&self, options: Option<CheckOptions>) -> Result<()> {
130 self.set_checked(true, options).await
131 }
132
133 pub async fn uncheck(&self, options: Option<CheckOptions>) -> Result<()> {
135 self.set_checked(false, options).await
136 }
137
138 pub async fn set_checked(&self, checked: bool, options: Option<CheckOptions>) -> Result<()> {
140 let opts = options.unwrap_or_default();
141 set_checked_element(&self.page, &self.object_id, checked, &opts).await
142 }
143
144 pub async fn select_option(
146 &self,
147 value: impl Into<SelectOption>,
148 _options: Option<SelectOptions>,
149 ) -> Result<Vec<String>> {
150 let value = value.into();
151 let arg: Value = match value {
152 SelectOption::Value(v) => json!({ "Value": v }),
153 SelectOption::Label(v) => json!({ "Label": v }),
154 SelectOption::Index(i) => json!({ "Index": i }),
155 };
156 select_option_element(&self.page, &self.object_id, arg).await
157 }
158
159 pub async fn drag_and_drop(
161 &self,
162 target: &ElementHandle,
163 _options: Option<DragToOptions>,
164 ) -> Result<()> {
165 let _ = self
166 .page
167 .session()
168 .send("DOM.scrollIntoViewIfNeeded", json!({ "objectId": self.object_id }))
169 .await;
170 let (sx, sy) = element_center(&self.page, &self.object_id).await?;
171 let _ = target
172 .page
173 .session()
174 .send("DOM.scrollIntoViewIfNeeded", json!({ "objectId": target.object_id }))
175 .await;
176 let (tx, ty) = element_center(&target.page, &target.object_id).await?;
177 drag_mouse(&self.page, sx, sy, tx, ty).await
178 }
179
180 pub async fn focus(&self) -> Result<()> {
181 self.page
182 .session()
183 .send("DOM.focus", json!({ "objectId": self.object_id }))
184 .await
185 .map(|_: Value| ())
186 }
187
188 pub async fn blur(&self) -> Result<()> {
189 selectors::eval_object(self.page.session(), &self.object_id, "(el) => { el.blur(); }", Value::Null)
190 .await
191 .map(|_| ())
192 }
193
194 pub async fn scroll_into_view_if_needed(&self) -> Result<()> {
195 self.page
196 .session()
197 .send("DOM.scrollIntoViewIfNeeded", json!({ "objectId": self.object_id }))
198 .await
199 .map(|_: Value| ())
200 }
201
202 pub async fn dispatch_event(&self, type_: &str, init: Option<Value>) -> Result<()> {
203 let init = init.unwrap_or(Value::Null);
204 selectors::eval_object(
205 self.page.session(),
206 &self.object_id,
207 "(el, args) => { el.dispatchEvent(new Event(args.type, args.init || undefined)); }",
208 json!({ "type": type_, "init": init }),
209 )
210 .await
211 .map(|_| ())
212 }
213
214 pub async fn text_content(&self) -> Result<Option<String>> {
215 self.read_str_opt("(el) => el.textContent").await
216 }
217
218 pub async fn inner_text(&self) -> Result<String> {
219 Ok(self.read_str_opt("(el) => el.innerText").await?.unwrap_or_default())
220 }
221
222 pub async fn inner_html(&self) -> Result<String> {
223 Ok(self.read_str_opt("(el) => el.innerHTML").await?.unwrap_or_default())
224 }
225
226 pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
227 let v = selectors::eval_object(
228 self.page.session(),
229 &self.object_id,
230 "(el, name) => el.getAttribute(name)",
231 json!(name),
232 )
233 .await?;
234 Ok(v.as_str().map(String::from))
235 }
236
237 pub async fn evaluate<R: serde::de::DeserializeOwned>(
239 &self,
240 expression: &str,
241 arg: Option<Value>,
242 ) -> Result<R> {
243 let function = format!("(el, arg) => {{ return ({expression}); }}");
244 let v = selectors::eval_object(self.page.session(), &self.object_id, &function, arg.unwrap_or(Value::Null))
245 .await?;
246 serde_json::from_value::<R>(v).map_err(Error::from)
247 }
248
249 pub async fn evaluate_handle(
257 &self,
258 page_function: &str,
259 arg: Option<Value>,
260 ) -> Result<crate::js_handle::JSHandle> {
261 let function = format!("(el, arg) => {{ return ({page_function}); }}");
262 let params = json!({
263 "objectId": self.object_id,
264 "functionDeclaration": function,
265 "arguments": [{ "value": arg.unwrap_or(Value::Null) }],
266 "returnByValue": false,
267 "awaitPromise": true,
268 });
269 let resp = self.page.session().send("Runtime.callFunctionOn", params).await?;
270 if let Some(exc) = resp.get("exceptionDetails") {
271 let msg = exc
272 .get("exception")
273 .and_then(|e| e.get("description"))
274 .and_then(|v| v.as_str())
275 .unwrap_or("evaluation threw");
276 return Err(Error::ProtocolError(format!("eval error: {msg}")));
277 }
278 let result_oid = resp
279 .get("result")
280 .and_then(|r| r.get("objectId"))
281 .and_then(|v| v.as_str())
282 .ok_or_else(|| {
283 Error::ProtocolError(
284 "callFunctionOn did not return a remote object (no objectId)".into(),
285 )
286 })?
287 .to_string();
288 Ok(crate::js_handle::JSHandle::new(self.page.session_arc(), result_oid))
289 }
290
291 async fn read_str_opt(&self, function: &str) -> Result<Option<String>> {
292 let v = selectors::eval_object(self.page.session(), &self.object_id, function, Value::Null).await?;
293 Ok(v.as_str().map(String::from))
294 }
295
296 async fn state(&self, field: &str) -> Result<bool> {
297 let function = format!(
298 "(el) => self.__pwcdpInjected && self.__pwcdpInjected.elementState(el).{field}"
299 );
300 let v = selectors::eval_object(self.page.session(), &self.object_id, &function, Value::Null).await?;
301 Ok(v.as_bool().unwrap_or(false))
302 }
303
304 pub async fn is_visible(&self) -> Result<bool> {
305 self.state("visible").await
306 }
307 pub async fn is_enabled(&self) -> Result<bool> {
308 self.state("enabled").await
309 }
310 pub async fn is_checked(&self) -> Result<bool> {
311 self.state("checked").await
312 }
313 pub async fn is_editable(&self) -> Result<bool> {
314 self.state("editable").await
315 }
316 pub async fn is_hidden(&self) -> Result<bool> {
317 Ok(!self.is_visible().await?)
318 }
319
320 pub async fn bounding_box(&self) -> Result<Option<BoundingBox>> {
321 match element_box(&self.page, &self.object_id).await {
322 Ok((x, y, w, h)) => Ok(Some(BoundingBox { x, y, width: w, height: h })),
323 Err(_) => Ok(None),
324 }
325 }
326
327 pub async fn screenshot(&self, opts: Option<ScreenshotOptions>) -> Result<Vec<u8>> {
328 let (x, y, w, h) = element_box(&self.page, &self.object_id).await?;
329 let opts = opts.unwrap_or_default();
330 let format = match opts.r#type.unwrap_or_default() {
331 crate::types::ScreenshotType::Png => "png",
332 crate::types::ScreenshotType::Jpeg => "jpeg",
333 crate::types::ScreenshotType::Webp => "webp",
334 };
335 let mut params = json!({
336 "format": format,
337 "clip": { "x": x, "y": y, "width": w, "height": h, "scale": 1 },
338 });
339 if opts.omit_background.unwrap_or(false) && format == "png" {
340 params["omitBackground"] = json!(true);
341 }
342 let resp = self.page.session().send("Page.captureScreenshot", params).await?;
343 let data = resp
344 .get("data")
345 .and_then(|v| v.as_str())
346 .ok_or_else(|| Error::ProtocolError("screenshot missing data".into()))?;
347 let bytes = base64::engine::general_purpose::STANDARD
348 .decode(data)
349 .map_err(|e| Error::ProtocolError(format!("base64 decode: {e}")))?;
350 if let Some(path) = &opts.path {
351 tokio::fs::write(path, &bytes).await?;
352 }
353 Ok(bytes)
354 }
355
356 pub async fn set_input_files(&self, files: &[&str]) -> Result<()> {
358 let desc = self
359 .page
360 .session()
361 .send("DOM.requestNode", json!({ "objectId": self.object_id }))
362 .await?;
363 let node_id = desc
364 .get("nodeId")
365 .and_then(|v| v.as_i64())
366 .ok_or_else(|| Error::ProtocolError("requestNode missing nodeId".into()))?;
367 self.page
368 .session()
369 .send("DOM.setFileInputFiles", json!({ "nodeId": node_id, "files": files }))
370 .await
371 .map(|_: Value| ())
372 }
373
374 pub async fn dispose(&self) -> Result<()> {
376 let _ = self
377 .page
378 .session()
379 .send("Runtime.releaseObject", json!({ "objectId": self.object_id }))
380 .await;
381 Ok(())
382 }
383
384 pub async fn content_frame(&self) -> Result<Option<Frame>> {
391 let fid = content_frame_id(&self.page, &self.object_id).await?;
392 Ok(fid.map(|id| Frame::new(self.page.clone(), id)))
393 }
394
395 pub async fn owner_frame(&self) -> Result<Option<Frame>> {
401 let desc = self
403 .page
404 .session()
405 .send("DOM.describeNode", json!({ "objectId": self.object_id }))
406 .await?;
407 let frame_id = desc
409 .get("node")
410 .and_then(|n| n.get("frameId"))
411 .and_then(|v| v.as_str())
412 .map(String::from)
413 .or_else(|| {
414 desc.get("node")
417 .and_then(|n| n.get("owner"))
418 .and_then(|o| o.get("frameId"))
419 .and_then(|v| v.as_str())
420 .map(String::from)
421 })
422 .or_else(|| self.page.main_frame_id());
423 Ok(frame_id.map(|id| Frame::new(self.page.clone(), id)))
424 }
425
426 pub async fn wait_for_element_state(&self, state: ElementState) -> Result<()> {
432 let timeout = self.page.default_timeout();
433 let deadline = tokio::time::Instant::now() + timeout;
434 match state {
435 ElementState::Visible | ElementState::Hidden
436 | ElementState::Enabled | ElementState::Editable => {
437 let field = match state {
438 ElementState::Visible => "visible",
439 ElementState::Hidden => "hidden",
440 ElementState::Enabled => "enabled",
441 ElementState::Editable => "editable",
442 ElementState::Stable => "visible",
443 };
444 loop {
445 let v = self.state(field).await?;
446 if v {
447 return Ok(());
448 }
449 if tokio::time::Instant::now() >= deadline {
450 return Err(Error::Timeout(format!(
451 "wait_for_element_state({state:?}) timed out after {}ms",
452 timeout.as_millis()
453 )));
454 }
455 tokio::time::sleep(Duration::from_millis(100)).await;
456 }
457 }
458 ElementState::Stable => {
459 let mut prev = self.bounding_box().await?;
461 loop {
462 tokio::time::sleep(Duration::from_millis(100)).await;
463 let cur = self.bounding_box().await?;
464 let stable = match (prev, cur) {
465 (Some(a), Some(b)) => {
466 (a.x - b.x).abs() < 0.5
467 && (a.y - b.y).abs() < 0.5
468 && (a.width - b.width).abs() < 0.5
469 && (a.height - b.height).abs() < 0.5
470 }
471 (None, None) => true,
472 _ => false,
473 };
474 if stable {
475 return Ok(());
476 }
477 prev = cur;
478 if tokio::time::Instant::now() >= deadline {
479 return Err(Error::Timeout(format!(
480 "wait_for_element_state(Stable) timed out after {}ms",
481 timeout.as_millis()
482 )));
483 }
484 }
485 }
486 }
487 }
488
489 pub async fn count(&self) -> Result<usize> {
493 Ok(1)
494 }
495}