ferridriver_script/bindings/
element_handle.rs1use ferridriver::ElementHandle;
8use ferridriver::backend::ImageFormat;
9use rquickjs::JsLifetime;
10use rquickjs::class::Trace;
11
12use crate::bindings::convert::{
13 FerriResultExt, extract_page_function, quickjs_arg_to_serialized, serialized_value_to_quickjs,
14};
15
16fn parse_screenshot_format<'js>(
21 _ctx: &rquickjs::Ctx<'js>,
22 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
23) -> rquickjs::Result<ImageFormat> {
24 let Some(opts_val) = options.0 else {
25 return Ok(ImageFormat::Png);
26 };
27 if opts_val.is_undefined() || opts_val.is_null() {
28 return Ok(ImageFormat::Png);
29 }
30 let Some(obj) = opts_val.as_object() else {
31 return Ok(ImageFormat::Png);
32 };
33 let type_field: Option<String> = obj.get("type").ok();
34 match type_field.as_deref() {
35 None | Some("" | "png") => Ok(ImageFormat::Png),
36 Some("jpeg" | "jpg") => Ok(ImageFormat::Jpeg),
37 Some("webp") => Ok(ImageFormat::Webp),
38 Some(other) => Err(rquickjs::Error::new_from_js_message(
39 "screenshot",
40 "invalid format",
41 &format!("unsupported screenshot type {other:?}; expected 'png' | 'jpeg' | 'webp'"),
42 )),
43 }
44}
45
46#[derive(JsLifetime, Trace)]
48#[rquickjs::class(rename = "ElementHandle")]
49pub struct ElementHandleJs {
50 #[qjs(skip_trace)]
51 inner: ElementHandle,
52}
53
54impl ElementHandleJs {
55 #[must_use]
56 pub fn new(inner: ElementHandle) -> Self {
57 Self { inner }
58 }
59
60 #[must_use]
61 pub fn inner(&self) -> &ElementHandle {
62 &self.inner
63 }
64}
65
66#[rquickjs::methods]
67impl ElementHandleJs {
68 #[qjs(rename = "isDisposed")]
71 pub fn is_disposed(&self) -> bool {
72 self.inner.is_disposed()
73 }
74
75 #[qjs(rename = "dispose")]
77 pub async fn dispose(&self) -> rquickjs::Result<()> {
78 self.inner.dispose().await.into_js()
79 }
80
81 #[qjs(rename = "asJSHandle")]
85 pub fn as_js_handle(&self) -> crate::bindings::js_handle::JSHandleJs {
86 crate::bindings::js_handle::JSHandleJs::new(self.inner.as_js_handle().clone())
87 }
88
89 #[qjs(rename = "evaluate")]
91 pub async fn evaluate<'js>(
92 &self,
93 ctx: rquickjs::Ctx<'js>,
94 page_function: rquickjs::Value<'js>,
95 arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
96 ) -> rquickjs::Result<rquickjs::Value<'js>> {
97 let (source, is_fn) = extract_page_function(&ctx, page_function)?;
98 let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
99 let result = self
100 .inner
101 .as_js_handle()
102 .evaluate(&source, serialized, is_fn)
103 .await
104 .into_js()?;
105 serialized_value_to_quickjs(&ctx, &result)
106 }
107
108 #[qjs(rename = "evaluateHandle")]
110 pub async fn evaluate_handle<'js>(
111 &self,
112 ctx: rquickjs::Ctx<'js>,
113 page_function: rquickjs::Value<'js>,
114 arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
115 ) -> rquickjs::Result<crate::bindings::js_handle::JSHandleJs> {
116 let (source, is_fn) = extract_page_function(&ctx, page_function)?;
117 let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
118 let handle = self
119 .inner
120 .as_js_handle()
121 .evaluate_handle(&source, serialized, is_fn)
122 .await
123 .into_js()?;
124 Ok(crate::bindings::js_handle::JSHandleJs::new(handle))
125 }
126
127 #[qjs(rename = "innerHTML")]
130 pub async fn inner_html(&self) -> rquickjs::Result<String> {
131 self.inner.inner_html().await.into_js()
132 }
133
134 #[qjs(rename = "innerText")]
135 pub async fn inner_text(&self) -> rquickjs::Result<String> {
136 self.inner.inner_text().await.into_js()
137 }
138
139 #[qjs(rename = "textContent")]
140 pub async fn text_content(&self) -> rquickjs::Result<Option<String>> {
141 self.inner.text_content().await.into_js()
142 }
143
144 #[qjs(rename = "getAttribute")]
145 pub async fn get_attribute(&self, name: String) -> rquickjs::Result<Option<String>> {
146 self.inner.get_attribute(&name).await.into_js()
147 }
148
149 #[qjs(rename = "inputValue")]
150 pub async fn input_value(&self) -> rquickjs::Result<String> {
151 self.inner.input_value().await.into_js()
152 }
153
154 #[qjs(rename = "isVisible")]
157 pub async fn is_visible(&self) -> rquickjs::Result<bool> {
158 self.inner.is_visible().await.into_js()
159 }
160
161 #[qjs(rename = "isHidden")]
162 pub async fn is_hidden(&self) -> rquickjs::Result<bool> {
163 self.inner.is_hidden().await.into_js()
164 }
165
166 #[qjs(rename = "isDisabled")]
167 pub async fn is_disabled(&self) -> rquickjs::Result<bool> {
168 self.inner.is_disabled().await.into_js()
169 }
170
171 #[qjs(rename = "isEnabled")]
172 pub async fn is_enabled(&self) -> rquickjs::Result<bool> {
173 self.inner.is_enabled().await.into_js()
174 }
175
176 #[qjs(rename = "isChecked")]
177 pub async fn is_checked(&self) -> rquickjs::Result<bool> {
178 self.inner.is_checked().await.into_js()
179 }
180
181 #[qjs(rename = "isEditable")]
182 pub async fn is_editable(&self) -> rquickjs::Result<bool> {
183 self.inner.is_editable().await.into_js()
184 }
185
186 #[qjs(rename = "boundingBox")]
191 pub async fn bounding_box<'js>(&self, ctx: rquickjs::Ctx<'js>) -> rquickjs::Result<rquickjs::Value<'js>> {
192 let bbox = self.inner.bounding_box().await.into_js()?;
193 match bbox {
194 None => Ok(rquickjs::Value::new_null(ctx)),
195 Some(b) => {
196 let obj = rquickjs::Object::new(ctx.clone())?;
197 obj.set("x", b.x)?;
198 obj.set("y", b.y)?;
199 obj.set("width", b.width)?;
200 obj.set("height", b.height)?;
201 Ok(obj.into_value())
202 },
203 }
204 }
205
206 #[qjs(rename = "click")]
209 pub async fn click<'js>(
210 &self,
211 ctx: rquickjs::Ctx<'js>,
212 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
213 ) -> rquickjs::Result<()> {
214 let opts = crate::bindings::convert::parse_click_options(&ctx, options)?;
215 self.inner.click(opts).await.into_js()
216 }
217
218 #[qjs(rename = "dblclick")]
219 pub async fn dblclick<'js>(
220 &self,
221 ctx: rquickjs::Ctx<'js>,
222 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
223 ) -> rquickjs::Result<()> {
224 let opts = crate::bindings::convert::parse_dblclick_options(&ctx, options)?;
225 self.inner.dblclick(opts).await.into_js()
226 }
227
228 #[qjs(rename = "hover")]
229 pub async fn hover<'js>(
230 &self,
231 ctx: rquickjs::Ctx<'js>,
232 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
233 ) -> rquickjs::Result<()> {
234 let opts = crate::bindings::convert::parse_hover_options(&ctx, options)?;
235 self.inner.hover(opts).await.into_js()
236 }
237
238 #[qjs(rename = "type")]
239 pub async fn type_str<'js>(
240 &self,
241 ctx: rquickjs::Ctx<'js>,
242 text: String,
243 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
244 ) -> rquickjs::Result<()> {
245 let opts = crate::bindings::convert::parse_type_options(&ctx, options)?;
246 self.inner.type_str(&text, opts).await.into_js()
247 }
248
249 #[qjs(rename = "focus")]
250 pub async fn focus(&self) -> rquickjs::Result<()> {
251 self.inner.focus().await.into_js()
252 }
253
254 #[qjs(rename = "scrollIntoViewIfNeeded")]
255 pub async fn scroll_into_view_if_needed(&self) -> rquickjs::Result<()> {
256 self.inner.scroll_into_view_if_needed().await.into_js()
257 }
258
259 #[qjs(rename = "screenshot")]
265 pub async fn screenshot<'js>(
266 &self,
267 ctx: rquickjs::Ctx<'js>,
268 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
269 ) -> rquickjs::Result<Vec<u8>> {
270 let format = parse_screenshot_format(&ctx, options)?;
271 self.inner.screenshot(format).await.into_js()
272 }
273
274 #[qjs(rename = "$eval")]
279 pub async fn dollar_eval<'js>(
280 &self,
281 ctx: rquickjs::Ctx<'js>,
282 selector: String,
283 page_function: rquickjs::Value<'js>,
284 arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
285 ) -> rquickjs::Result<rquickjs::Value<'js>> {
286 let (source, _is_fn) = extract_page_function(&ctx, page_function)?;
287 let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
288 let result = self
289 .inner
290 .eval_on_selector(&selector, &source, serialized)
291 .await
292 .into_js()?;
293 serialized_value_to_quickjs(&ctx, &result)
294 }
295
296 #[qjs(rename = "$$eval")]
299 pub async fn dollar_dollar_eval<'js>(
300 &self,
301 ctx: rquickjs::Ctx<'js>,
302 selector: String,
303 page_function: rquickjs::Value<'js>,
304 arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
305 ) -> rquickjs::Result<rquickjs::Value<'js>> {
306 let (source, _is_fn) = extract_page_function(&ctx, page_function)?;
307 let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
308 let result = self
309 .inner
310 .eval_on_selector_all(&selector, &source, serialized)
311 .await
312 .into_js()?;
313 serialized_value_to_quickjs(&ctx, &result)
314 }
315
316 #[qjs(rename = "$")]
321 pub async fn query_selector(&self, selector: String) -> rquickjs::Result<Option<ElementHandleJs>> {
322 let maybe = self.inner.query_selector(&selector).await.into_js()?;
323 Ok(maybe.map(ElementHandleJs::new))
324 }
325
326 #[qjs(rename = "$$")]
329 pub async fn query_selector_all(&self, selector: String) -> rquickjs::Result<Vec<ElementHandleJs>> {
330 let handles = self.inner.query_selector_all(&selector).await.into_js()?;
331 Ok(handles.into_iter().map(ElementHandleJs::new).collect())
332 }
333
334 #[qjs(rename = "ownerFrame")]
338 pub async fn owner_frame(&self) -> rquickjs::Result<Option<crate::bindings::frame::FrameJs>> {
339 let maybe = self.inner.owner_frame().await.into_js()?;
340 Ok(maybe.map(crate::bindings::frame::FrameJs::new))
341 }
342
343 #[qjs(rename = "contentFrame")]
345 pub async fn content_frame(&self) -> rquickjs::Result<Option<crate::bindings::frame::FrameJs>> {
346 let maybe = self.inner.content_frame().await.into_js()?;
347 Ok(maybe.map(crate::bindings::frame::FrameJs::new))
348 }
349
350 #[qjs(rename = "waitForElementState")]
354 pub async fn wait_for_element_state(
355 &self,
356 state: String,
357 timeout: rquickjs::function::Opt<f64>,
358 ) -> rquickjs::Result<()> {
359 let st = ferridriver::ElementState::parse(&state).into_js()?;
360 let timeout_ms = timeout.0.map(|ms| ms as u64);
361 self.inner.wait_for_element_state(st, timeout_ms).await.into_js()
362 }
363
364 #[qjs(rename = "waitForSelector")]
366 pub async fn wait_for_selector(
367 &self,
368 selector: String,
369 timeout: rquickjs::function::Opt<f64>,
370 ) -> rquickjs::Result<Option<ElementHandleJs>> {
371 let timeout_ms = timeout.0.map(|ms| ms as u64);
372 let maybe = self.inner.wait_for_selector(&selector, timeout_ms).await.into_js()?;
373 Ok(maybe.map(ElementHandleJs::new))
374 }
375
376 #[qjs(rename = "fill")]
380 pub async fn fill<'js>(
381 &self,
382 ctx: rquickjs::Ctx<'js>,
383 value: String,
384 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
385 ) -> rquickjs::Result<()> {
386 let opts = crate::bindings::convert::parse_fill_options(&ctx, options)?;
387 self.inner.fill(&value, opts).await.into_js()
388 }
389
390 #[qjs(rename = "check")]
392 pub async fn check<'js>(
393 &self,
394 ctx: rquickjs::Ctx<'js>,
395 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
396 ) -> rquickjs::Result<()> {
397 let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
398 self.inner.check(opts).await.into_js()
399 }
400
401 #[qjs(rename = "uncheck")]
403 pub async fn uncheck<'js>(
404 &self,
405 ctx: rquickjs::Ctx<'js>,
406 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
407 ) -> rquickjs::Result<()> {
408 let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
409 self.inner.uncheck(opts).await.into_js()
410 }
411
412 #[qjs(rename = "setChecked")]
414 pub async fn set_checked<'js>(
415 &self,
416 ctx: rquickjs::Ctx<'js>,
417 checked: bool,
418 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
419 ) -> rquickjs::Result<()> {
420 let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
421 self.inner.set_checked(checked, opts).await.into_js()
422 }
423
424 #[qjs(rename = "tap")]
426 pub async fn tap<'js>(
427 &self,
428 ctx: rquickjs::Ctx<'js>,
429 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
430 ) -> rquickjs::Result<()> {
431 let opts = crate::bindings::convert::parse_tap_options(&ctx, options)?;
432 self.inner.tap(opts).await.into_js()
433 }
434
435 #[qjs(rename = "press")]
437 pub async fn press<'js>(
438 &self,
439 ctx: rquickjs::Ctx<'js>,
440 key: String,
441 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
442 ) -> rquickjs::Result<()> {
443 let opts = crate::bindings::convert::parse_press_options(&ctx, options)?;
444 self.inner.press(&key, opts).await.into_js()
445 }
446
447 #[qjs(rename = "dispatchEvent")]
449 pub async fn dispatch_event<'js>(
450 &self,
451 ctx: rquickjs::Ctx<'js>,
452 event_type: String,
453 event_init: rquickjs::function::Opt<rquickjs::Value<'js>>,
454 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
455 ) -> rquickjs::Result<()> {
456 let init_json = match event_init.0 {
457 Some(v) if !v.is_undefined() && !v.is_null() => {
458 Some(crate::bindings::convert::serde_from_js::<serde_json::Value>(&ctx, v)?)
459 },
460 _ => None,
461 };
462 let opts = crate::bindings::convert::parse_dispatch_event_options(&ctx, options)?;
463 self.inner.dispatch_event(&event_type, init_json, opts).await.into_js()
464 }
465
466 #[qjs(rename = "selectOption")]
468 pub async fn select_option<'js>(
469 &self,
470 ctx: rquickjs::Ctx<'js>,
471 values: rquickjs::Value<'js>,
472 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
473 ) -> rquickjs::Result<Vec<String>> {
474 let values = crate::bindings::convert::parse_select_option_values(&ctx, values)?;
475 let opts = crate::bindings::convert::parse_select_option_options(&ctx, options)?;
476 self.inner.select_option(values, opts).await.into_js()
477 }
478
479 #[qjs(rename = "selectText")]
481 pub async fn select_text(&self) -> rquickjs::Result<()> {
482 self.inner.select_text().await.into_js()
483 }
484
485 #[qjs(rename = "setInputFiles")]
487 pub async fn set_input_files<'js>(
488 &self,
489 ctx: rquickjs::Ctx<'js>,
490 files: rquickjs::Value<'js>,
491 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
492 ) -> rquickjs::Result<()> {
493 let files = crate::bindings::convert::parse_input_files(&ctx, files)?;
494 let opts = crate::bindings::convert::parse_set_input_files_options(&ctx, options)?;
495 self.inner.set_input_files(files, opts).await.into_js()
496 }
497}