1use std::sync::Arc;
8use std::sync::atomic::{AtomicU64, Ordering};
9
10use ferridriver::Page;
11use rquickjs::JsLifetime;
12use rquickjs::class::Trace;
13
14use ferridriver::options::WaitOptions;
15use rquickjs::function::Opt;
16use serde::Deserialize;
17
18use crate::bindings::convert::{
19 FerriResultExt, extract_page_function, init_script_from_js, quickjs_arg_to_serialized, serde_from_js,
20 serialized_value_to_quickjs,
21};
22use crate::bindings::keyboard::KeyboardJs;
23use crate::bindings::locator::LocatorJs;
24use crate::bindings::mouse::MouseJs;
25
26#[derive(Debug, Default, Deserialize)]
28#[serde(default)]
29struct JsWaitOptions {
30 state: Option<String>,
31 timeout: Option<u64>,
32}
33
34fn parse_wait_options<'js>(
35 ctx: &rquickjs::Ctx<'js>,
36 value: Opt<rquickjs::Value<'js>>,
37) -> rquickjs::Result<WaitOptions> {
38 match value.0 {
39 Some(v) if !v.is_undefined() && !v.is_null() => {
40 let js: JsWaitOptions = serde_from_js(ctx, v)?;
41 Ok(WaitOptions {
42 state: js.state,
43 timeout: js.timeout,
44 })
45 },
46 _ => Ok(WaitOptions::default()),
47 }
48}
49
50#[derive(serde::Deserialize, Debug, Default)]
51#[serde(rename_all = "camelCase", default)]
52struct JsGotoOptions {
53 wait_until: Option<String>,
54 timeout: Option<u64>,
55 referer: Option<String>,
56}
57
58fn parse_goto_options<'js>(
59 ctx: &rquickjs::Ctx<'js>,
60 value: Opt<rquickjs::Value<'js>>,
61) -> rquickjs::Result<Option<ferridriver::options::GotoOptions>> {
62 match value.0 {
63 Some(v) if !v.is_undefined() && !v.is_null() => {
64 let js: JsGotoOptions = serde_from_js(ctx, v)?;
65 Ok(Some(ferridriver::options::GotoOptions {
66 wait_until: js.wait_until,
67 timeout: js.timeout,
68 referer: js.referer,
69 }))
70 },
71 _ => Ok(None),
72 }
73}
74
75#[derive(serde::Deserialize, Debug, Default)]
76#[serde(rename_all = "camelCase", default)]
77struct JsPageCloseOptions {
78 run_before_unload: Option<bool>,
79 reason: Option<String>,
80}
81
82#[derive(serde::Deserialize, Debug, Default)]
86#[serde(rename_all = "camelCase", default)]
87pub(crate) struct JsDragAndDropOptions {
88 force: Option<bool>,
89 no_wait_after: Option<bool>,
90 source_position: Option<JsPoint>,
91 target_position: Option<JsPoint>,
92 steps: Option<u32>,
93 strict: Option<bool>,
94 timeout: Option<u64>,
95 trial: Option<bool>,
96}
97
98#[derive(serde::Deserialize, Debug, Default, Clone, Copy)]
99pub(crate) struct JsPoint {
100 x: f64,
101 y: f64,
102}
103
104impl From<JsPoint> for ferridriver::options::Point {
105 fn from(p: JsPoint) -> Self {
106 Self { x: p.x, y: p.y }
107 }
108}
109
110fn parse_emulate_media_field<'js>(
123 obj: &rquickjs::Object<'js>,
124 key: &str,
125) -> rquickjs::Result<ferridriver::options::MediaOverride> {
126 use ferridriver::options::MediaOverride;
127 if !obj.contains_key(key)? {
128 return Ok(MediaOverride::Unchanged);
129 }
130 let val: rquickjs::Value<'js> = obj.get(key)?;
131 if val.is_undefined() {
132 Ok(MediaOverride::Unchanged)
133 } else if val.is_null() {
134 Ok(MediaOverride::Disabled)
135 } else if let Some(s) = val.as_string() {
136 Ok(MediaOverride::Set(s.to_string()?))
137 } else {
138 Err(rquickjs::Error::new_from_js_message(
139 "emulateMedia options",
140 "field",
141 format!("{key}: expected null, undefined, or string"),
142 ))
143 }
144}
145
146pub(crate) fn parse_emulate_media_options<'js>(
147 _ctx: &rquickjs::Ctx<'js>,
148 value: Opt<rquickjs::Value<'js>>,
149) -> rquickjs::Result<ferridriver::options::EmulateMediaOptions> {
150 let Some(v) = value.0.filter(|v| !v.is_undefined() && !v.is_null()) else {
151 return Ok(ferridriver::options::EmulateMediaOptions::default());
152 };
153 let Some(obj) = v.as_object() else {
154 return Ok(ferridriver::options::EmulateMediaOptions::default());
155 };
156 Ok(ferridriver::options::EmulateMediaOptions {
157 media: parse_emulate_media_field(obj, "media")?,
158 color_scheme: parse_emulate_media_field(obj, "colorScheme")?,
159 reduced_motion: parse_emulate_media_field(obj, "reducedMotion")?,
160 forced_colors: parse_emulate_media_field(obj, "forcedColors")?,
161 contrast: parse_emulate_media_field(obj, "contrast")?,
162 })
163}
164
165pub(crate) fn parse_drag_options<'js>(
166 ctx: &rquickjs::Ctx<'js>,
167 value: Opt<rquickjs::Value<'js>>,
168) -> rquickjs::Result<Option<ferridriver::options::DragAndDropOptions>> {
169 match value.0 {
170 Some(v) if !v.is_undefined() && !v.is_null() => {
171 let js: JsDragAndDropOptions = serde_from_js(ctx, v)?;
172 Ok(Some(ferridriver::options::DragAndDropOptions {
173 force: js.force,
174 no_wait_after: js.no_wait_after,
175 source_position: js.source_position.map(Into::into),
176 target_position: js.target_position.map(Into::into),
177 steps: js.steps,
178 strict: js.strict,
179 timeout: js.timeout,
180 trial: js.trial,
181 }))
182 },
183 _ => Ok(None),
184 }
185}
186
187fn parse_page_close_options<'js>(
188 ctx: &rquickjs::Ctx<'js>,
189 value: Opt<rquickjs::Value<'js>>,
190) -> rquickjs::Result<Option<ferridriver::options::PageCloseOptions>> {
191 match value.0 {
192 Some(v) if !v.is_undefined() && !v.is_null() => {
193 let js: JsPageCloseOptions = serde_from_js(ctx, v)?;
194 Ok(Some(ferridriver::options::PageCloseOptions {
195 run_before_unload: js.run_before_unload,
196 reason: js.reason,
197 }))
198 },
199 _ => Ok(None),
200 }
201}
202
203#[derive(Default)]
214pub(crate) struct PageCallbacks {
215 route_handlers: rustc_hash::FxHashMap<u64, rquickjs::Persistent<rquickjs::Function<'static>>>,
216 route_preds: rustc_hash::FxHashMap<u64, rquickjs::Persistent<rquickjs::Function<'static>>>,
217 exposed: rustc_hash::FxHashMap<String, rquickjs::Persistent<rquickjs::Function<'static>>>,
218 screencast: Option<rquickjs::Persistent<rquickjs::Function<'static>>>,
219}
220
221pub(crate) struct PageCallbacksUd(std::cell::RefCell<PageCallbacks>);
222
223#[allow(unsafe_code)]
227unsafe impl rquickjs::JsLifetime<'_> for PageCallbacksUd {
228 type Changed<'to> = PageCallbacksUd;
229}
230
231pub(crate) fn ensure_page_callbacks(ctx: &rquickjs::Ctx<'_>) {
235 if ctx.userdata::<PageCallbacksUd>().is_none() {
236 let _ = ctx.store_userdata(PageCallbacksUd(std::cell::RefCell::new(PageCallbacks::default())));
237 }
238}
239
240fn with_page_callbacks<R>(ctx: &rquickjs::Ctx<'_>, f: impl FnOnce(&mut PageCallbacks) -> R) -> rquickjs::Result<R> {
241 ensure_page_callbacks(ctx);
242 let ud = ctx.userdata::<PageCallbacksUd>().ok_or_else(|| {
243 rquickjs::Error::new_from_js_message("page", "Error", "page callbacks registry missing".to_string())
244 })?;
245 let mut reg = ud.0.borrow_mut();
246 Ok(f(&mut reg))
247}
248
249#[derive(JsLifetime, Trace)]
254#[rquickjs::class(rename = "Page")]
255pub struct PageJs {
256 #[qjs(skip_trace)]
260 inner: Arc<Page>,
261 #[qjs(skip_trace)]
267 async_ctx: Option<rquickjs::AsyncContext>,
268 #[qjs(skip_trace)]
273 next_route_id: Arc<AtomicU64>,
274 #[qjs(skip_trace)]
281 route_matchers: Arc<std::sync::Mutex<rustc_hash::FxHashMap<u64, ferridriver::url_matcher::UrlMatcher>>>,
282}
283
284impl PageJs {
285 #[must_use]
286 pub fn new(inner: Arc<Page>) -> Self {
287 Self {
288 inner,
289 async_ctx: None,
290 next_route_id: Arc::new(AtomicU64::new(0)),
291 route_matchers: Arc::new(std::sync::Mutex::new(rustc_hash::FxHashMap::default())),
292 }
293 }
294
295 #[must_use]
296 pub fn new_with_async_ctx(inner: Arc<Page>, async_ctx: rquickjs::AsyncContext) -> Self {
297 Self {
298 inner,
299 async_ctx: Some(async_ctx),
300 next_route_id: Arc::new(AtomicU64::new(0)),
301 route_matchers: Arc::new(std::sync::Mutex::new(rustc_hash::FxHashMap::default())),
302 }
303 }
304
305 #[must_use]
308 pub fn page_arc(&self) -> Arc<Page> {
309 self.inner.clone()
310 }
311
312 #[must_use]
313 pub fn page(&self) -> &Arc<Page> {
314 &self.inner
315 }
316}
317
318pub(crate) fn pagejs_for_ctx(ctx: &rquickjs::Ctx<'_>, page: Arc<Page>) -> PageJs {
324 match ctx.userdata::<crate::engine::SessionAsyncCtx>() {
325 Some(ud) => PageJs::new_with_async_ctx(page, ud.0.clone()),
326 None => PageJs::new(page),
327 }
328}
329
330#[rquickjs::methods]
331impl PageJs {
332 #[qjs(rename = "goto")]
337 pub async fn goto<'js>(
338 &self,
339 ctx: rquickjs::Ctx<'js>,
340 url: String,
341 options: Opt<rquickjs::Value<'js>>,
342 ) -> rquickjs::Result<Option<crate::bindings::network::ResponseJs>> {
343 let opts = parse_goto_options(&ctx, options)?;
344 let resp = self.inner.goto(&url, opts).await.into_js()?;
345 Ok(resp.map(|r| crate::bindings::network::ResponseJs::new_with_page(r, self.inner.clone())))
346 }
347
348 #[qjs(rename = "reload")]
350 pub async fn reload<'js>(
351 &self,
352 ctx: rquickjs::Ctx<'js>,
353 options: Opt<rquickjs::Value<'js>>,
354 ) -> rquickjs::Result<Option<crate::bindings::network::ResponseJs>> {
355 let opts = parse_goto_options(&ctx, options)?;
356 let resp = self.inner.reload(opts).await.into_js()?;
357 Ok(resp.map(|r| crate::bindings::network::ResponseJs::new_with_page(r, self.inner.clone())))
358 }
359
360 #[qjs(rename = "goBack")]
362 pub async fn go_back<'js>(
363 &self,
364 ctx: rquickjs::Ctx<'js>,
365 options: Opt<rquickjs::Value<'js>>,
366 ) -> rquickjs::Result<Option<crate::bindings::network::ResponseJs>> {
367 let opts = parse_goto_options(&ctx, options)?;
368 let resp = self.inner.go_back(opts).await.into_js()?;
369 Ok(resp.map(|r| crate::bindings::network::ResponseJs::new_with_page(r, self.inner.clone())))
370 }
371
372 #[qjs(rename = "goForward")]
374 pub async fn go_forward<'js>(
375 &self,
376 ctx: rquickjs::Ctx<'js>,
377 options: Opt<rquickjs::Value<'js>>,
378 ) -> rquickjs::Result<Option<crate::bindings::network::ResponseJs>> {
379 let opts = parse_goto_options(&ctx, options)?;
380 let resp = self.inner.go_forward(opts).await.into_js()?;
381 Ok(resp.map(|r| crate::bindings::network::ResponseJs::new_with_page(r, self.inner.clone())))
382 }
383
384 #[qjs(rename = "url")]
387 pub fn url(&self) -> String {
388 self.inner.url()
389 }
390
391 #[qjs(rename = "title")]
393 pub async fn title(&self) -> rquickjs::Result<String> {
394 self.inner.title().await.into_js()
395 }
396
397 #[qjs(rename = "video")]
402 pub fn video<'js>(&self, ctx: rquickjs::Ctx<'js>) -> rquickjs::Result<rquickjs::Value<'js>> {
403 use rquickjs::class::Class;
404 match self.inner.video() {
405 Some(video) => {
406 let wrapper = crate::bindings::video::VideoJs::new(video);
407 let instance = Class::instance(ctx.clone(), wrapper)?;
408 rquickjs::IntoJs::into_js(instance, &ctx)
409 },
410 None => Ok(rquickjs::Value::new_null(ctx)),
411 }
412 }
413
414 #[qjs(rename = "content")]
416 pub async fn content(&self) -> rquickjs::Result<String> {
417 self.inner.content().await.into_js()
418 }
419
420 #[qjs(rename = "setContent")]
422 pub async fn set_content(&self, html: String) -> rquickjs::Result<()> {
423 self.inner.set_content(&html).await.into_js()
424 }
425
426 #[qjs(rename = "addInitScript")]
434 pub async fn add_init_script<'js>(
435 &self,
436 ctx: rquickjs::Ctx<'js>,
437 script: rquickjs::Value<'js>,
438 arg: Opt<rquickjs::Value<'js>>,
439 ) -> rquickjs::Result<String> {
440 let (init, arg_json) = init_script_from_js(&ctx, script, arg.0)?;
441 self.inner.add_init_script(init, arg_json).await.into_js()
442 }
443
444 #[qjs(rename = "removeInitScript")]
446 pub async fn remove_init_script(&self, identifier: String) -> rquickjs::Result<()> {
447 self.inner.remove_init_script(&identifier).await.into_js()
448 }
449
450 #[qjs(rename = "markdown")]
453 pub async fn markdown(&self) -> rquickjs::Result<String> {
454 self.inner.markdown().await.into_js()
455 }
456
457 #[qjs(rename = "waitForSelector")]
461 pub async fn wait_for_selector<'js>(
462 &self,
463 ctx: rquickjs::Ctx<'js>,
464 selector: String,
465 options: Opt<rquickjs::Value<'js>>,
466 ) -> rquickjs::Result<()> {
467 let opts = parse_wait_options(&ctx, options)?;
468 self.inner.wait_for_selector(&selector, opts).await.into_js()
469 }
470
471 #[qjs(rename = "querySelector")]
479 pub async fn query_selector(
480 &self,
481 selector: String,
482 ) -> rquickjs::Result<Option<crate::bindings::element_handle::ElementHandleJs>> {
483 let inner = self.inner.query_selector(&selector).await.into_js()?;
484 Ok(inner.map(crate::bindings::element_handle::ElementHandleJs::new))
485 }
486
487 #[qjs(rename = "$")]
489 pub async fn dollar(
490 &self,
491 selector: String,
492 ) -> rquickjs::Result<Option<crate::bindings::element_handle::ElementHandleJs>> {
493 self.query_selector(selector).await
494 }
495
496 #[qjs(rename = "querySelectorAll")]
498 pub async fn query_selector_all(
499 &self,
500 selector: String,
501 ) -> rquickjs::Result<Vec<crate::bindings::element_handle::ElementHandleJs>> {
502 let inner_handles = self.inner.query_selector_all(&selector).await.into_js()?;
503 Ok(
504 inner_handles
505 .into_iter()
506 .map(crate::bindings::element_handle::ElementHandleJs::new)
507 .collect(),
508 )
509 }
510
511 #[qjs(rename = "$$")]
513 pub async fn dollar_dollar(
514 &self,
515 selector: String,
516 ) -> rquickjs::Result<Vec<crate::bindings::element_handle::ElementHandleJs>> {
517 self.query_selector_all(selector).await
518 }
519
520 #[qjs(rename = "evaluate")]
526 pub async fn evaluate<'js>(
527 &self,
528 ctx: rquickjs::Ctx<'js>,
529 page_function: rquickjs::Value<'js>,
530 arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
531 ) -> rquickjs::Result<rquickjs::Value<'js>> {
532 let (source, is_fn) = extract_page_function(&ctx, page_function)?;
533 let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
534 let result = self.inner.evaluate(&source, serialized, is_fn).await.into_js()?;
535 serialized_value_to_quickjs(&ctx, &result)
536 }
537
538 #[qjs(rename = "evaluateHandle")]
540 pub async fn evaluate_handle<'js>(
541 &self,
542 ctx: rquickjs::Ctx<'js>,
543 page_function: rquickjs::Value<'js>,
544 arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
545 ) -> rquickjs::Result<crate::bindings::js_handle::JSHandleJs> {
546 let (source, is_fn) = extract_page_function(&ctx, page_function)?;
547 let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
548 let handle = self.inner.evaluate_handle(&source, serialized, is_fn).await.into_js()?;
549 Ok(crate::bindings::js_handle::JSHandleJs::new(handle))
550 }
551
552 #[qjs(rename = "locator")]
555 pub fn locator<'js>(
556 &self,
557 ctx: rquickjs::Ctx<'js>,
558 selector: String,
559 options: Opt<rquickjs::Value<'js>>,
560 ) -> rquickjs::Result<LocatorJs> {
561 let parsed = crate::bindings::locator::parse_locator_options_public(&ctx, options, true)?;
562 let opts = ferridriver::options::FilterOptions {
563 has_text: parsed.has_text,
564 has_not_text: parsed.has_not_text,
565 has: parsed.has,
566 has_not: parsed.has_not,
567 visible: parsed.visible,
568 };
569 let filter = if crate::bindings::locator::is_empty_filter(&opts) {
570 None
571 } else {
572 Some(opts)
573 };
574 Ok(LocatorJs::new(self.inner.locator(&selector, filter)))
575 }
576
577 #[qjs(rename = "getByRole")]
581 pub fn get_by_role(
582 &self,
583 role: String,
584 options: rquickjs::function::Opt<rquickjs::Value<'_>>,
585 ) -> rquickjs::Result<LocatorJs> {
586 let opts = parse_role_options(options)?;
587 Ok(LocatorJs::new(self.inner.get_by_role(&role, &opts)))
588 }
589
590 #[qjs(rename = "getByText")]
592 pub fn get_by_text(
593 &self,
594 text: rquickjs::Value<'_>,
595 options: rquickjs::function::Opt<rquickjs::Value<'_>>,
596 ) -> rquickjs::Result<LocatorJs> {
597 let t = string_or_regex_from_js(text)?;
598 let opts = parse_text_options(options);
599 Ok(LocatorJs::new(self.inner.get_by_text(&t, &opts)))
600 }
601
602 #[qjs(rename = "getByLabel")]
604 pub fn get_by_label(
605 &self,
606 text: rquickjs::Value<'_>,
607 options: rquickjs::function::Opt<rquickjs::Value<'_>>,
608 ) -> rquickjs::Result<LocatorJs> {
609 let t = string_or_regex_from_js(text)?;
610 let opts = parse_text_options(options);
611 Ok(LocatorJs::new(self.inner.get_by_label(&t, &opts)))
612 }
613
614 #[qjs(rename = "getByPlaceholder")]
616 pub fn get_by_placeholder(
617 &self,
618 text: rquickjs::Value<'_>,
619 options: rquickjs::function::Opt<rquickjs::Value<'_>>,
620 ) -> rquickjs::Result<LocatorJs> {
621 let t = string_or_regex_from_js(text)?;
622 let opts = parse_text_options(options);
623 Ok(LocatorJs::new(self.inner.get_by_placeholder(&t, &opts)))
624 }
625
626 #[qjs(rename = "getByAltText")]
628 pub fn get_by_alt_text(
629 &self,
630 text: rquickjs::Value<'_>,
631 options: rquickjs::function::Opt<rquickjs::Value<'_>>,
632 ) -> rquickjs::Result<LocatorJs> {
633 let t = string_or_regex_from_js(text)?;
634 let opts = parse_text_options(options);
635 Ok(LocatorJs::new(self.inner.get_by_alt_text(&t, &opts)))
636 }
637
638 #[qjs(rename = "getByTitle")]
640 pub fn get_by_title(
641 &self,
642 text: rquickjs::Value<'_>,
643 options: rquickjs::function::Opt<rquickjs::Value<'_>>,
644 ) -> rquickjs::Result<LocatorJs> {
645 let t = string_or_regex_from_js(text)?;
646 let opts = parse_text_options(options);
647 Ok(LocatorJs::new(self.inner.get_by_title(&t, &opts)))
648 }
649
650 #[qjs(rename = "getByTestId")]
652 pub fn get_by_test_id(&self, test_id: rquickjs::Value<'_>) -> rquickjs::Result<LocatorJs> {
653 let t = string_or_regex_from_js(test_id)?;
654 Ok(LocatorJs::new(self.inner.get_by_test_id(&t)))
655 }
656
657 #[qjs(rename = "click")]
662 pub async fn click<'js>(
663 &self,
664 ctx: rquickjs::Ctx<'js>,
665 selector: String,
666 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
667 ) -> rquickjs::Result<()> {
668 let opts = crate::bindings::convert::parse_click_options(&ctx, options)?;
669 self.inner.click(&selector, opts).await.into_js()
670 }
671
672 #[qjs(rename = "dblclick")]
675 pub async fn dblclick<'js>(
676 &self,
677 ctx: rquickjs::Ctx<'js>,
678 selector: String,
679 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
680 ) -> rquickjs::Result<()> {
681 let opts = crate::bindings::convert::parse_dblclick_options(&ctx, options)?;
682 self.inner.dblclick(&selector, opts).await.into_js()
683 }
684
685 #[qjs(rename = "fill")]
688 pub async fn fill<'js>(
689 &self,
690 ctx: rquickjs::Ctx<'js>,
691 selector: String,
692 value: String,
693 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
694 ) -> rquickjs::Result<()> {
695 let opts = crate::bindings::convert::parse_fill_options(&ctx, options)?;
696 self.inner.fill(&selector, &value, opts).await.into_js()
697 }
698
699 #[qjs(rename = "type")]
705 pub async fn type_<'js>(
706 &self,
707 ctx: rquickjs::Ctx<'js>,
708 selector: String,
709 text: String,
710 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
711 ) -> rquickjs::Result<()> {
712 let opts = crate::bindings::convert::parse_type_options(&ctx, options)?;
713 self.inner.r#type(&selector, &text, opts).await.into_js()
714 }
715
716 #[qjs(rename = "press")]
719 pub async fn press<'js>(
720 &self,
721 ctx: rquickjs::Ctx<'js>,
722 selector: String,
723 key: String,
724 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
725 ) -> rquickjs::Result<()> {
726 let opts = crate::bindings::convert::parse_press_options(&ctx, options)?;
727 self.inner.press(&selector, &key, opts).await.into_js()
728 }
729
730 #[qjs(rename = "focus")]
732 pub async fn focus(
733 &self,
734 selector: String,
735 _options: rquickjs::function::Opt<rquickjs::Value<'_>>,
736 ) -> rquickjs::Result<()> {
737 self.inner.focus(&selector).await.into_js()
738 }
739
740 #[qjs(rename = "hover")]
743 pub async fn hover<'js>(
744 &self,
745 ctx: rquickjs::Ctx<'js>,
746 selector: String,
747 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
748 ) -> rquickjs::Result<()> {
749 let opts = crate::bindings::convert::parse_hover_options(&ctx, options)?;
750 self.inner.hover(&selector, opts).await.into_js()
751 }
752
753 #[qjs(rename = "dispatchEvent")]
756 pub async fn dispatch_event<'js>(
757 &self,
758 ctx: rquickjs::Ctx<'js>,
759 selector: String,
760 event_type: String,
761 event_init: rquickjs::function::Opt<rquickjs::Value<'js>>,
762 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
763 ) -> rquickjs::Result<()> {
764 let init_json = match event_init.0 {
765 Some(v) if !v.is_undefined() && !v.is_null() => {
766 Some(crate::bindings::convert::serde_from_js::<serde_json::Value>(&ctx, v)?)
767 },
768 _ => None,
769 };
770 let opts = crate::bindings::convert::parse_dispatch_event_options(&ctx, options)?;
771 self
772 .inner
773 .dispatch_event(&selector, &event_type, init_json, opts)
774 .await
775 .into_js()
776 }
777
778 #[qjs(rename = "tap")]
781 pub async fn tap<'js>(
782 &self,
783 ctx: rquickjs::Ctx<'js>,
784 selector: String,
785 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
786 ) -> rquickjs::Result<()> {
787 let opts = crate::bindings::convert::parse_tap_options(&ctx, options)?;
788 self.inner.tap(&selector, opts).await.into_js()
789 }
790
791 #[qjs(rename = "check")]
794 pub async fn check<'js>(
795 &self,
796 ctx: rquickjs::Ctx<'js>,
797 selector: String,
798 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
799 ) -> rquickjs::Result<()> {
800 let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
801 self.inner.check(&selector, opts).await.into_js()
802 }
803
804 #[qjs(rename = "uncheck")]
807 pub async fn uncheck<'js>(
808 &self,
809 ctx: rquickjs::Ctx<'js>,
810 selector: String,
811 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
812 ) -> rquickjs::Result<()> {
813 let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
814 self.inner.uncheck(&selector, opts).await.into_js()
815 }
816
817 #[qjs(rename = "setChecked")]
820 pub async fn set_checked<'js>(
821 &self,
822 ctx: rquickjs::Ctx<'js>,
823 selector: String,
824 checked: bool,
825 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
826 ) -> rquickjs::Result<()> {
827 let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
828 self.inner.set_checked(&selector, checked, opts).await.into_js()
829 }
830
831 #[qjs(rename = "selectOption")]
835 pub async fn select_option<'js>(
836 &self,
837 ctx: rquickjs::Ctx<'js>,
838 selector: String,
839 values: rquickjs::Value<'js>,
840 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
841 ) -> rquickjs::Result<Vec<String>> {
842 let values = crate::bindings::convert::parse_select_option_values(&ctx, values)?;
843 let opts = crate::bindings::convert::parse_select_option_options(&ctx, options)?;
844 self.inner.select_option(&selector, values, opts).await.into_js()
845 }
846
847 #[qjs(rename = "textContent")]
851 pub async fn text_content(&self, selector: String) -> rquickjs::Result<Option<String>> {
852 self.inner.text_content(&selector).await.into_js()
853 }
854
855 #[qjs(rename = "innerText")]
857 pub async fn inner_text(&self, selector: String) -> rquickjs::Result<String> {
858 self.inner.inner_text(&selector).await.into_js()
859 }
860
861 #[qjs(rename = "innerHTML")]
863 pub async fn inner_html(&self, selector: String) -> rquickjs::Result<String> {
864 self.inner.inner_html(&selector).await.into_js()
865 }
866
867 #[qjs(rename = "inputValue")]
869 pub async fn input_value(&self, selector: String) -> rquickjs::Result<String> {
870 self.inner.input_value(&selector).await.into_js()
871 }
872
873 #[qjs(rename = "getAttribute")]
876 pub async fn get_attribute(&self, selector: String, name: String) -> rquickjs::Result<Option<String>> {
877 self.inner.get_attribute(&selector, &name).await.into_js()
878 }
879
880 #[qjs(rename = "isVisible")]
882 pub async fn is_visible(&self, selector: String) -> rquickjs::Result<bool> {
883 self.inner.is_visible(&selector).await.into_js()
884 }
885
886 #[qjs(rename = "isHidden")]
888 pub async fn is_hidden(&self, selector: String) -> rquickjs::Result<bool> {
889 self.inner.is_hidden(&selector).await.into_js()
890 }
891
892 #[qjs(rename = "isEnabled")]
894 pub async fn is_enabled(&self, selector: String) -> rquickjs::Result<bool> {
895 self.inner.is_enabled(&selector).await.into_js()
896 }
897
898 #[qjs(rename = "isDisabled")]
900 pub async fn is_disabled(&self, selector: String) -> rquickjs::Result<bool> {
901 self.inner.is_disabled(&selector).await.into_js()
902 }
903
904 #[qjs(rename = "isChecked")]
906 pub async fn is_checked(&self, selector: String) -> rquickjs::Result<bool> {
907 self.inner.is_checked(&selector).await.into_js()
908 }
909
910 #[qjs(get, rename = "mouse")]
915 pub fn mouse(&self) -> MouseJs {
916 MouseJs::new(self.inner.clone())
917 }
918
919 #[qjs(get, rename = "keyboard")]
922 pub fn keyboard(&self) -> KeyboardJs {
923 KeyboardJs::new(self.inner.clone())
924 }
925
926 #[qjs(rename = "clickAt")]
929 pub async fn click_at(&self, x: f64, y: f64) -> rquickjs::Result<()> {
930 self.inner.click_at(x, y).await.into_js()
931 }
932
933 #[qjs(rename = "moveMouseSmooth")]
937 pub async fn move_mouse_smooth(
938 &self,
939 from_x: f64,
940 from_y: f64,
941 to_x: f64,
942 to_y: f64,
943 steps: u32,
944 ) -> rquickjs::Result<()> {
945 self
946 .inner
947 .move_mouse_smooth(from_x, from_y, to_x, to_y, steps)
948 .await
949 .into_js()
950 }
951
952 #[qjs(rename = "dragAndDrop")]
956 pub async fn drag_and_drop<'js>(
957 &self,
958 ctx: rquickjs::Ctx<'js>,
959 source: String,
960 target: String,
961 options: Opt<rquickjs::Value<'js>>,
962 ) -> rquickjs::Result<()> {
963 let opts = parse_drag_options(&ctx, options)?;
964 self.inner.drag_and_drop(&source, &target, opts).await.into_js()
965 }
966
967 #[qjs(rename = "setInputFiles")]
973 pub async fn set_input_files<'js>(
974 &self,
975 ctx: rquickjs::Ctx<'js>,
976 selector: String,
977 files: rquickjs::Value<'js>,
978 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
979 ) -> rquickjs::Result<()> {
980 let files = crate::bindings::convert::parse_input_files(&ctx, files)?;
981 let opts = crate::bindings::convert::parse_set_input_files_options(&ctx, options)?;
982 self.inner.set_input_files(&selector, files, opts).await.into_js()
983 }
984
985 #[qjs(rename = "setViewportSize")]
992 pub async fn set_viewport_size<'js>(
993 &self,
994 ctx: rquickjs::Ctx<'js>,
995 size: rquickjs::Value<'js>,
996 ) -> rquickjs::Result<()> {
997 #[derive(serde::Deserialize)]
998 struct Size {
999 width: i64,
1000 height: i64,
1001 }
1002 let s: Size = crate::bindings::convert::serde_from_js(&ctx, size)?;
1003 self.inner.set_viewport_size(s.width, s.height).await.into_js()
1004 }
1005
1006 #[qjs(rename = "emulateMedia")]
1011 pub async fn emulate_media<'js>(
1012 &self,
1013 ctx: rquickjs::Ctx<'js>,
1014 options: Opt<rquickjs::Value<'js>>,
1015 ) -> rquickjs::Result<()> {
1016 let opts = parse_emulate_media_options(&ctx, options)?;
1017 self.inner.emulate_media(&opts).await.into_js()
1018 }
1019
1020 #[qjs(rename = "screenshot")]
1026 pub async fn screenshot<'js>(
1027 &self,
1028 ctx: rquickjs::Ctx<'js>,
1029 options: Opt<rquickjs::Value<'js>>,
1030 ) -> rquickjs::Result<Vec<u8>> {
1031 let opts = parse_screenshot_options(&ctx, options)?;
1032 self.inner.screenshot(opts).await.into_js()
1033 }
1034
1035 #[qjs(rename = "screenshotElement")]
1037 pub async fn screenshot_element(&self, selector: String) -> rquickjs::Result<Vec<u8>> {
1038 self.inner.screenshot_element(&selector).await.into_js()
1039 }
1040
1041 #[qjs(rename = "pdf")]
1045 pub async fn pdf<'js>(
1046 &self,
1047 ctx: rquickjs::Ctx<'js>,
1048 options: Opt<rquickjs::Value<'js>>,
1049 ) -> rquickjs::Result<Vec<u8>> {
1050 let opts = parse_pdf_options(&ctx, options)?;
1051 self.inner.pdf(opts).await.into_js()
1052 }
1053
1054 #[qjs(rename = "close")]
1059 pub async fn close<'js>(&self, ctx: rquickjs::Ctx<'js>, options: Opt<rquickjs::Value<'js>>) -> rquickjs::Result<()> {
1060 let opts = parse_page_close_options(&ctx, options)?;
1061 self.inner.close(opts).await.into_js()
1062 }
1063
1064 #[qjs(rename = "setDefaultTimeout")]
1067 pub fn set_default_timeout(&self, ms: u64) {
1068 self.inner.set_default_timeout(ms);
1069 }
1070
1071 #[qjs(rename = "setDefaultNavigationTimeout")]
1075 pub fn set_default_navigation_timeout(&self, ms: u64) {
1076 self.inner.set_default_navigation_timeout(ms);
1077 }
1078
1079 #[qjs(rename = "isClosed")]
1081 pub fn is_closed(&self) -> bool {
1082 self.inner.is_closed()
1083 }
1084
1085 #[qjs(rename = "route")]
1103 pub async fn route<'js>(
1104 &self,
1105 ctx: rquickjs::Ctx<'js>,
1106 url: rquickjs::Value<'js>,
1107 handler: rquickjs::Function<'js>,
1108 ) -> rquickjs::Result<()> {
1109 let async_ctx = self.async_ctx.clone().ok_or_else(|| {
1110 rquickjs::Error::new_from_js_message(
1111 "page.route",
1112 "Error",
1113 "page.route requires the script engine's AsyncContext (install_page)".to_string(),
1114 )
1115 })?;
1116 let id = self.next_route_id.fetch_add(1, Ordering::Relaxed);
1117 let saved_handler = rquickjs::Persistent::save(&ctx, handler);
1118 with_page_callbacks(&ctx, |r| r.route_handlers.insert(id, saved_handler))?;
1119
1120 let has_predicate = url.as_function().is_some();
1126 let matcher = if let Some(pred) = url.as_function() {
1127 let saved_pred = rquickjs::Persistent::save(&ctx, pred.clone());
1128 with_page_callbacks(&ctx, |r| r.route_preds.insert(id, saved_pred))?;
1129 let m = ferridriver::url_matcher::UrlMatcher::predicate(|_| true);
1130 self
1131 .route_matchers
1132 .lock()
1133 .unwrap_or_else(std::sync::PoisonError::into_inner)
1134 .insert(id, m.clone());
1135 m
1136 } else {
1137 url_value_to_matcher(&ctx, url)?
1138 };
1139
1140 let rust_handler: ferridriver::route::RouteHandler = std::sync::Arc::new(move |route| {
1151 let async_ctx = async_ctx.clone();
1152 tokio::spawn(async move {
1158 use rquickjs::class::Class;
1159 let _: rquickjs::Result<()> = rquickjs::async_with!(async_ctx => |ctx| {
1160 if has_predicate {
1161 let pred = with_page_callbacks(&ctx, |r| r.route_preds.get(&id).cloned())?
1162 .ok_or_else(|| rquickjs::Error::new_from_js_message("page.route", "Error", "route predicate gone".to_string()))?
1163 .restore(&ctx)?;
1164 let url_ctor: rquickjs::function::Constructor<'_> = ctx.globals().get("URL")?;
1165 let url_obj: rquickjs::Value<'_> = url_ctor.construct((route.request().url.clone(),))?;
1166 if !call_predicate_truthy(&pred, url_obj, &ctx).await? {
1167 route.continue_route(ferridriver::route::ContinueOverrides::default());
1168 return Ok(());
1169 }
1170 }
1171 let f = with_page_callbacks(&ctx, |r| r.route_handlers.get(&id).cloned())?
1172 .ok_or_else(|| rquickjs::Error::new_from_js_message("page.route", "Error", "route handler gone".to_string()))?
1173 .restore(&ctx)?;
1174 let route_class = Class::instance(ctx.clone(), crate::bindings::network::RouteJs::new(route))?;
1175 let _: rquickjs::Value<'_> = f.call((route_class,))?;
1176 Ok(())
1177 })
1178 .await;
1179 });
1180 });
1181
1182 self.inner.route(matcher, rust_handler).await.into_js()
1183 }
1184
1185 #[qjs(rename = "unroute")]
1190 pub async fn unroute<'js>(&self, ctx: rquickjs::Ctx<'js>, url: rquickjs::Value<'js>) -> rquickjs::Result<()> {
1191 if let Some(pred) = url.as_function() {
1192 let saved: Vec<(u64, rquickjs::Persistent<rquickjs::Function<'static>>)> =
1198 with_page_callbacks(&ctx, |r| r.route_preds.iter().map(|(k, v)| (*k, v.clone())).collect())?;
1199 let mut victims: Vec<u64> = Vec::new();
1200 for (id, sp) in saved {
1201 let stored = sp.restore(&ctx)?;
1202 if stored.as_value() == pred.as_value() {
1203 victims.push(id);
1204 }
1205 }
1206 for id in victims {
1207 let m = self
1208 .route_matchers
1209 .lock()
1210 .unwrap_or_else(std::sync::PoisonError::into_inner)
1211 .remove(&id);
1212 if let Some(m) = m {
1213 self.inner.unroute(&m).await.into_js()?;
1214 }
1215 with_page_callbacks(&ctx, |r| {
1216 r.route_preds.remove(&id);
1217 r.route_handlers.remove(&id);
1218 })?;
1219 }
1220 return Ok(());
1221 }
1222 let matcher = url_value_to_matcher(&ctx, url)?;
1223 self.inner.unroute(&matcher).await.into_js()
1224 }
1225
1226 #[qjs(rename = "waitForRequest")]
1236 pub async fn wait_for_request<'js>(
1237 &self,
1238 ctx: rquickjs::Ctx<'js>,
1239 url: rquickjs::Value<'js>,
1240 timeout_ms: Opt<f64>,
1241 ) -> rquickjs::Result<crate::bindings::network::RequestJs> {
1242 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1243 let timeout = timeout_ms.0.map(|t| t as u64);
1244 if let Some(pred) = url.as_function() {
1245 let t = timeout.unwrap_or_else(|| self.inner.default_timeout());
1246 return wait_request_predicate(ctx.clone(), self.inner.clone(), pred.clone(), t).await;
1247 }
1248 let matcher = url_value_to_matcher(&ctx, url)?;
1249 let req = self.inner.wait_for_request(matcher, timeout).await.into_js()?;
1250 Ok(crate::bindings::network::RequestJs::new_with_page(
1251 req,
1252 self.inner.clone(),
1253 ))
1254 }
1255
1256 #[qjs(rename = "waitForResponse")]
1259 pub async fn wait_for_response<'js>(
1260 &self,
1261 ctx: rquickjs::Ctx<'js>,
1262 url: rquickjs::Value<'js>,
1263 timeout_ms: Opt<f64>,
1264 ) -> rquickjs::Result<crate::bindings::network::ResponseJs> {
1265 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1266 let timeout = timeout_ms.0.map(|t| t as u64);
1267 if let Some(pred) = url.as_function() {
1268 let t = timeout.unwrap_or_else(|| self.inner.default_timeout());
1269 return wait_response_predicate(ctx.clone(), self.inner.clone(), pred.clone(), t).await;
1270 }
1271 let matcher = url_value_to_matcher(&ctx, url)?;
1272 let resp = self.inner.wait_for_response(matcher, timeout).await.into_js()?;
1273 Ok(crate::bindings::network::ResponseJs::new_with_page(
1274 resp,
1275 self.inner.clone(),
1276 ))
1277 }
1278
1279 #[qjs(rename = "waitForLoadState")]
1289 pub async fn wait_for_load_state(&self, state: Opt<String>) -> rquickjs::Result<()> {
1290 use crate::bindings::convert::FerriResultExt;
1291 self.inner.wait_for_load_state(state.0.as_deref()).await.into_js()
1292 }
1293
1294 #[qjs(rename = "waitForURL")]
1300 pub async fn wait_for_url<'js>(&self, ctx: rquickjs::Ctx<'js>, url: rquickjs::Value<'js>) -> rquickjs::Result<()> {
1301 use crate::bindings::convert::FerriResultExt;
1302 let matcher = url_value_to_matcher(&ctx, url)?;
1303 self.inner.wait_for_url(matcher).await.into_js()
1304 }
1305
1306 #[qjs(rename = "waitForFunction")]
1312 pub async fn wait_for_function<'js>(
1313 &self,
1314 ctx: rquickjs::Ctx<'js>,
1315 page_function: rquickjs::Value<'js>,
1316 _arg: Opt<rquickjs::Value<'js>>,
1317 options: Opt<rquickjs::Value<'js>>,
1318 ) -> rquickjs::Result<rquickjs::Value<'js>> {
1319 #[derive(serde::Deserialize, Default)]
1320 #[serde(rename_all = "camelCase", default)]
1321 struct JsOpts {
1322 timeout: Option<u64>,
1323 }
1324 let opts: JsOpts = match options.0 {
1325 Some(v) if !v.is_undefined() && !v.is_null() => crate::bindings::convert::serde_from_js(&ctx, v)?,
1326 _ => JsOpts::default(),
1327 };
1328 let (src, is_fn) = crate::bindings::convert::extract_page_function(&ctx, page_function)?;
1329 let expr = if is_fn.unwrap_or(false) {
1333 format!("({src})()")
1334 } else {
1335 src
1336 };
1337 let v = self
1338 .inner
1339 .wait_for_function(&expr, opts.timeout)
1340 .await
1341 .map_err(|e| crate::bindings::convert::to_rq_error(&e))?;
1342 crate::bindings::convert::json_to_js(&ctx, &v)
1343 }
1344
1345 #[qjs(rename = "waitForEvent")]
1346 pub async fn wait_for_event<'js>(
1347 &self,
1348 ctx: rquickjs::Ctx<'js>,
1349 event: String,
1350 timeout_ms: Opt<f64>,
1351 ) -> rquickjs::Result<rquickjs::Value<'js>> {
1352 use rquickjs::class::Class;
1353 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1354 let timeout = timeout_ms.0.unwrap_or(30_000.0) as u64;
1355 let event_lc = event.to_ascii_lowercase();
1356
1357 if event_lc == "dialog" {
1362 let dialog = self
1363 .inner
1364 .wait_for_dialog(timeout)
1365 .await
1366 .map_err(|e| rquickjs::Error::new_from_js_message("Page.waitForEvent", "Error", e.to_string()))?;
1367 let wrapper = crate::bindings::dialog::DialogJs::new(dialog);
1368 let instance = Class::instance(ctx.clone(), wrapper)?;
1369 return rquickjs::IntoJs::into_js(instance, &ctx);
1370 }
1371 if event_lc == "filechooser" {
1375 let chooser = self
1376 .inner
1377 .wait_for_file_chooser(timeout)
1378 .await
1379 .map_err(|e| rquickjs::Error::new_from_js_message("Page.waitForEvent", "Error", e.to_string()))?;
1380 let wrapper = crate::bindings::file_chooser::FileChooserJs::new(chooser);
1381 let instance = Class::instance(ctx.clone(), wrapper)?;
1382 return rquickjs::IntoJs::into_js(instance, &ctx);
1383 }
1384 if event_lc == "download" {
1387 let download = self
1388 .inner
1389 .wait_for_download(timeout)
1390 .await
1391 .map_err(|e| rquickjs::Error::new_from_js_message("Page.waitForEvent", "Error", e.to_string()))?;
1392 let wrapper = crate::bindings::download::DownloadJs::new(download);
1393 let instance = Class::instance(ctx.clone(), wrapper)?;
1394 return rquickjs::IntoJs::into_js(instance, &ctx);
1395 }
1396
1397 let name = event_lc.clone();
1398 let ev = self
1399 .inner
1400 .events()
1401 .wait_for(move |e| match_event_name(&name, e), timeout)
1402 .await
1403 .map_err(|e| rquickjs::Error::new_from_js_message("Page.waitForEvent", "Error", e.to_string()))?;
1404 match ev {
1405 ferridriver::events::PageEvent::WebSocket(ws) => {
1406 let wrapper = crate::bindings::network::WebSocketJs::new(ws);
1407 let instance = Class::instance(ctx.clone(), wrapper)?;
1408 rquickjs::IntoJs::into_js(instance, &ctx)
1409 },
1410 ferridriver::events::PageEvent::Request(req)
1411 | ferridriver::events::PageEvent::RequestFinished(req)
1412 | ferridriver::events::PageEvent::RequestFailed(req) => {
1413 let wrapper = crate::bindings::network::RequestJs::new_with_page(req, self.inner.clone());
1414 let instance = Class::instance(ctx.clone(), wrapper)?;
1415 rquickjs::IntoJs::into_js(instance, &ctx)
1416 },
1417 ferridriver::events::PageEvent::Response(resp) => {
1418 let wrapper = crate::bindings::network::ResponseJs::new_with_page(resp, self.inner.clone());
1419 let instance = Class::instance(ctx.clone(), wrapper)?;
1420 rquickjs::IntoJs::into_js(instance, &ctx)
1421 },
1422 ferridriver::events::PageEvent::Dialog(dialog) => {
1423 let wrapper = crate::bindings::dialog::DialogJs::new(dialog);
1427 let instance = Class::instance(ctx.clone(), wrapper)?;
1428 rquickjs::IntoJs::into_js(instance, &ctx)
1429 },
1430 ferridriver::events::PageEvent::FileChooser(chooser) => {
1431 let wrapper = crate::bindings::file_chooser::FileChooserJs::new(chooser);
1432 let instance = Class::instance(ctx.clone(), wrapper)?;
1433 rquickjs::IntoJs::into_js(instance, &ctx)
1434 },
1435 ferridriver::events::PageEvent::Download(download) => {
1436 let wrapper = crate::bindings::download::DownloadJs::new(download);
1437 let instance = Class::instance(ctx.clone(), wrapper)?;
1438 rquickjs::IntoJs::into_js(instance, &ctx)
1439 },
1440 ferridriver::events::PageEvent::Console(msg) => {
1441 let wrapper = crate::bindings::console_message::ConsoleMessageJs::new(msg);
1442 let instance = Class::instance(ctx.clone(), wrapper)?;
1443 rquickjs::IntoJs::into_js(instance, &ctx)
1444 },
1445 ferridriver::events::PageEvent::PageError(err) => {
1449 crate::bindings::web_error::build_native_error(&ctx, err.error())
1450 },
1451 other => page_event_to_js(&ctx, &other),
1452 }
1453 }
1454
1455 #[qjs(rename = "mainFrame")]
1465 pub fn main_frame(&self) -> crate::bindings::frame::FrameJs {
1466 crate::bindings::frame::FrameJs::new(self.inner.main_frame())
1467 }
1468
1469 #[qjs(rename = "frames")]
1472 pub fn frames(&self) -> Vec<crate::bindings::frame::FrameJs> {
1473 self
1474 .inner
1475 .frames()
1476 .into_iter()
1477 .map(crate::bindings::frame::FrameJs::new)
1478 .collect()
1479 }
1480
1481 #[qjs(rename = "frameLocator")]
1485 pub fn frame_locator(&self, selector: String) -> crate::bindings::frame_locator::FrameLocatorJs {
1486 crate::bindings::frame_locator::FrameLocatorJs::new(self.inner.frame_locator(&selector))
1487 }
1488
1489 #[qjs(rename = "frame")]
1497 pub fn frame<'js>(
1498 &self,
1499 ctx: rquickjs::Ctx<'js>,
1500 selector: rquickjs::Value<'js>,
1501 ) -> rquickjs::Result<Option<crate::bindings::frame::FrameJs>> {
1502 let core_sel = if let Some(s) = selector.as_string() {
1503 ferridriver::options::FrameSelector::by_name(s.to_string()?)
1504 } else if let Some(obj) = selector.as_object() {
1505 let read = |key: &str| -> rquickjs::Result<Option<String>> {
1506 let v: rquickjs::Value<'_> = obj
1507 .get(key)
1508 .unwrap_or_else(|_| rquickjs::Value::new_undefined(ctx.clone()));
1509 if v.is_undefined() || v.is_null() {
1510 Ok(None)
1511 } else if let Some(s) = v.as_string() {
1512 Ok(Some(s.to_string()?))
1513 } else {
1514 Ok(None)
1515 }
1516 };
1517 ferridriver::options::FrameSelector {
1518 name: read("name")?,
1519 url: read("url")?,
1520 }
1521 } else {
1522 return Ok(None);
1523 };
1524
1525 if core_sel.is_empty() {
1526 return Ok(None);
1527 }
1528 Ok(self.inner.frame(core_sel).map(crate::bindings::frame::FrameJs::new))
1529 }
1530
1531 #[qjs(rename = "touchscreen", get)]
1533 pub fn touchscreen(&self) -> TouchscreenJs {
1534 TouchscreenJs {
1535 page: self.inner.clone(),
1536 }
1537 }
1538
1539 #[qjs(rename = "snapshotForAI")]
1544 pub async fn snapshot_for_ai<'js>(
1545 &self,
1546 ctx: rquickjs::Ctx<'js>,
1547 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
1548 ) -> rquickjs::Result<rquickjs::Value<'js>> {
1549 let core_opts = match options.0 {
1550 None => ferridriver::snapshot::SnapshotOptions::default(),
1551 Some(v) if v.is_undefined() || v.is_null() => ferridriver::snapshot::SnapshotOptions::default(),
1552 Some(v) => {
1553 #[derive(serde::Deserialize, Default)]
1554 #[serde(rename_all = "camelCase", default)]
1555 struct JsSnap {
1556 depth: Option<i32>,
1557 track: Option<String>,
1558 }
1559 let parsed: JsSnap = crate::bindings::convert::serde_from_js(&ctx, v)?;
1560 ferridriver::snapshot::SnapshotOptions {
1561 depth: parsed.depth,
1562 track: parsed.track,
1563 }
1564 },
1565 };
1566 let snap = self.inner.snapshot_for_ai(core_opts).await.into_js()?;
1567 let obj = rquickjs::Object::new(ctx.clone())?;
1568 obj.set("full", snap.full)?;
1569 if let Some(inc) = snap.incremental {
1570 obj.set("incremental", inc)?;
1571 }
1572 let ref_map = rquickjs::Object::new(ctx.clone())?;
1573 for (k, v) in snap.ref_map {
1574 ref_map.set(k, v as f64)?;
1575 }
1576 obj.set("refMap", ref_map)?;
1577 rquickjs::IntoJs::into_js(obj, &ctx)
1578 }
1579
1580 #[qjs(rename = "ariaSnapshot")]
1582 pub async fn aria_snapshot<'js>(
1583 &self,
1584 ctx: rquickjs::Ctx<'js>,
1585 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
1586 ) -> rquickjs::Result<String> {
1587 let core_opts = match options.0 {
1588 Some(v) if !v.is_undefined() && !v.is_null() => {
1589 #[derive(serde::Deserialize, Default)]
1590 #[serde(rename_all = "camelCase", default)]
1591 struct JsSnap {
1592 depth: Option<i32>,
1593 track: Option<String>,
1594 }
1595 let p: JsSnap = crate::bindings::convert::serde_from_js(&ctx, v)?;
1596 ferridriver::snapshot::SnapshotOptions {
1597 depth: p.depth,
1598 track: p.track,
1599 }
1600 },
1601 _ => ferridriver::snapshot::SnapshotOptions::default(),
1602 };
1603 self.inner.aria_snapshot(core_opts).await.into_js()
1604 }
1605
1606 #[qjs(rename = "exposeFunction")]
1615 pub async fn expose_function<'js>(
1616 &self,
1617 ctx: rquickjs::Ctx<'js>,
1618 name: String,
1619 callback: rquickjs::Function<'js>,
1620 ) -> rquickjs::Result<()> {
1621 let async_ctx = self.async_ctx.clone().ok_or_else(|| {
1622 rquickjs::Error::new_from_js_message(
1623 "page.exposeFunction",
1624 "Error",
1625 "page.exposeFunction requires the script engine's AsyncContext (install_page)".to_string(),
1626 )
1627 })?;
1628 let saved = rquickjs::Persistent::save(&ctx, callback);
1632 with_page_callbacks(&ctx, |r| r.exposed.insert(name.clone(), saved))?;
1633
1634 let cb: ferridriver::events::ExposedFn = std::sync::Arc::new({
1635 let name = name.clone();
1636 move |args: Vec<serde_json::Value>| {
1637 let async_ctx = async_ctx.clone();
1638 let name = name.clone();
1639 Box::pin(async move {
1647 let out: rquickjs::Result<serde_json::Value> = rquickjs::async_with!(async_ctx => |ctx| {
1648 let f = with_page_callbacks(&ctx, |r| r.exposed.get(&name).cloned())?
1649 .ok_or_else(|| {
1650 rquickjs::Error::new_from_js_message(
1651 "page.exposeFunction",
1652 "Error",
1653 "exposed callback gone".to_string(),
1654 )
1655 })?
1656 .restore(&ctx)?;
1657 let mut call_args = rquickjs::function::Args::new_unsized(ctx.clone());
1662 for v in args {
1663 call_args.push_arg(crate::bindings::convert::json_to_js(&ctx, &v)?)?;
1669 }
1670 let mp: rquickjs::promise::MaybePromise<'_> = call_args.apply(&f)?;
1671 let res = mp.into_future::<rquickjs::Value<'_>>().await?;
1672 let json = match ctx.json_stringify(res)? {
1677 Some(s) => serde_json::from_str(&s.to_string()?).unwrap_or(serde_json::Value::Null),
1678 None => serde_json::Value::Null,
1679 };
1680 Ok(json)
1681 })
1682 .await;
1683 out.unwrap_or(serde_json::Value::Null)
1684 })
1685 }
1686 });
1687 self.inner.expose_function(&name, cb).await.into_js()
1688 }
1689
1690 #[qjs(rename = "startScreencast")]
1695 pub async fn start_screencast<'js>(
1696 &self,
1697 ctx: rquickjs::Ctx<'js>,
1698 quality: u8,
1699 max_width: u32,
1700 max_height: u32,
1701 callback: rquickjs::Function<'js>,
1702 ) -> rquickjs::Result<()> {
1703 let async_ctx = self.async_ctx.clone().ok_or_else(|| {
1704 rquickjs::Error::new_from_js_message(
1705 "page.startScreencast",
1706 "Error",
1707 "page.startScreencast requires the script engine's AsyncContext (install_page)".to_string(),
1708 )
1709 })?;
1710 let saved = rquickjs::Persistent::save(&ctx, callback);
1711 with_page_callbacks(&ctx, |r| r.screencast = Some(saved))?;
1712 let (mut rx, _shutdown) = self
1718 .inner
1719 .start_screencast(quality, max_width, max_height)
1720 .await
1721 .into_js()?;
1722 tokio::spawn(async move {
1723 while let Some((bytes, ts)) = rx.recv().await {
1724 let _: rquickjs::Result<()> = rquickjs::async_with!(async_ctx => |ctx| {
1725 let f = with_page_callbacks(&ctx, |r| r.screencast.clone())?
1726 .ok_or_else(|| rquickjs::Error::new_from_js_message("page.startScreencast", "Error", "screencast callback gone".to_string()))?
1727 .restore(&ctx)?;
1728 let payload = rquickjs::Object::new(ctx.clone())?;
1729 let buf = rquickjs::TypedArray::<u8>::new(ctx.clone(), bytes)?;
1730 payload.set("frame", buf)?;
1731 payload.set("timestamp", ts)?;
1732 let _: rquickjs::Value<'_> = f.call((payload,))?;
1733 Ok(())
1734 })
1735 .await;
1736 }
1737 });
1738 Ok(())
1739 }
1740
1741 #[qjs(rename = "stopScreencast")]
1744 pub async fn stop_screencast(&self) -> rquickjs::Result<()> {
1745 self.inner.stop_screencast().await.into_js()
1746 }
1747}
1748
1749#[derive(rquickjs::JsLifetime, rquickjs::class::Trace)]
1751#[rquickjs::class(rename = "Touchscreen")]
1752pub struct TouchscreenJs {
1753 #[qjs(skip_trace)]
1754 page: std::sync::Arc<ferridriver::Page>,
1755}
1756
1757#[rquickjs::methods]
1758impl TouchscreenJs {
1759 #[qjs(rename = "tap")]
1761 pub async fn tap(&self, x: f64, y: f64) -> rquickjs::Result<()> {
1762 self.page.touchscreen().tap(x, y).await.into_js()
1763 }
1764}
1765
1766#[derive(Debug, Default, Deserialize)]
1770#[serde(default, rename_all = "camelCase")]
1771struct JsScreenshotOptions {
1772 animations: Option<String>,
1773 caret: Option<String>,
1774 clip: Option<JsClipRect>,
1775 full_page: Option<bool>,
1776 #[serde(rename = "type")]
1777 format: Option<String>,
1778 mask: Option<Vec<String>>,
1783 mask_color: Option<String>,
1784 omit_background: Option<bool>,
1785 path: Option<String>,
1786 quality: Option<i64>,
1787 scale: Option<String>,
1788 style: Option<String>,
1789 timeout: Option<u64>,
1790}
1791
1792#[derive(Debug, Default, Deserialize, Clone, Copy)]
1793struct JsClipRect {
1794 x: f64,
1795 y: f64,
1796 width: f64,
1797 height: f64,
1798}
1799
1800impl From<JsClipRect> for ferridriver::options::ClipRect {
1801 fn from(c: JsClipRect) -> Self {
1802 Self {
1803 x: c.x,
1804 y: c.y,
1805 width: c.width,
1806 height: c.height,
1807 }
1808 }
1809}
1810
1811fn parse_screenshot_options<'js>(
1812 ctx: &rquickjs::Ctx<'js>,
1813 value: Opt<rquickjs::Value<'js>>,
1814) -> rquickjs::Result<ferridriver::options::ScreenshotOptions> {
1815 match value.0 {
1816 Some(v) if !v.is_undefined() && !v.is_null() => {
1817 let js: JsScreenshotOptions = serde_from_js(ctx, v)?;
1818 Ok(ferridriver::options::ScreenshotOptions {
1819 animations: js.animations,
1820 caret: js.caret,
1821 clip: js.clip.map(Into::into),
1822 full_page: js.full_page,
1823 format: js.format,
1824 mask: js.mask.unwrap_or_default(),
1825 mask_color: js.mask_color,
1826 omit_background: js.omit_background,
1827 path: js.path.map(std::path::PathBuf::from),
1828 quality: js.quality,
1829 scale: js.scale,
1830 style: js.style,
1831 timeout: js.timeout,
1832 })
1833 },
1834 _ => Ok(ferridriver::options::ScreenshotOptions::default()),
1835 }
1836}
1837
1838#[derive(Debug, Default, Deserialize)]
1842#[serde(default, rename_all = "camelCase")]
1843struct JsPdfOptions {
1844 format: Option<String>,
1845 landscape: Option<bool>,
1846 print_background: Option<bool>,
1847 scale: Option<f64>,
1848 display_header_footer: Option<bool>,
1849 header_template: Option<String>,
1850 footer_template: Option<String>,
1851 page_ranges: Option<String>,
1852 prefer_css_page_size: Option<bool>,
1853 outline: Option<bool>,
1854 tagged: Option<bool>,
1855}
1856
1857fn parse_pdf_options<'js>(
1858 ctx: &rquickjs::Ctx<'js>,
1859 value: Opt<rquickjs::Value<'js>>,
1860) -> rquickjs::Result<ferridriver::options::PdfOptions> {
1861 match value.0 {
1862 Some(v) if !v.is_undefined() && !v.is_null() => {
1863 let js: JsPdfOptions = serde_from_js(ctx, v)?;
1864 Ok(ferridriver::options::PdfOptions {
1865 format: js.format,
1866 path: None,
1867 scale: js.scale,
1868 display_header_footer: js.display_header_footer,
1869 header_template: js.header_template,
1870 footer_template: js.footer_template,
1871 print_background: js.print_background,
1872 landscape: js.landscape,
1873 page_ranges: js.page_ranges,
1874 width: None,
1875 height: None,
1876 margin: None,
1877 prefer_css_page_size: js.prefer_css_page_size,
1878 outline: js.outline,
1879 tagged: js.tagged,
1880 })
1881 },
1882 _ => Ok(ferridriver::options::PdfOptions::default()),
1883 }
1884}
1885
1886fn match_event_name(name: &str, ev: &ferridriver::events::PageEvent) -> bool {
1887 use ferridriver::events::PageEvent;
1888 matches!(
1889 (name, ev),
1890 ("console", PageEvent::Console(_))
1891 | ("request", PageEvent::Request(_))
1892 | ("response", PageEvent::Response(_))
1893 | ("requestfinished", PageEvent::RequestFinished(_))
1894 | ("requestfailed", PageEvent::RequestFailed(_))
1895 | ("websocket", PageEvent::WebSocket(_))
1896 | ("dialog", PageEvent::Dialog(_))
1897 | ("filechooser", PageEvent::FileChooser(_))
1898 | ("frameattached", PageEvent::FrameAttached(_))
1899 | ("framedetached", PageEvent::FrameDetached { .. })
1900 | ("framenavigated", PageEvent::FrameNavigated(_))
1901 | ("load", PageEvent::Load)
1902 | ("domcontentloaded", PageEvent::DomContentLoaded)
1903 | ("close", PageEvent::Close)
1904 | ("pageerror", PageEvent::PageError(_))
1905 | ("download", PageEvent::Download(_))
1906 )
1907}
1908
1909fn page_event_to_js<'js>(
1913 ctx: &rquickjs::Ctx<'js>,
1914 ev: &ferridriver::events::PageEvent,
1915) -> rquickjs::Result<rquickjs::Value<'js>> {
1916 use ferridriver::events::PageEvent;
1917 let obj = || rquickjs::Object::new(ctx.clone());
1918 match ev {
1919 PageEvent::Console(msg) => {
1920 let loc = msg.location();
1921 let o = obj()?;
1922 o.set("type", msg.type_str())?;
1923 o.set("text", msg.text())?;
1924 let l = obj()?;
1925 l.set("url", loc.url.as_str())?;
1926 l.set("lineNumber", f64::from(loc.line_number))?;
1927 l.set("columnNumber", f64::from(loc.column_number))?;
1928 o.set("location", l)?;
1929 o.set("timestamp", msg.timestamp())?;
1930 o.set("argsCount", msg.args().len() as f64)?;
1931 Ok(o.into_value())
1932 },
1933 PageEvent::Dialog(d) => {
1934 let o = obj()?;
1935 o.set("type", d.dialog_type().as_str())?;
1936 o.set("message", d.message())?;
1937 o.set("defaultValue", d.default_value())?;
1938 Ok(o.into_value())
1939 },
1940 PageEvent::FileChooser(fc) => {
1941 let o = obj()?;
1942 o.set("isMultiple", fc.is_multiple())?;
1943 Ok(o.into_value())
1944 },
1945 PageEvent::FrameAttached(f) | PageEvent::FrameNavigated(f) => crate::bindings::convert::serde_to_js(ctx, f),
1946 PageEvent::FrameDetached { frame_id } => {
1947 let o = obj()?;
1948 o.set("frameId", frame_id.as_str())?;
1949 Ok(o.into_value())
1950 },
1951 PageEvent::Download(d) => {
1952 let o = obj()?;
1953 o.set("url", d.url())?;
1954 o.set("suggestedFilename", d.suggested_filename())?;
1955 Ok(o.into_value())
1956 },
1957 PageEvent::Load => {
1958 let o = obj()?;
1959 o.set("type", "load")?;
1960 Ok(o.into_value())
1961 },
1962 PageEvent::DomContentLoaded => {
1963 let o = obj()?;
1964 o.set("type", "domcontentloaded")?;
1965 Ok(o.into_value())
1966 },
1967 PageEvent::Close => {
1968 let o = obj()?;
1969 o.set("type", "close")?;
1970 Ok(o.into_value())
1971 },
1972 PageEvent::PageError(err) => {
1973 let details = err.error();
1974 let o = obj()?;
1975 o.set("name", details.name.as_str())?;
1976 o.set("message", details.message.as_str())?;
1977 o.set("stack", details.stack.as_str())?;
1978 Ok(o.into_value())
1979 },
1980 _ => Ok(rquickjs::Value::new_null(ctx.clone())),
1981 }
1982}
1983
1984fn js_truthy(v: &rquickjs::Value<'_>) -> bool {
1986 if v.is_undefined() || v.is_null() {
1987 return false;
1988 }
1989 if let Some(b) = v.as_bool() {
1990 return b;
1991 }
1992 if let Some(i) = v.as_int() {
1993 return i != 0;
1994 }
1995 if let Some(f) = v.as_float() {
1996 return f != 0.0 && !f.is_nan();
1997 }
1998 if let Some(s) = v.as_string() {
1999 return !s.to_string().unwrap_or_default().is_empty();
2000 }
2001 true
2002}
2003
2004async fn call_predicate_truthy<'js>(
2006 pred: &rquickjs::Function<'js>,
2007 arg: impl rquickjs::IntoJs<'js>,
2008 ctx: &rquickjs::Ctx<'js>,
2009) -> rquickjs::Result<bool> {
2010 let arg = arg.into_js(ctx)?;
2011 let mp: rquickjs::promise::MaybePromise<'js> = pred.call((arg,))?;
2012 let v: rquickjs::Value<'js> = mp.into_future().await?;
2013 Ok(js_truthy(&v))
2014}
2015
2016async fn wait_request_predicate<'js>(
2020 ctx: rquickjs::Ctx<'js>,
2021 page: Arc<Page>,
2022 pred: rquickjs::Function<'js>,
2023 timeout_ms: u64,
2024) -> rquickjs::Result<crate::bindings::network::RequestJs> {
2025 use ferridriver::events::PageEvent;
2026 use rquickjs::class::Class;
2027 let mut rx = page.events().subscribe();
2028 let deadline = tokio::time::Instant::now() + std::time::Duration::from_millis(timeout_ms);
2029 loop {
2030 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
2031 if remaining.is_zero() {
2032 return Err(rquickjs::Error::new_from_js_message(
2033 "page.waitForRequest",
2034 "TimeoutError",
2035 format!("Timeout {timeout_ms}ms exceeded while waiting for request"),
2036 ));
2037 }
2038 match tokio::time::timeout(remaining, rx.recv()).await {
2039 Ok(Ok(PageEvent::Request(req))) => {
2040 let probe = crate::bindings::network::RequestJs::new_with_page(req.clone(), page.clone());
2041 let inst = Class::instance(ctx.clone(), probe)?;
2042 if call_predicate_truthy(&pred, inst, &ctx).await? {
2043 return Ok(crate::bindings::network::RequestJs::new_with_page(req, page.clone()));
2044 }
2045 },
2046 Ok(Ok(_) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => {},
2047 Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => {
2048 return Err(rquickjs::Error::new_from_js_message(
2049 "page.waitForRequest",
2050 "Error",
2051 "page closed while waiting for request".to_string(),
2052 ));
2053 },
2054 Err(_) => {
2055 return Err(rquickjs::Error::new_from_js_message(
2056 "page.waitForRequest",
2057 "TimeoutError",
2058 format!("Timeout {timeout_ms}ms exceeded while waiting for request"),
2059 ));
2060 },
2061 }
2062 }
2063}
2064
2065async fn wait_response_predicate<'js>(
2067 ctx: rquickjs::Ctx<'js>,
2068 page: Arc<Page>,
2069 pred: rquickjs::Function<'js>,
2070 timeout_ms: u64,
2071) -> rquickjs::Result<crate::bindings::network::ResponseJs> {
2072 use ferridriver::events::PageEvent;
2073 use rquickjs::class::Class;
2074 let mut rx = page.events().subscribe();
2075 let deadline = tokio::time::Instant::now() + std::time::Duration::from_millis(timeout_ms);
2076 loop {
2077 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
2078 if remaining.is_zero() {
2079 return Err(rquickjs::Error::new_from_js_message(
2080 "page.waitForResponse",
2081 "TimeoutError",
2082 format!("Timeout {timeout_ms}ms exceeded while waiting for response"),
2083 ));
2084 }
2085 match tokio::time::timeout(remaining, rx.recv()).await {
2086 Ok(Ok(PageEvent::Response(resp))) => {
2087 let probe = crate::bindings::network::ResponseJs::new_with_page(resp.clone(), page.clone());
2088 let inst = Class::instance(ctx.clone(), probe)?;
2089 if call_predicate_truthy(&pred, inst, &ctx).await? {
2090 return Ok(crate::bindings::network::ResponseJs::new_with_page(resp, page.clone()));
2091 }
2092 },
2093 Ok(Ok(_) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => {},
2094 Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => {
2095 return Err(rquickjs::Error::new_from_js_message(
2096 "page.waitForResponse",
2097 "Error",
2098 "page closed while waiting for response".to_string(),
2099 ));
2100 },
2101 Err(_) => {
2102 return Err(rquickjs::Error::new_from_js_message(
2103 "page.waitForResponse",
2104 "TimeoutError",
2105 format!("Timeout {timeout_ms}ms exceeded while waiting for response"),
2106 ));
2107 },
2108 }
2109 }
2110}
2111
2112fn url_value_to_matcher<'js>(
2117 ctx: &rquickjs::Ctx<'js>,
2118 value: rquickjs::Value<'js>,
2119) -> rquickjs::Result<ferridriver::url_matcher::UrlMatcher> {
2120 use crate::bindings::convert::FerriResultExt;
2121 if let Some(s) = value.as_string() {
2122 let glob = s.to_string()?;
2123 return ferridriver::url_matcher::UrlMatcher::glob(glob).into_js();
2124 }
2125 if let Some(obj) = value.as_object() {
2126 let source: rquickjs::Result<String> = obj.get("source");
2129 let flags: rquickjs::Result<String> = obj.get("flags");
2130 if let (Ok(source), Ok(flags)) = (source, flags) {
2131 return ferridriver::url_matcher::UrlMatcher::regex_from_source(&source, &flags).into_js();
2132 }
2133 }
2134 let _ = ctx;
2135 Err(rquickjs::Error::new_from_js_message(
2136 "Page.waitFor*",
2137 "url",
2138 "expected string | RegExp".to_string(),
2139 ))
2140}
2141
2142pub(crate) fn string_or_regex_from_js(
2148 value: rquickjs::Value<'_>,
2149) -> rquickjs::Result<ferridriver::options::StringOrRegex> {
2150 if let Some(s) = value.as_string() {
2151 return Ok(ferridriver::options::StringOrRegex::String(s.to_string()?));
2152 }
2153 if let Some(obj) = value.as_object() {
2154 let source: rquickjs::Result<String> = obj.get("source");
2155 let flags: rquickjs::Result<String> = obj.get("flags");
2156 if let (Ok(source), Ok(flags)) = (source, flags) {
2157 return Ok(ferridriver::options::StringOrRegex::Regex { source, flags });
2158 }
2159 }
2160 Err(rquickjs::Error::new_from_js_message(
2161 "getBy*",
2162 "text",
2163 "expected string | RegExp".to_string(),
2164 ))
2165}
2166
2167pub(crate) fn parse_text_options(
2169 value: rquickjs::function::Opt<rquickjs::Value<'_>>,
2170) -> ferridriver::options::TextOptions {
2171 let Some(v) = value.0 else {
2172 return ferridriver::options::TextOptions::default();
2173 };
2174 if v.is_undefined() || v.is_null() {
2175 return ferridriver::options::TextOptions::default();
2176 }
2177 let Some(obj) = v.as_object() else {
2178 return ferridriver::options::TextOptions::default();
2179 };
2180 let exact: Option<bool> = obj.get("exact").ok();
2181 ferridriver::options::TextOptions { exact }
2182}
2183
2184pub(crate) fn parse_role_options<'js>(
2188 value: rquickjs::function::Opt<rquickjs::Value<'js>>,
2189) -> rquickjs::Result<ferridriver::options::RoleOptions> {
2190 let Some(v) = value.0 else {
2191 return Ok(ferridriver::options::RoleOptions::default());
2192 };
2193 if v.is_undefined() || v.is_null() {
2194 return Ok(ferridriver::options::RoleOptions::default());
2195 }
2196 let Some(obj) = v.as_object() else {
2197 return Ok(ferridriver::options::RoleOptions::default());
2198 };
2199 let name_val: Option<rquickjs::Value<'js>> = obj.get("name").ok();
2200 let name = match name_val {
2201 Some(val) if !val.is_undefined() && !val.is_null() => Some(string_or_regex_from_js(val)?),
2202 _ => None,
2203 };
2204 let exact: Option<bool> = obj.get("exact").ok();
2205 let checked: Option<bool> = obj.get("checked").ok();
2206 let disabled: Option<bool> = obj.get("disabled").ok();
2207 let expanded: Option<bool> = obj.get("expanded").ok();
2208 let level: Option<i32> = obj.get("level").ok();
2209 let pressed: Option<bool> = obj.get("pressed").ok();
2210 let selected: Option<bool> = obj.get("selected").ok();
2211 let include_hidden: Option<bool> = obj.get("includeHidden").ok();
2212 Ok(ferridriver::options::RoleOptions {
2213 name,
2214 exact,
2215 checked,
2216 disabled,
2217 expanded,
2218 level,
2219 pressed,
2220 selected,
2221 include_hidden,
2222 })
2223}