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
34pub(crate) fn 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 locator_handlers: rustc_hash::FxHashMap<u64, rquickjs::Persistent<rquickjs::Function<'static>>>,
222}
223
224impl PageCallbacks {
225 pub(crate) fn insert_route_handler(&mut self, id: u64, f: rquickjs::Persistent<rquickjs::Function<'static>>) {
226 self.route_handlers.insert(id, f);
227 }
228
229 pub(crate) fn insert_route_pred(&mut self, id: u64, f: rquickjs::Persistent<rquickjs::Function<'static>>) {
230 self.route_preds.insert(id, f);
231 }
232
233 pub(crate) fn get_route_handler(&self, id: u64) -> Option<rquickjs::Persistent<rquickjs::Function<'static>>> {
234 self.route_handlers.get(&id).cloned()
235 }
236
237 pub(crate) fn get_route_pred(&self, id: u64) -> Option<rquickjs::Persistent<rquickjs::Function<'static>>> {
238 self.route_preds.get(&id).cloned()
239 }
240
241 pub(crate) fn route_preds_snapshot(&self) -> Vec<(u64, rquickjs::Persistent<rquickjs::Function<'static>>)> {
242 self.route_preds.iter().map(|(k, v)| (*k, v.clone())).collect()
243 }
244
245 pub(crate) fn remove_route(&mut self, id: u64) {
246 self.route_preds.remove(&id);
247 self.route_handlers.remove(&id);
248 }
249
250 pub(crate) fn remove_locator_handler(&mut self, id: u64) {
251 self.locator_handlers.remove(&id);
252 }
253}
254
255pub(crate) struct PageCallbacksUd(std::cell::RefCell<PageCallbacks>);
256
257#[allow(unsafe_code)]
261unsafe impl rquickjs::JsLifetime<'_> for PageCallbacksUd {
262 type Changed<'to> = PageCallbacksUd;
263}
264
265pub(crate) fn ensure_page_callbacks(ctx: &rquickjs::Ctx<'_>) {
269 if ctx.userdata::<PageCallbacksUd>().is_none() {
270 let _ = ctx.store_userdata(PageCallbacksUd(std::cell::RefCell::new(PageCallbacks::default())));
271 }
272}
273
274pub(crate) fn with_page_callbacks<R>(
275 ctx: &rquickjs::Ctx<'_>,
276 f: impl FnOnce(&mut PageCallbacks) -> R,
277) -> rquickjs::Result<R> {
278 ensure_page_callbacks(ctx);
279 let ud = ctx.userdata::<PageCallbacksUd>().ok_or_else(|| {
280 rquickjs::Error::new_from_js_message("page", "Error", "page callbacks registry missing".to_string())
281 })?;
282 let mut reg = ud.0.borrow_mut();
283 Ok(f(&mut reg))
284}
285
286pub(crate) fn insert_exposed_callback(
292 ctx: &rquickjs::Ctx<'_>,
293 name: String,
294 cb: rquickjs::Persistent<rquickjs::Function<'static>>,
295) -> rquickjs::Result<()> {
296 with_page_callbacks(ctx, |r| r.exposed.insert(name, cb))?;
297 Ok(())
298}
299
300pub(crate) fn get_exposed_callback(
302 ctx: &rquickjs::Ctx<'_>,
303 name: &str,
304) -> rquickjs::Result<Option<rquickjs::Persistent<rquickjs::Function<'static>>>> {
305 with_page_callbacks(ctx, |r| r.exposed.get(name).cloned())
306}
307
308fn parse_unroute_behavior(behavior: &str) -> rquickjs::Result<ferridriver::options::UnrouteBehavior> {
309 match behavior {
310 "default" => Ok(ferridriver::options::UnrouteBehavior::Default),
311 "wait" => Ok(ferridriver::options::UnrouteBehavior::Wait),
312 "ignoreErrors" => Ok(ferridriver::options::UnrouteBehavior::IgnoreErrors),
313 other => Err(rquickjs::Error::new_from_js_message(
314 "unrouteAll options",
315 "behavior",
316 format!("invalid behavior {other:?} (expected 'wait', 'ignoreErrors', or 'default')"),
317 )),
318 }
319}
320
321pub(crate) fn parse_route_times(
325 options: &rquickjs::function::Opt<rquickjs::Value<'_>>,
326) -> rquickjs::Result<Option<u32>> {
327 let Some(v) = options.0.as_ref() else { return Ok(None) };
328 if v.is_undefined() || v.is_null() {
329 return Ok(None);
330 }
331 let Some(obj) = v.as_object() else { return Ok(None) };
332 let t: rquickjs::Value<'_> = obj.get("times")?;
333 if t.is_undefined() || t.is_null() {
334 return Ok(None);
335 }
336 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
337 Ok(t.as_number().map(|n| if n < 0.0 { 0 } else { n as u32 }))
338}
339
340pub(crate) fn parse_har_options(
343 options: &rquickjs::function::Opt<rquickjs::Value<'_>>,
344) -> rquickjs::Result<ferridriver::har::RouteFromHarOptions> {
345 let mut out = ferridriver::har::RouteFromHarOptions::default();
346 let Some(v) = options.0.as_ref() else { return Ok(out) };
347 let Some(obj) = v.as_object() else { return Ok(out) };
348 let url: rquickjs::Value<'_> = obj.get("url")?;
349 if let Some(s) = url.as_string() {
350 let glob = s.to_string()?;
351 out.url = Some(
352 ferridriver::url_matcher::UrlMatcher::glob(glob)
353 .map_err(|e| rquickjs::Error::new_from_js_message("routeFromHAR", "url", format!("invalid url glob: {e}")))?,
354 );
355 }
356 let nf: rquickjs::Value<'_> = obj.get("notFound")?;
357 if let Some(s) = nf.as_string() {
358 match s.to_string()?.as_str() {
359 "fallback" => out.not_found = ferridriver::har::HarNotFound::Fallback,
360 "abort" => out.not_found = ferridriver::har::HarNotFound::Abort,
361 other => {
362 return Err(rquickjs::Error::new_from_js_message(
363 "routeFromHAR",
364 "notFound",
365 format!("invalid notFound {other:?} (expected 'abort' or 'fallback')"),
366 ));
367 },
368 }
369 }
370 Ok(out)
371}
372
373#[derive(JsLifetime, Trace)]
378#[rquickjs::class(rename = "Page")]
379pub struct PageJs {
380 #[qjs(skip_trace)]
384 inner: Arc<Page>,
385 #[qjs(skip_trace)]
391 async_ctx: Option<rquickjs::AsyncContext>,
392 #[qjs(skip_trace)]
397 next_route_id: Arc<AtomicU64>,
398 #[qjs(skip_trace)]
405 route_matchers: Arc<std::sync::Mutex<rustc_hash::FxHashMap<u64, ferridriver::url_matcher::UrlMatcher>>>,
406 #[qjs(skip_trace)]
410 locator_handler_ids: Arc<std::sync::Mutex<rustc_hash::FxHashMap<String, Vec<u64>>>>,
411}
412
413impl PageJs {
414 #[must_use]
415 pub fn new(inner: Arc<Page>) -> Self {
416 Self {
417 inner,
418 async_ctx: None,
419 next_route_id: Arc::new(AtomicU64::new(0)),
420 route_matchers: Arc::new(std::sync::Mutex::new(rustc_hash::FxHashMap::default())),
421 locator_handler_ids: Arc::new(std::sync::Mutex::new(rustc_hash::FxHashMap::default())),
422 }
423 }
424
425 #[must_use]
426 pub fn new_with_async_ctx(inner: Arc<Page>, async_ctx: rquickjs::AsyncContext) -> Self {
427 Self {
428 inner,
429 async_ctx: Some(async_ctx),
430 next_route_id: Arc::new(AtomicU64::new(0)),
431 route_matchers: Arc::new(std::sync::Mutex::new(rustc_hash::FxHashMap::default())),
432 locator_handler_ids: Arc::new(std::sync::Mutex::new(rustc_hash::FxHashMap::default())),
433 }
434 }
435
436 #[must_use]
439 pub fn page_arc(&self) -> Arc<Page> {
440 self.inner.clone()
441 }
442
443 #[must_use]
444 pub fn page(&self) -> &Arc<Page> {
445 &self.inner
446 }
447}
448
449pub(crate) fn pagejs_for_ctx(ctx: &rquickjs::Ctx<'_>, page: Arc<Page>) -> PageJs {
455 match ctx.userdata::<crate::engine::SessionAsyncCtx>() {
456 Some(ud) => PageJs::new_with_async_ctx(page, ud.0.clone()),
457 None => PageJs::new(page),
458 }
459}
460
461#[rquickjs::methods]
462impl PageJs {
463 #[qjs(rename = "goto")]
468 pub async fn goto<'js>(
469 &self,
470 ctx: rquickjs::Ctx<'js>,
471 url: String,
472 options: Opt<rquickjs::Value<'js>>,
473 ) -> rquickjs::Result<Option<crate::bindings::network::ResponseJs>> {
474 let opts = parse_goto_options(&ctx, options)?;
475 let resp = self.inner.goto(&url, opts).await.into_js()?;
476 Ok(resp.map(|r| crate::bindings::network::ResponseJs::new_with_page(r, self.inner.clone())))
477 }
478
479 #[qjs(rename = "reload")]
481 pub async fn reload<'js>(
482 &self,
483 ctx: rquickjs::Ctx<'js>,
484 options: Opt<rquickjs::Value<'js>>,
485 ) -> rquickjs::Result<Option<crate::bindings::network::ResponseJs>> {
486 let opts = parse_goto_options(&ctx, options)?;
487 let resp = self.inner.reload(opts).await.into_js()?;
488 Ok(resp.map(|r| crate::bindings::network::ResponseJs::new_with_page(r, self.inner.clone())))
489 }
490
491 #[qjs(rename = "goBack")]
493 pub async fn go_back<'js>(
494 &self,
495 ctx: rquickjs::Ctx<'js>,
496 options: Opt<rquickjs::Value<'js>>,
497 ) -> rquickjs::Result<Option<crate::bindings::network::ResponseJs>> {
498 let opts = parse_goto_options(&ctx, options)?;
499 let resp = self.inner.go_back(opts).await.into_js()?;
500 Ok(resp.map(|r| crate::bindings::network::ResponseJs::new_with_page(r, self.inner.clone())))
501 }
502
503 #[qjs(rename = "goForward")]
505 pub async fn go_forward<'js>(
506 &self,
507 ctx: rquickjs::Ctx<'js>,
508 options: Opt<rquickjs::Value<'js>>,
509 ) -> rquickjs::Result<Option<crate::bindings::network::ResponseJs>> {
510 let opts = parse_goto_options(&ctx, options)?;
511 let resp = self.inner.go_forward(opts).await.into_js()?;
512 Ok(resp.map(|r| crate::bindings::network::ResponseJs::new_with_page(r, self.inner.clone())))
513 }
514
515 #[qjs(rename = "url")]
518 pub fn url(&self) -> String {
519 self.inner.url()
520 }
521
522 #[qjs(rename = "title")]
524 pub async fn title(&self) -> rquickjs::Result<String> {
525 self.inner.title().await.into_js()
526 }
527
528 #[qjs(rename = "video")]
533 pub fn video<'js>(&self, ctx: rquickjs::Ctx<'js>) -> rquickjs::Result<rquickjs::Value<'js>> {
534 use rquickjs::class::Class;
535 match self.inner.video() {
536 Some(video) => {
537 let wrapper = crate::bindings::video::VideoJs::new(video);
538 let instance = Class::instance(ctx.clone(), wrapper)?;
539 rquickjs::IntoJs::into_js(instance, &ctx)
540 },
541 None => Ok(rquickjs::Value::new_null(ctx)),
542 }
543 }
544
545 #[qjs(rename = "content")]
547 pub async fn content(&self) -> rquickjs::Result<String> {
548 self.inner.content().await.into_js()
549 }
550
551 #[qjs(rename = "setContent")]
553 pub async fn set_content(&self, html: String) -> rquickjs::Result<()> {
554 self.inner.set_content(&html).await.into_js()
555 }
556
557 #[qjs(rename = "addInitScript")]
565 pub async fn add_init_script<'js>(
566 &self,
567 ctx: rquickjs::Ctx<'js>,
568 script: rquickjs::Value<'js>,
569 arg: Opt<rquickjs::Value<'js>>,
570 ) -> rquickjs::Result<rquickjs::Value<'js>> {
571 let (init, arg_json) = init_script_from_js(&ctx, script, arg.0)?;
572 let disposable = self.inner.add_init_script(init, arg_json).await.into_js()?;
573 let instance =
574 rquickjs::class::Class::instance(ctx.clone(), crate::bindings::disposable::DisposableJs::new(disposable))?;
575 rquickjs::IntoJs::into_js(instance, &ctx)
576 }
577
578 #[qjs(rename = "removeInitScript")]
580 pub async fn remove_init_script(&self, identifier: String) -> rquickjs::Result<()> {
581 self.inner.remove_init_script(&identifier).await.into_js()
582 }
583
584 #[qjs(rename = "markdown")]
587 pub async fn markdown(&self) -> rquickjs::Result<String> {
588 self.inner.markdown().await.into_js()
589 }
590
591 #[qjs(rename = "waitForSelector")]
595 pub async fn wait_for_selector<'js>(
596 &self,
597 ctx: rquickjs::Ctx<'js>,
598 selector: String,
599 options: Opt<rquickjs::Value<'js>>,
600 ) -> rquickjs::Result<()> {
601 let opts = parse_wait_options(&ctx, options)?;
602 self.inner.wait_for_selector(&selector, opts).await.into_js()
603 }
604
605 #[qjs(rename = "querySelector")]
613 pub async fn query_selector(
614 &self,
615 selector: String,
616 ) -> rquickjs::Result<Option<crate::bindings::element_handle::ElementHandleJs>> {
617 let inner = self.inner.query_selector(&selector).await.into_js()?;
618 Ok(inner.map(crate::bindings::element_handle::ElementHandleJs::new))
619 }
620
621 #[qjs(rename = "$")]
623 pub async fn dollar(
624 &self,
625 selector: String,
626 ) -> rquickjs::Result<Option<crate::bindings::element_handle::ElementHandleJs>> {
627 self.query_selector(selector).await
628 }
629
630 #[qjs(rename = "querySelectorAll")]
632 pub async fn query_selector_all(
633 &self,
634 selector: String,
635 ) -> rquickjs::Result<Vec<crate::bindings::element_handle::ElementHandleJs>> {
636 let inner_handles = self.inner.query_selector_all(&selector).await.into_js()?;
637 Ok(
638 inner_handles
639 .into_iter()
640 .map(crate::bindings::element_handle::ElementHandleJs::new)
641 .collect(),
642 )
643 }
644
645 #[qjs(rename = "$$")]
647 pub async fn dollar_dollar(
648 &self,
649 selector: String,
650 ) -> rquickjs::Result<Vec<crate::bindings::element_handle::ElementHandleJs>> {
651 self.query_selector_all(selector).await
652 }
653
654 #[qjs(rename = "evaluate")]
660 pub async fn evaluate<'js>(
661 &self,
662 ctx: rquickjs::Ctx<'js>,
663 page_function: rquickjs::Value<'js>,
664 arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
665 ) -> rquickjs::Result<rquickjs::Value<'js>> {
666 let (source, is_fn) = extract_page_function(&ctx, page_function)?;
667 let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
668 let result = self.inner.evaluate(&source, serialized, is_fn).await.into_js()?;
669 serialized_value_to_quickjs(&ctx, &result)
670 }
671
672 #[qjs(rename = "evaluateHandle")]
674 pub async fn evaluate_handle<'js>(
675 &self,
676 ctx: rquickjs::Ctx<'js>,
677 page_function: rquickjs::Value<'js>,
678 arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
679 ) -> rquickjs::Result<crate::bindings::js_handle::JSHandleJs> {
680 let (source, is_fn) = extract_page_function(&ctx, page_function)?;
681 let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
682 let handle = self.inner.evaluate_handle(&source, serialized, is_fn).await.into_js()?;
683 Ok(crate::bindings::js_handle::JSHandleJs::new(handle))
684 }
685
686 #[qjs(rename = "locator")]
689 pub fn locator<'js>(
690 &self,
691 ctx: rquickjs::Ctx<'js>,
692 selector: String,
693 options: Opt<rquickjs::Value<'js>>,
694 ) -> rquickjs::Result<LocatorJs> {
695 let parsed = crate::bindings::locator::parse_locator_options_public(&ctx, options, true)?;
696 let opts = ferridriver::options::FilterOptions {
697 has_text: parsed.has_text,
698 has_not_text: parsed.has_not_text,
699 has: parsed.has,
700 has_not: parsed.has_not,
701 visible: parsed.visible,
702 };
703 let filter = if crate::bindings::locator::is_empty_filter(&opts) {
704 None
705 } else {
706 Some(opts)
707 };
708 Ok(LocatorJs::new(self.inner.locator(&selector, filter)))
709 }
710
711 #[qjs(rename = "getByRole")]
715 pub fn get_by_role(
716 &self,
717 role: String,
718 options: rquickjs::function::Opt<rquickjs::Value<'_>>,
719 ) -> rquickjs::Result<LocatorJs> {
720 let opts = parse_role_options(options)?;
721 Ok(LocatorJs::new(self.inner.get_by_role(&role, &opts)))
722 }
723
724 #[qjs(rename = "getByText")]
726 pub fn get_by_text(
727 &self,
728 text: rquickjs::Value<'_>,
729 options: rquickjs::function::Opt<rquickjs::Value<'_>>,
730 ) -> rquickjs::Result<LocatorJs> {
731 let t = string_or_regex_from_js(text)?;
732 let opts = parse_text_options(options);
733 Ok(LocatorJs::new(self.inner.get_by_text(&t, &opts)))
734 }
735
736 #[qjs(rename = "getByLabel")]
738 pub fn get_by_label(
739 &self,
740 text: rquickjs::Value<'_>,
741 options: rquickjs::function::Opt<rquickjs::Value<'_>>,
742 ) -> rquickjs::Result<LocatorJs> {
743 let t = string_or_regex_from_js(text)?;
744 let opts = parse_text_options(options);
745 Ok(LocatorJs::new(self.inner.get_by_label(&t, &opts)))
746 }
747
748 #[qjs(rename = "getByPlaceholder")]
750 pub fn get_by_placeholder(
751 &self,
752 text: rquickjs::Value<'_>,
753 options: rquickjs::function::Opt<rquickjs::Value<'_>>,
754 ) -> rquickjs::Result<LocatorJs> {
755 let t = string_or_regex_from_js(text)?;
756 let opts = parse_text_options(options);
757 Ok(LocatorJs::new(self.inner.get_by_placeholder(&t, &opts)))
758 }
759
760 #[qjs(rename = "getByAltText")]
762 pub fn get_by_alt_text(
763 &self,
764 text: rquickjs::Value<'_>,
765 options: rquickjs::function::Opt<rquickjs::Value<'_>>,
766 ) -> rquickjs::Result<LocatorJs> {
767 let t = string_or_regex_from_js(text)?;
768 let opts = parse_text_options(options);
769 Ok(LocatorJs::new(self.inner.get_by_alt_text(&t, &opts)))
770 }
771
772 #[qjs(rename = "getByTitle")]
774 pub fn get_by_title(
775 &self,
776 text: rquickjs::Value<'_>,
777 options: rquickjs::function::Opt<rquickjs::Value<'_>>,
778 ) -> rquickjs::Result<LocatorJs> {
779 let t = string_or_regex_from_js(text)?;
780 let opts = parse_text_options(options);
781 Ok(LocatorJs::new(self.inner.get_by_title(&t, &opts)))
782 }
783
784 #[qjs(rename = "getByTestId")]
786 pub fn get_by_test_id(&self, test_id: rquickjs::Value<'_>) -> rquickjs::Result<LocatorJs> {
787 let t = string_or_regex_from_js(test_id)?;
788 Ok(LocatorJs::new(self.inner.get_by_test_id(&t)))
789 }
790
791 #[qjs(rename = "click")]
796 pub async fn click<'js>(
797 &self,
798 ctx: rquickjs::Ctx<'js>,
799 selector: String,
800 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
801 ) -> rquickjs::Result<()> {
802 let opts = crate::bindings::convert::parse_click_options(&ctx, options)?;
803 self.inner.click(&selector, opts).await.into_js()
804 }
805
806 #[qjs(rename = "dblclick")]
809 pub async fn dblclick<'js>(
810 &self,
811 ctx: rquickjs::Ctx<'js>,
812 selector: String,
813 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
814 ) -> rquickjs::Result<()> {
815 let opts = crate::bindings::convert::parse_dblclick_options(&ctx, options)?;
816 self.inner.dblclick(&selector, opts).await.into_js()
817 }
818
819 #[qjs(rename = "fill")]
822 pub async fn fill<'js>(
823 &self,
824 ctx: rquickjs::Ctx<'js>,
825 selector: String,
826 value: String,
827 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
828 ) -> rquickjs::Result<()> {
829 let opts = crate::bindings::convert::parse_fill_options(&ctx, options)?;
830 self.inner.fill(&selector, &value, opts).await.into_js()
831 }
832
833 #[qjs(rename = "type")]
839 pub async fn type_<'js>(
840 &self,
841 ctx: rquickjs::Ctx<'js>,
842 selector: String,
843 text: String,
844 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
845 ) -> rquickjs::Result<()> {
846 let opts = crate::bindings::convert::parse_type_options(&ctx, options)?;
847 self.inner.r#type(&selector, &text, opts).await.into_js()
848 }
849
850 #[qjs(rename = "press")]
853 pub async fn press<'js>(
854 &self,
855 ctx: rquickjs::Ctx<'js>,
856 selector: String,
857 key: String,
858 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
859 ) -> rquickjs::Result<()> {
860 let opts = crate::bindings::convert::parse_press_options(&ctx, options)?;
861 self.inner.press(&selector, &key, opts).await.into_js()
862 }
863
864 #[qjs(rename = "focus")]
866 pub async fn focus(
867 &self,
868 selector: String,
869 _options: rquickjs::function::Opt<rquickjs::Value<'_>>,
870 ) -> rquickjs::Result<()> {
871 self.inner.focus(&selector).await.into_js()
872 }
873
874 #[qjs(rename = "hover")]
877 pub async fn hover<'js>(
878 &self,
879 ctx: rquickjs::Ctx<'js>,
880 selector: String,
881 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
882 ) -> rquickjs::Result<()> {
883 let opts = crate::bindings::convert::parse_hover_options(&ctx, options)?;
884 self.inner.hover(&selector, opts).await.into_js()
885 }
886
887 #[qjs(rename = "dispatchEvent")]
890 pub async fn dispatch_event<'js>(
891 &self,
892 ctx: rquickjs::Ctx<'js>,
893 selector: String,
894 event_type: String,
895 event_init: rquickjs::function::Opt<rquickjs::Value<'js>>,
896 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
897 ) -> rquickjs::Result<()> {
898 let init_json = match event_init.0 {
899 Some(v) if !v.is_undefined() && !v.is_null() => {
900 Some(crate::bindings::convert::serde_from_js::<serde_json::Value>(&ctx, v)?)
901 },
902 _ => None,
903 };
904 let opts = crate::bindings::convert::parse_dispatch_event_options(&ctx, options)?;
905 self
906 .inner
907 .dispatch_event(&selector, &event_type, init_json, opts)
908 .await
909 .into_js()
910 }
911
912 #[qjs(rename = "tap")]
915 pub async fn tap<'js>(
916 &self,
917 ctx: rquickjs::Ctx<'js>,
918 selector: String,
919 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
920 ) -> rquickjs::Result<()> {
921 let opts = crate::bindings::convert::parse_tap_options(&ctx, options)?;
922 self.inner.tap(&selector, opts).await.into_js()
923 }
924
925 #[qjs(rename = "check")]
928 pub async fn check<'js>(
929 &self,
930 ctx: rquickjs::Ctx<'js>,
931 selector: String,
932 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
933 ) -> rquickjs::Result<()> {
934 let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
935 self.inner.check(&selector, opts).await.into_js()
936 }
937
938 #[qjs(rename = "uncheck")]
941 pub async fn uncheck<'js>(
942 &self,
943 ctx: rquickjs::Ctx<'js>,
944 selector: String,
945 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
946 ) -> rquickjs::Result<()> {
947 let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
948 self.inner.uncheck(&selector, opts).await.into_js()
949 }
950
951 #[qjs(rename = "setChecked")]
954 pub async fn set_checked<'js>(
955 &self,
956 ctx: rquickjs::Ctx<'js>,
957 selector: String,
958 checked: bool,
959 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
960 ) -> rquickjs::Result<()> {
961 let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
962 self.inner.set_checked(&selector, checked, opts).await.into_js()
963 }
964
965 #[qjs(rename = "selectOption")]
969 pub async fn select_option<'js>(
970 &self,
971 ctx: rquickjs::Ctx<'js>,
972 selector: String,
973 values: rquickjs::Value<'js>,
974 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
975 ) -> rquickjs::Result<Vec<String>> {
976 let values = crate::bindings::convert::parse_select_option_values(&ctx, values)?;
977 let opts = crate::bindings::convert::parse_select_option_options(&ctx, options)?;
978 self.inner.select_option(&selector, values, opts).await.into_js()
979 }
980
981 #[qjs(rename = "textContent")]
985 pub async fn text_content(&self, selector: String) -> rquickjs::Result<Option<String>> {
986 self.inner.text_content(&selector).await.into_js()
987 }
988
989 #[qjs(rename = "innerText")]
991 pub async fn inner_text(&self, selector: String) -> rquickjs::Result<String> {
992 self.inner.inner_text(&selector).await.into_js()
993 }
994
995 #[qjs(rename = "innerHTML")]
997 pub async fn inner_html(&self, selector: String) -> rquickjs::Result<String> {
998 self.inner.inner_html(&selector).await.into_js()
999 }
1000
1001 #[qjs(rename = "inputValue")]
1003 pub async fn input_value(&self, selector: String) -> rquickjs::Result<String> {
1004 self.inner.input_value(&selector).await.into_js()
1005 }
1006
1007 #[qjs(rename = "getAttribute")]
1010 pub async fn get_attribute(&self, selector: String, name: String) -> rquickjs::Result<Option<String>> {
1011 self.inner.get_attribute(&selector, &name).await.into_js()
1012 }
1013
1014 #[qjs(rename = "isVisible")]
1016 pub async fn is_visible(&self, selector: String) -> rquickjs::Result<bool> {
1017 self.inner.is_visible(&selector).await.into_js()
1018 }
1019
1020 #[qjs(rename = "isHidden")]
1022 pub async fn is_hidden(&self, selector: String) -> rquickjs::Result<bool> {
1023 self.inner.is_hidden(&selector).await.into_js()
1024 }
1025
1026 #[qjs(rename = "isEnabled")]
1028 pub async fn is_enabled(&self, selector: String) -> rquickjs::Result<bool> {
1029 self.inner.is_enabled(&selector).await.into_js()
1030 }
1031
1032 #[qjs(rename = "isDisabled")]
1034 pub async fn is_disabled(&self, selector: String) -> rquickjs::Result<bool> {
1035 self.inner.is_disabled(&selector).await.into_js()
1036 }
1037
1038 #[qjs(rename = "isChecked")]
1040 pub async fn is_checked(&self, selector: String) -> rquickjs::Result<bool> {
1041 self.inner.is_checked(&selector).await.into_js()
1042 }
1043
1044 #[qjs(get, rename = "mouse")]
1049 pub fn mouse(&self) -> MouseJs {
1050 MouseJs::new(self.inner.clone())
1051 }
1052
1053 #[qjs(get, rename = "keyboard")]
1056 pub fn keyboard(&self) -> KeyboardJs {
1057 KeyboardJs::new(self.inner.clone())
1058 }
1059
1060 #[qjs(rename = "clickAt")]
1063 pub async fn click_at(&self, x: f64, y: f64) -> rquickjs::Result<()> {
1064 self.inner.click_at(x, y).await.into_js()
1065 }
1066
1067 #[qjs(rename = "moveMouseSmooth")]
1071 pub async fn move_mouse_smooth(
1072 &self,
1073 from_x: f64,
1074 from_y: f64,
1075 to_x: f64,
1076 to_y: f64,
1077 steps: u32,
1078 ) -> rquickjs::Result<()> {
1079 self
1080 .inner
1081 .move_mouse_smooth(from_x, from_y, to_x, to_y, steps)
1082 .await
1083 .into_js()
1084 }
1085
1086 #[qjs(rename = "dragAndDrop")]
1090 pub async fn drag_and_drop<'js>(
1091 &self,
1092 ctx: rquickjs::Ctx<'js>,
1093 source: String,
1094 target: String,
1095 options: Opt<rquickjs::Value<'js>>,
1096 ) -> rquickjs::Result<()> {
1097 let opts = parse_drag_options(&ctx, options)?;
1098 self.inner.drag_and_drop(&source, &target, opts).await.into_js()
1099 }
1100
1101 #[qjs(rename = "setInputFiles")]
1107 pub async fn set_input_files<'js>(
1108 &self,
1109 ctx: rquickjs::Ctx<'js>,
1110 selector: String,
1111 files: rquickjs::Value<'js>,
1112 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
1113 ) -> rquickjs::Result<()> {
1114 let files = crate::bindings::convert::parse_input_files(&ctx, files)?;
1115 let opts = crate::bindings::convert::parse_set_input_files_options(&ctx, options)?;
1116 self.inner.set_input_files(&selector, files, opts).await.into_js()
1117 }
1118
1119 #[qjs(rename = "setViewportSize")]
1126 pub async fn set_viewport_size<'js>(
1127 &self,
1128 ctx: rquickjs::Ctx<'js>,
1129 size: rquickjs::Value<'js>,
1130 ) -> rquickjs::Result<()> {
1131 #[derive(serde::Deserialize)]
1132 struct Size {
1133 width: i64,
1134 height: i64,
1135 }
1136 let s: Size = crate::bindings::convert::serde_from_js(&ctx, size)?;
1137 self.inner.set_viewport_size(s.width, s.height).await.into_js()
1138 }
1139
1140 #[qjs(rename = "emulateMedia")]
1145 pub async fn emulate_media<'js>(
1146 &self,
1147 ctx: rquickjs::Ctx<'js>,
1148 options: Opt<rquickjs::Value<'js>>,
1149 ) -> rquickjs::Result<()> {
1150 let opts = parse_emulate_media_options(&ctx, options)?;
1151 self.inner.emulate_media(&opts).await.into_js()
1152 }
1153
1154 #[qjs(rename = "screenshot")]
1160 pub async fn screenshot<'js>(
1161 &self,
1162 ctx: rquickjs::Ctx<'js>,
1163 options: Opt<rquickjs::Value<'js>>,
1164 ) -> rquickjs::Result<Vec<u8>> {
1165 let opts = parse_screenshot_options(&ctx, options)?;
1166 self.inner.screenshot(opts).await.into_js()
1167 }
1168
1169 #[qjs(rename = "screenshotElement")]
1171 pub async fn screenshot_element(&self, selector: String) -> rquickjs::Result<Vec<u8>> {
1172 self.inner.screenshot_element(&selector).await.into_js()
1173 }
1174
1175 #[qjs(rename = "pdf")]
1179 pub async fn pdf<'js>(
1180 &self,
1181 ctx: rquickjs::Ctx<'js>,
1182 options: Opt<rquickjs::Value<'js>>,
1183 ) -> rquickjs::Result<Vec<u8>> {
1184 let opts = parse_pdf_options(&ctx, options)?;
1185 self.inner.pdf(opts).await.into_js()
1186 }
1187
1188 #[qjs(rename = "close")]
1193 pub async fn close<'js>(&self, ctx: rquickjs::Ctx<'js>, options: Opt<rquickjs::Value<'js>>) -> rquickjs::Result<()> {
1194 let opts = parse_page_close_options(&ctx, options)?;
1195 self.inner.close(opts).await.into_js()
1196 }
1197
1198 #[qjs(rename = "setDefaultTimeout")]
1201 pub fn set_default_timeout(&self, ms: u64) {
1202 self.inner.set_default_timeout(ms);
1203 }
1204
1205 #[qjs(rename = "setDefaultNavigationTimeout")]
1209 pub fn set_default_navigation_timeout(&self, ms: u64) {
1210 self.inner.set_default_navigation_timeout(ms);
1211 }
1212
1213 #[qjs(rename = "isClosed")]
1215 pub fn is_closed(&self) -> bool {
1216 self.inner.is_closed()
1217 }
1218
1219 #[qjs(rename = "route")]
1237 pub async fn route<'js>(
1238 &self,
1239 ctx: rquickjs::Ctx<'js>,
1240 url: rquickjs::Value<'js>,
1241 handler: rquickjs::Function<'js>,
1242 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
1243 ) -> rquickjs::Result<rquickjs::Value<'js>> {
1244 let times = parse_route_times(&options)?;
1245 let async_ctx = self.async_ctx.clone().ok_or_else(|| {
1246 rquickjs::Error::new_from_js_message(
1247 "page.route",
1248 "Error",
1249 "page.route requires the script engine's AsyncContext (install_page)".to_string(),
1250 )
1251 })?;
1252 let id = self.next_route_id.fetch_add(1, Ordering::Relaxed);
1253 let saved_handler = rquickjs::Persistent::save(&ctx, handler);
1254 with_page_callbacks(&ctx, |r| r.route_handlers.insert(id, saved_handler))?;
1255
1256 let has_predicate = url.as_function().is_some();
1262 let matcher = if let Some(pred) = url.as_function() {
1263 let saved_pred = rquickjs::Persistent::save(&ctx, pred.clone());
1264 with_page_callbacks(&ctx, |r| r.route_preds.insert(id, saved_pred))?;
1265 let m = ferridriver::url_matcher::UrlMatcher::predicate(|_| true);
1266 self
1267 .route_matchers
1268 .lock()
1269 .unwrap_or_else(std::sync::PoisonError::into_inner)
1270 .insert(id, m.clone());
1271 m
1272 } else {
1273 url_value_to_matcher(&ctx, url)?
1274 };
1275
1276 let rust_handler: ferridriver::route::RouteHandler = std::sync::Arc::new(move |route| {
1287 let async_ctx = async_ctx.clone();
1288 tokio::spawn(async move {
1294 use rquickjs::class::Class;
1295 let _: rquickjs::Result<()> = rquickjs::async_with!(async_ctx => |ctx| {
1296 if has_predicate {
1297 let pred = with_page_callbacks(&ctx, |r| r.route_preds.get(&id).cloned())?
1298 .ok_or_else(|| rquickjs::Error::new_from_js_message("page.route", "Error", "route predicate gone".to_string()))?
1299 .restore(&ctx)?;
1300 let url_ctor: rquickjs::function::Constructor<'_> = ctx.globals().get("URL")?;
1301 let url_obj: rquickjs::Value<'_> = url_ctor.construct((route.request().url.clone(),))?;
1302 if !call_predicate_truthy(&pred, url_obj, &ctx).await? {
1303 route.continue_route(ferridriver::route::ContinueOverrides::default());
1304 return Ok(());
1305 }
1306 }
1307 let f = with_page_callbacks(&ctx, |r| r.route_handlers.get(&id).cloned())?
1308 .ok_or_else(|| rquickjs::Error::new_from_js_message("page.route", "Error", "route handler gone".to_string()))?
1309 .restore(&ctx)?;
1310 let route_class = Class::instance(ctx.clone(), crate::bindings::network::RouteJs::new(route))?;
1311 let _: rquickjs::Value<'_> = f.call((route_class,))?;
1312 Ok(())
1313 })
1314 .await;
1315 });
1316 });
1317
1318 let disposable = self.inner.route(matcher, rust_handler, times).await.into_js()?;
1319 let instance =
1320 rquickjs::class::Class::instance(ctx.clone(), crate::bindings::disposable::DisposableJs::new(disposable))?;
1321 rquickjs::IntoJs::into_js(instance, &ctx)
1322 }
1323
1324 #[qjs(rename = "routeFromHAR")]
1326 pub async fn route_from_har(
1327 &self,
1328 har: String,
1329 options: rquickjs::function::Opt<rquickjs::Value<'_>>,
1330 ) -> rquickjs::Result<()> {
1331 let opts = parse_har_options(&options)?;
1332 self
1333 .inner
1334 .route_from_har(std::path::Path::new(&har), opts)
1335 .await
1336 .into_js()
1337 }
1338
1339 #[qjs(rename = "unroute")]
1344 pub async fn unroute<'js>(&self, ctx: rquickjs::Ctx<'js>, url: rquickjs::Value<'js>) -> rquickjs::Result<()> {
1345 if let Some(pred) = url.as_function() {
1346 let saved: Vec<(u64, rquickjs::Persistent<rquickjs::Function<'static>>)> =
1352 with_page_callbacks(&ctx, |r| r.route_preds.iter().map(|(k, v)| (*k, v.clone())).collect())?;
1353 let mut victims: Vec<u64> = Vec::new();
1354 for (id, sp) in saved {
1355 let stored = sp.restore(&ctx)?;
1356 if stored.as_value() == pred.as_value() {
1357 victims.push(id);
1358 }
1359 }
1360 for id in victims {
1361 let m = self
1362 .route_matchers
1363 .lock()
1364 .unwrap_or_else(std::sync::PoisonError::into_inner)
1365 .remove(&id);
1366 if let Some(m) = m {
1367 self.inner.unroute(&m).await.into_js()?;
1368 }
1369 with_page_callbacks(&ctx, |r| {
1370 r.route_preds.remove(&id);
1371 r.route_handlers.remove(&id);
1372 })?;
1373 }
1374 return Ok(());
1375 }
1376 let matcher = url_value_to_matcher(&ctx, url)?;
1377 self.inner.unroute(&matcher).await.into_js()
1378 }
1379
1380 #[qjs(rename = "unrouteAll")]
1384 pub async fn unroute_all<'js>(
1385 &self,
1386 ctx: rquickjs::Ctx<'js>,
1387 options: Opt<rquickjs::Value<'js>>,
1388 ) -> rquickjs::Result<()> {
1389 let behavior = match options.0.and_then(rquickjs::Value::into_object) {
1390 Some(obj) => match obj.get::<_, Option<String>>("behavior")? {
1391 Some(b) => Some(parse_unroute_behavior(&b)?),
1392 None => None,
1393 },
1394 None => None,
1395 };
1396 self.inner.unroute_all(behavior).await.into_js()?;
1397 self
1398 .route_matchers
1399 .lock()
1400 .unwrap_or_else(std::sync::PoisonError::into_inner)
1401 .clear();
1402 with_page_callbacks(&ctx, |r| {
1403 r.route_preds.clear();
1404 r.route_handlers.clear();
1405 })?;
1406 Ok(())
1407 }
1408
1409 #[qjs(rename = "addLocatorHandler")]
1419 pub fn add_locator_handler(
1420 &self,
1421 _locator: rquickjs::Class<'_, LocatorJs>,
1422 _handler: rquickjs::Function<'_>,
1423 _options: Opt<rquickjs::Value<'_>>,
1424 ) -> rquickjs::Result<()> {
1425 ferridriver::error::Result::<()>::Err(ferridriver::error::FerriError::unsupported(
1433 "page.addLocatorHandler is not available in the QuickJS scripting engine \
1434 (handlers cannot fire during an in-VM action without deadlocking the \
1435 single-threaded VM); use the NAPI/core API for locator handlers",
1436 ))
1437 .into_js()
1438 }
1439
1440 #[qjs(rename = "removeLocatorHandler")]
1444 pub fn remove_locator_handler<'js>(
1445 &self,
1446 ctx: rquickjs::Ctx<'js>,
1447 locator: rquickjs::Class<'js, LocatorJs>,
1448 ) -> rquickjs::Result<()> {
1449 let core_locator = locator.borrow().inner_ref().clone();
1450 self.inner.remove_locator_handler(&core_locator);
1451 let ids = self
1452 .locator_handler_ids
1453 .lock()
1454 .unwrap_or_else(std::sync::PoisonError::into_inner)
1455 .remove(core_locator.selector())
1456 .unwrap_or_default();
1457 with_page_callbacks(&ctx, |r| {
1458 for id in ids {
1459 r.remove_locator_handler(id);
1460 }
1461 })?;
1462 Ok(())
1463 }
1464
1465 #[qjs(rename = "pickLocator")]
1468 pub async fn pick_locator(&self) -> rquickjs::Result<LocatorJs> {
1469 let loc = self.inner.pick_locator().await.into_js()?;
1470 Ok(LocatorJs::new(loc))
1471 }
1472
1473 #[qjs(rename = "cancelPickLocator")]
1475 pub async fn cancel_pick_locator(&self) -> rquickjs::Result<()> {
1476 self.inner.cancel_pick_locator().await.into_js()
1477 }
1478
1479 #[qjs(rename = "hideHighlight")]
1481 pub async fn hide_highlight(&self) -> rquickjs::Result<()> {
1482 self.inner.hide_highlight().await.into_js()
1483 }
1484
1485 #[qjs(rename = "waitForRequest")]
1495 pub async fn wait_for_request<'js>(
1496 &self,
1497 ctx: rquickjs::Ctx<'js>,
1498 url: rquickjs::Value<'js>,
1499 timeout_ms: Opt<f64>,
1500 ) -> rquickjs::Result<crate::bindings::network::RequestJs> {
1501 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1502 let timeout = timeout_ms.0.map(|t| t as u64);
1503 if let Some(pred) = url.as_function() {
1504 let t = timeout.unwrap_or_else(|| self.inner.default_timeout());
1505 return wait_request_predicate(ctx.clone(), self.inner.clone(), pred.clone(), t).await;
1506 }
1507 let matcher = url_value_to_matcher(&ctx, url)?;
1508 let req = self.inner.wait_for_request(matcher, timeout).await.into_js()?;
1509 Ok(crate::bindings::network::RequestJs::new_with_page(
1510 req,
1511 self.inner.clone(),
1512 ))
1513 }
1514
1515 #[qjs(rename = "waitForResponse")]
1518 pub async fn wait_for_response<'js>(
1519 &self,
1520 ctx: rquickjs::Ctx<'js>,
1521 url: rquickjs::Value<'js>,
1522 timeout_ms: Opt<f64>,
1523 ) -> rquickjs::Result<crate::bindings::network::ResponseJs> {
1524 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1525 let timeout = timeout_ms.0.map(|t| t as u64);
1526 if let Some(pred) = url.as_function() {
1527 let t = timeout.unwrap_or_else(|| self.inner.default_timeout());
1528 return wait_response_predicate(ctx.clone(), self.inner.clone(), pred.clone(), t).await;
1529 }
1530 let matcher = url_value_to_matcher(&ctx, url)?;
1531 let resp = self.inner.wait_for_response(matcher, timeout).await.into_js()?;
1532 Ok(crate::bindings::network::ResponseJs::new_with_page(
1533 resp,
1534 self.inner.clone(),
1535 ))
1536 }
1537
1538 #[qjs(rename = "waitForLoadState")]
1548 pub async fn wait_for_load_state(&self, state: Opt<String>) -> rquickjs::Result<()> {
1549 use crate::bindings::convert::FerriResultExt;
1550 self.inner.wait_for_load_state(state.0.as_deref()).await.into_js()
1551 }
1552
1553 #[qjs(rename = "waitForURL")]
1559 pub async fn wait_for_url<'js>(&self, ctx: rquickjs::Ctx<'js>, url: rquickjs::Value<'js>) -> rquickjs::Result<()> {
1560 use crate::bindings::convert::FerriResultExt;
1561 let matcher = url_value_to_matcher(&ctx, url)?;
1562 self.inner.wait_for_url(matcher).await.into_js()
1563 }
1564
1565 #[qjs(rename = "waitForFunction")]
1571 pub async fn wait_for_function<'js>(
1572 &self,
1573 ctx: rquickjs::Ctx<'js>,
1574 page_function: rquickjs::Value<'js>,
1575 _arg: Opt<rquickjs::Value<'js>>,
1576 options: Opt<rquickjs::Value<'js>>,
1577 ) -> rquickjs::Result<rquickjs::Value<'js>> {
1578 #[derive(serde::Deserialize, Default)]
1579 #[serde(rename_all = "camelCase", default)]
1580 struct JsOpts {
1581 timeout: Option<u64>,
1582 }
1583 let opts: JsOpts = match options.0 {
1584 Some(v) if !v.is_undefined() && !v.is_null() => crate::bindings::convert::serde_from_js(&ctx, v)?,
1585 _ => JsOpts::default(),
1586 };
1587 let (src, is_fn) = crate::bindings::convert::extract_page_function(&ctx, page_function)?;
1588 let expr = if is_fn.unwrap_or(false) {
1592 format!("({src})()")
1593 } else {
1594 src
1595 };
1596 let v = self
1597 .inner
1598 .wait_for_function(&expr, opts.timeout)
1599 .await
1600 .map_err(|e| crate::bindings::convert::to_rq_error(&e))?;
1601 crate::bindings::convert::json_to_js(&ctx, &v)
1602 }
1603
1604 #[qjs(rename = "waitForEvent")]
1605 pub async fn wait_for_event<'js>(
1606 &self,
1607 ctx: rquickjs::Ctx<'js>,
1608 event: String,
1609 timeout_ms: Opt<f64>,
1610 ) -> rquickjs::Result<rquickjs::Value<'js>> {
1611 use rquickjs::class::Class;
1612 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1613 let timeout = timeout_ms.0.unwrap_or(30_000.0) as u64;
1614 let event_lc = event.to_ascii_lowercase();
1615
1616 if event_lc == "dialog" {
1621 let dialog = self
1622 .inner
1623 .wait_for_dialog(timeout)
1624 .await
1625 .map_err(|e| rquickjs::Error::new_from_js_message("Page.waitForEvent", "Error", e.to_string()))?;
1626 let wrapper = crate::bindings::dialog::DialogJs::new(dialog);
1627 let instance = Class::instance(ctx.clone(), wrapper)?;
1628 return rquickjs::IntoJs::into_js(instance, &ctx);
1629 }
1630 if event_lc == "filechooser" {
1634 let chooser = self
1635 .inner
1636 .wait_for_file_chooser(timeout)
1637 .await
1638 .map_err(|e| rquickjs::Error::new_from_js_message("Page.waitForEvent", "Error", e.to_string()))?;
1639 let wrapper = crate::bindings::file_chooser::FileChooserJs::new(chooser);
1640 let instance = Class::instance(ctx.clone(), wrapper)?;
1641 return rquickjs::IntoJs::into_js(instance, &ctx);
1642 }
1643 if event_lc == "download" {
1646 let download = self
1647 .inner
1648 .wait_for_download(timeout)
1649 .await
1650 .map_err(|e| rquickjs::Error::new_from_js_message("Page.waitForEvent", "Error", e.to_string()))?;
1651 let wrapper = crate::bindings::download::DownloadJs::new(download);
1652 let instance = Class::instance(ctx.clone(), wrapper)?;
1653 return rquickjs::IntoJs::into_js(instance, &ctx);
1654 }
1655
1656 let name = event_lc.clone();
1657 let ev = self
1658 .inner
1659 .events()
1660 .wait_for(move |e| match_event_name(&name, e), timeout)
1661 .await
1662 .map_err(|e| rquickjs::Error::new_from_js_message("Page.waitForEvent", "Error", e.to_string()))?;
1663 match ev {
1664 ferridriver::events::PageEvent::WebSocket(ws) => {
1665 let wrapper = crate::bindings::network::WebSocketJs::new(ws);
1666 let instance = Class::instance(ctx.clone(), wrapper)?;
1667 rquickjs::IntoJs::into_js(instance, &ctx)
1668 },
1669 ferridriver::events::PageEvent::Request(req)
1670 | ferridriver::events::PageEvent::RequestFinished(req)
1671 | ferridriver::events::PageEvent::RequestFailed(req) => {
1672 let wrapper = crate::bindings::network::RequestJs::new_with_page(req, self.inner.clone());
1673 let instance = Class::instance(ctx.clone(), wrapper)?;
1674 rquickjs::IntoJs::into_js(instance, &ctx)
1675 },
1676 ferridriver::events::PageEvent::Response(resp) => {
1677 let wrapper = crate::bindings::network::ResponseJs::new_with_page(resp, self.inner.clone());
1678 let instance = Class::instance(ctx.clone(), wrapper)?;
1679 rquickjs::IntoJs::into_js(instance, &ctx)
1680 },
1681 ferridriver::events::PageEvent::Dialog(dialog) => {
1682 let wrapper = crate::bindings::dialog::DialogJs::new(dialog);
1686 let instance = Class::instance(ctx.clone(), wrapper)?;
1687 rquickjs::IntoJs::into_js(instance, &ctx)
1688 },
1689 ferridriver::events::PageEvent::FileChooser(chooser) => {
1690 let wrapper = crate::bindings::file_chooser::FileChooserJs::new(chooser);
1691 let instance = Class::instance(ctx.clone(), wrapper)?;
1692 rquickjs::IntoJs::into_js(instance, &ctx)
1693 },
1694 ferridriver::events::PageEvent::Download(download) => {
1695 let wrapper = crate::bindings::download::DownloadJs::new(download);
1696 let instance = Class::instance(ctx.clone(), wrapper)?;
1697 rquickjs::IntoJs::into_js(instance, &ctx)
1698 },
1699 ferridriver::events::PageEvent::Console(msg) => {
1700 let wrapper = crate::bindings::console_message::ConsoleMessageJs::new(msg);
1701 let instance = Class::instance(ctx.clone(), wrapper)?;
1702 rquickjs::IntoJs::into_js(instance, &ctx)
1703 },
1704 ferridriver::events::PageEvent::PageError(err) => {
1708 crate::bindings::web_error::build_native_error(&ctx, err.error())
1709 },
1710 other => page_event_to_js(&ctx, &other),
1711 }
1712 }
1713
1714 #[qjs(rename = "mainFrame")]
1724 pub fn main_frame(&self) -> crate::bindings::frame::FrameJs {
1725 crate::bindings::frame::FrameJs::new(self.inner.main_frame())
1726 }
1727
1728 #[qjs(rename = "frames")]
1731 pub fn frames(&self) -> Vec<crate::bindings::frame::FrameJs> {
1732 self
1733 .inner
1734 .frames()
1735 .into_iter()
1736 .map(crate::bindings::frame::FrameJs::new)
1737 .collect()
1738 }
1739
1740 #[qjs(rename = "frameLocator")]
1744 pub fn frame_locator(&self, selector: String) -> crate::bindings::frame_locator::FrameLocatorJs {
1745 crate::bindings::frame_locator::FrameLocatorJs::new(self.inner.frame_locator(&selector))
1746 }
1747
1748 #[qjs(rename = "frame")]
1756 pub fn frame<'js>(
1757 &self,
1758 ctx: rquickjs::Ctx<'js>,
1759 selector: rquickjs::Value<'js>,
1760 ) -> rquickjs::Result<Option<crate::bindings::frame::FrameJs>> {
1761 let core_sel = if let Some(s) = selector.as_string() {
1762 ferridriver::options::FrameSelector::by_name(s.to_string()?)
1763 } else if let Some(obj) = selector.as_object() {
1764 let read = |key: &str| -> rquickjs::Result<Option<String>> {
1765 let v: rquickjs::Value<'_> = obj
1766 .get(key)
1767 .unwrap_or_else(|_| rquickjs::Value::new_undefined(ctx.clone()));
1768 if v.is_undefined() || v.is_null() {
1769 Ok(None)
1770 } else if let Some(s) = v.as_string() {
1771 Ok(Some(s.to_string()?))
1772 } else {
1773 Ok(None)
1774 }
1775 };
1776 ferridriver::options::FrameSelector {
1777 name: read("name")?,
1778 url: read("url")?,
1779 }
1780 } else {
1781 return Ok(None);
1782 };
1783
1784 if core_sel.is_empty() {
1785 return Ok(None);
1786 }
1787 Ok(self.inner.frame(core_sel).map(crate::bindings::frame::FrameJs::new))
1788 }
1789
1790 #[qjs(rename = "touchscreen", get)]
1792 pub fn touchscreen(&self) -> TouchscreenJs {
1793 TouchscreenJs {
1794 page: self.inner.clone(),
1795 }
1796 }
1797
1798 #[qjs(rename = "snapshotForAI")]
1803 pub async fn snapshot_for_ai<'js>(
1804 &self,
1805 ctx: rquickjs::Ctx<'js>,
1806 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
1807 ) -> rquickjs::Result<rquickjs::Value<'js>> {
1808 let core_opts = match options.0 {
1809 None => ferridriver::snapshot::SnapshotOptions::default(),
1810 Some(v) if v.is_undefined() || v.is_null() => ferridriver::snapshot::SnapshotOptions::default(),
1811 Some(v) => {
1812 #[derive(serde::Deserialize, Default)]
1813 #[serde(rename_all = "camelCase", default)]
1814 struct JsSnap {
1815 depth: Option<i32>,
1816 track: Option<String>,
1817 }
1818 let parsed: JsSnap = crate::bindings::convert::serde_from_js(&ctx, v)?;
1819 ferridriver::snapshot::SnapshotOptions {
1820 depth: parsed.depth,
1821 track: parsed.track,
1822 }
1823 },
1824 };
1825 let snap = self.inner.snapshot_for_ai(core_opts).await.into_js()?;
1826 let obj = rquickjs::Object::new(ctx.clone())?;
1827 obj.set("full", snap.full)?;
1828 if let Some(inc) = snap.incremental {
1829 obj.set("incremental", inc)?;
1830 }
1831 let ref_map = rquickjs::Object::new(ctx.clone())?;
1832 for (k, v) in snap.ref_map {
1833 ref_map.set(k, v as f64)?;
1834 }
1835 obj.set("refMap", ref_map)?;
1836 rquickjs::IntoJs::into_js(obj, &ctx)
1837 }
1838
1839 #[qjs(rename = "ariaSnapshot")]
1841 pub async fn aria_snapshot<'js>(
1842 &self,
1843 ctx: rquickjs::Ctx<'js>,
1844 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
1845 ) -> rquickjs::Result<String> {
1846 let core_opts = match options.0 {
1847 Some(v) if !v.is_undefined() && !v.is_null() => {
1848 #[derive(serde::Deserialize, Default)]
1849 #[serde(rename_all = "camelCase", default)]
1850 struct JsSnap {
1851 depth: Option<i32>,
1852 track: Option<String>,
1853 }
1854 let p: JsSnap = crate::bindings::convert::serde_from_js(&ctx, v)?;
1855 ferridriver::snapshot::SnapshotOptions {
1856 depth: p.depth,
1857 track: p.track,
1858 }
1859 },
1860 _ => ferridriver::snapshot::SnapshotOptions::default(),
1861 };
1862 self.inner.aria_snapshot(core_opts).await.into_js()
1863 }
1864
1865 #[qjs(rename = "exposeFunction")]
1874 pub async fn expose_function<'js>(
1875 &self,
1876 ctx: rquickjs::Ctx<'js>,
1877 name: String,
1878 callback: rquickjs::Function<'js>,
1879 ) -> rquickjs::Result<()> {
1880 let async_ctx = self.async_ctx.clone().ok_or_else(|| {
1881 rquickjs::Error::new_from_js_message(
1882 "page.exposeFunction",
1883 "Error",
1884 "page.exposeFunction requires the script engine's AsyncContext (install_page)".to_string(),
1885 )
1886 })?;
1887 let saved = rquickjs::Persistent::save(&ctx, callback);
1891 with_page_callbacks(&ctx, |r| r.exposed.insert(name.clone(), saved))?;
1892
1893 let cb: ferridriver::events::ExposedFn = std::sync::Arc::new({
1894 let name = name.clone();
1895 move |args: Vec<serde_json::Value>| {
1896 let async_ctx = async_ctx.clone();
1897 let name = name.clone();
1898 Box::pin(async move {
1906 let out: rquickjs::Result<serde_json::Value> = rquickjs::async_with!(async_ctx => |ctx| {
1907 let f = with_page_callbacks(&ctx, |r| r.exposed.get(&name).cloned())?
1908 .ok_or_else(|| {
1909 rquickjs::Error::new_from_js_message(
1910 "page.exposeFunction",
1911 "Error",
1912 "exposed callback gone".to_string(),
1913 )
1914 })?
1915 .restore(&ctx)?;
1916 let mut call_args = rquickjs::function::Args::new_unsized(ctx.clone());
1921 for v in args {
1922 call_args.push_arg(crate::bindings::convert::json_to_js(&ctx, &v)?)?;
1928 }
1929 let mp: rquickjs::promise::MaybePromise<'_> = call_args.apply(&f)?;
1930 let res = mp.into_future::<rquickjs::Value<'_>>().await?;
1931 let json = match ctx.json_stringify(res)? {
1936 Some(s) => serde_json::from_str(&s.to_string()?).unwrap_or(serde_json::Value::Null),
1937 None => serde_json::Value::Null,
1938 };
1939 Ok(json)
1940 })
1941 .await;
1942 out.unwrap_or(serde_json::Value::Null)
1943 })
1944 }
1945 });
1946 self.inner.expose_function(&name, cb).await.into_js()
1947 }
1948
1949 #[qjs(rename = "startScreencast")]
1954 pub async fn start_screencast<'js>(
1955 &self,
1956 ctx: rquickjs::Ctx<'js>,
1957 quality: u8,
1958 max_width: u32,
1959 max_height: u32,
1960 callback: rquickjs::Function<'js>,
1961 ) -> rquickjs::Result<()> {
1962 let async_ctx = self.async_ctx.clone().ok_or_else(|| {
1963 rquickjs::Error::new_from_js_message(
1964 "page.startScreencast",
1965 "Error",
1966 "page.startScreencast requires the script engine's AsyncContext (install_page)".to_string(),
1967 )
1968 })?;
1969 let saved = rquickjs::Persistent::save(&ctx, callback);
1970 with_page_callbacks(&ctx, |r| r.screencast = Some(saved))?;
1971 let (mut rx, _shutdown) = self
1977 .inner
1978 .start_screencast(quality, max_width, max_height)
1979 .await
1980 .into_js()?;
1981 tokio::spawn(async move {
1982 while let Some((bytes, ts)) = rx.recv().await {
1983 let _: rquickjs::Result<()> = rquickjs::async_with!(async_ctx => |ctx| {
1984 let f = with_page_callbacks(&ctx, |r| r.screencast.clone())?
1985 .ok_or_else(|| rquickjs::Error::new_from_js_message("page.startScreencast", "Error", "screencast callback gone".to_string()))?
1986 .restore(&ctx)?;
1987 let payload = rquickjs::Object::new(ctx.clone())?;
1988 let buf = rquickjs::TypedArray::<u8>::new(ctx.clone(), bytes)?;
1989 payload.set("frame", buf)?;
1990 payload.set("timestamp", ts)?;
1991 let _: rquickjs::Value<'_> = f.call((payload,))?;
1992 Ok(())
1993 })
1994 .await;
1995 }
1996 });
1997 Ok(())
1998 }
1999
2000 #[qjs(rename = "stopScreencast")]
2003 pub async fn stop_screencast(&self) -> rquickjs::Result<()> {
2004 self.inner.stop_screencast().await.into_js()
2005 }
2006}
2007
2008#[derive(rquickjs::JsLifetime, rquickjs::class::Trace)]
2010#[rquickjs::class(rename = "Touchscreen")]
2011pub struct TouchscreenJs {
2012 #[qjs(skip_trace)]
2013 page: std::sync::Arc<ferridriver::Page>,
2014}
2015
2016#[rquickjs::methods]
2017impl TouchscreenJs {
2018 #[qjs(rename = "tap")]
2020 pub async fn tap(&self, x: f64, y: f64) -> rquickjs::Result<()> {
2021 self.page.touchscreen().tap(x, y).await.into_js()
2022 }
2023}
2024
2025#[derive(Debug, Default, Deserialize)]
2029#[serde(default, rename_all = "camelCase")]
2030struct JsScreenshotOptions {
2031 animations: Option<String>,
2032 caret: Option<String>,
2033 clip: Option<JsClipRect>,
2034 full_page: Option<bool>,
2035 #[serde(rename = "type")]
2036 format: Option<String>,
2037 #[serde(skip)]
2042 _mask_placeholder: (),
2043 mask_color: Option<String>,
2044 omit_background: Option<bool>,
2045 path: Option<String>,
2046 quality: Option<i64>,
2047 scale: Option<String>,
2048 style: Option<String>,
2049 timeout: Option<u64>,
2050}
2051
2052#[derive(Debug, Default, Deserialize, Clone, Copy)]
2053struct JsClipRect {
2054 x: f64,
2055 y: f64,
2056 width: f64,
2057 height: f64,
2058}
2059
2060impl From<JsClipRect> for ferridriver::options::ClipRect {
2061 fn from(c: JsClipRect) -> Self {
2062 Self {
2063 x: c.x,
2064 y: c.y,
2065 width: c.width,
2066 height: c.height,
2067 }
2068 }
2069}
2070
2071fn parse_mask_locators<'js>(obj: &rquickjs::Object<'js>) -> rquickjs::Result<Vec<ferridriver::Locator>> {
2076 let v: rquickjs::Value<'js> = obj.get("mask")?;
2077 if v.is_undefined() || v.is_null() {
2078 return Ok(Vec::new());
2079 }
2080 let arr = v.into_array().ok_or_else(|| {
2081 rquickjs::Error::new_from_js_message("screenshot options", "mask", "expected an array of Locator")
2082 })?;
2083 let mut out = Vec::with_capacity(arr.len());
2084 for item in arr.iter::<rquickjs::Value<'js>>() {
2085 let item = item?;
2086 if let Ok(class) = rquickjs::Class::<LocatorJs>::from_value(&item) {
2087 out.push(class.borrow().inner_ref().clone());
2088 } else {
2089 return Err(rquickjs::Error::new_from_js_message(
2090 "screenshot options",
2091 "mask",
2092 "each mask entry must be a Locator instance",
2093 ));
2094 }
2095 }
2096 Ok(out)
2097}
2098
2099fn parse_screenshot_options<'js>(
2100 ctx: &rquickjs::Ctx<'js>,
2101 value: Opt<rquickjs::Value<'js>>,
2102) -> rquickjs::Result<ferridriver::options::ScreenshotOptions> {
2103 match value.0 {
2104 Some(v) if !v.is_undefined() && !v.is_null() => {
2105 let mask = match v.as_object() {
2106 Some(obj) => parse_mask_locators(obj)?,
2107 None => Vec::new(),
2108 };
2109 let js: JsScreenshotOptions = serde_from_js(ctx, v)?;
2110 Ok(ferridriver::options::ScreenshotOptions {
2111 animations: js.animations,
2112 caret: js.caret,
2113 clip: js.clip.map(Into::into),
2114 full_page: js.full_page,
2115 format: js.format,
2116 mask,
2117 mask_color: js.mask_color,
2118 omit_background: js.omit_background,
2119 path: js.path.map(std::path::PathBuf::from),
2120 quality: js.quality,
2121 scale: js.scale,
2122 style: js.style,
2123 timeout: js.timeout,
2124 })
2125 },
2126 _ => Ok(ferridriver::options::ScreenshotOptions::default()),
2127 }
2128}
2129
2130#[derive(Debug, Default, Deserialize)]
2134#[serde(default, rename_all = "camelCase")]
2135struct JsPdfOptions {
2136 format: Option<String>,
2137 landscape: Option<bool>,
2138 print_background: Option<bool>,
2139 scale: Option<f64>,
2140 display_header_footer: Option<bool>,
2141 header_template: Option<String>,
2142 footer_template: Option<String>,
2143 page_ranges: Option<String>,
2144 prefer_css_page_size: Option<bool>,
2145 outline: Option<bool>,
2146 tagged: Option<bool>,
2147}
2148
2149fn parse_pdf_options<'js>(
2150 ctx: &rquickjs::Ctx<'js>,
2151 value: Opt<rquickjs::Value<'js>>,
2152) -> rquickjs::Result<ferridriver::options::PdfOptions> {
2153 match value.0 {
2154 Some(v) if !v.is_undefined() && !v.is_null() => {
2155 let js: JsPdfOptions = serde_from_js(ctx, v)?;
2156 Ok(ferridriver::options::PdfOptions {
2157 format: js.format,
2158 path: None,
2159 scale: js.scale,
2160 display_header_footer: js.display_header_footer,
2161 header_template: js.header_template,
2162 footer_template: js.footer_template,
2163 print_background: js.print_background,
2164 landscape: js.landscape,
2165 page_ranges: js.page_ranges,
2166 width: None,
2167 height: None,
2168 margin: None,
2169 prefer_css_page_size: js.prefer_css_page_size,
2170 outline: js.outline,
2171 tagged: js.tagged,
2172 })
2173 },
2174 _ => Ok(ferridriver::options::PdfOptions::default()),
2175 }
2176}
2177
2178fn match_event_name(name: &str, ev: &ferridriver::events::PageEvent) -> bool {
2179 use ferridriver::events::PageEvent;
2180 matches!(
2181 (name, ev),
2182 ("console", PageEvent::Console(_))
2183 | ("request", PageEvent::Request(_))
2184 | ("response", PageEvent::Response(_))
2185 | ("requestfinished", PageEvent::RequestFinished(_))
2186 | ("requestfailed", PageEvent::RequestFailed(_))
2187 | ("websocket", PageEvent::WebSocket(_))
2188 | ("dialog", PageEvent::Dialog(_))
2189 | ("filechooser", PageEvent::FileChooser(_))
2190 | ("frameattached", PageEvent::FrameAttached(_))
2191 | ("framedetached", PageEvent::FrameDetached { .. })
2192 | ("framenavigated", PageEvent::FrameNavigated(_))
2193 | ("load", PageEvent::Load)
2194 | ("domcontentloaded", PageEvent::DomContentLoaded)
2195 | ("close", PageEvent::Close)
2196 | ("pageerror", PageEvent::PageError(_))
2197 | ("download", PageEvent::Download(_))
2198 )
2199}
2200
2201fn page_event_to_js<'js>(
2205 ctx: &rquickjs::Ctx<'js>,
2206 ev: &ferridriver::events::PageEvent,
2207) -> rquickjs::Result<rquickjs::Value<'js>> {
2208 use ferridriver::events::PageEvent;
2209 let obj = || rquickjs::Object::new(ctx.clone());
2210 match ev {
2211 PageEvent::Console(msg) => {
2212 let loc = msg.location();
2213 let o = obj()?;
2214 o.set("type", msg.type_str())?;
2215 o.set("text", msg.text())?;
2216 let l = obj()?;
2217 l.set("url", loc.url.as_str())?;
2218 l.set("lineNumber", f64::from(loc.line_number))?;
2219 l.set("columnNumber", f64::from(loc.column_number))?;
2220 o.set("location", l)?;
2221 o.set("timestamp", msg.timestamp())?;
2222 o.set("argsCount", msg.args().len() as f64)?;
2223 Ok(o.into_value())
2224 },
2225 PageEvent::Dialog(d) => {
2226 let o = obj()?;
2227 o.set("type", d.dialog_type().as_str())?;
2228 o.set("message", d.message())?;
2229 o.set("defaultValue", d.default_value())?;
2230 Ok(o.into_value())
2231 },
2232 PageEvent::FileChooser(fc) => {
2233 let o = obj()?;
2234 o.set("isMultiple", fc.is_multiple())?;
2235 Ok(o.into_value())
2236 },
2237 PageEvent::FrameAttached(f) | PageEvent::FrameNavigated(f) => crate::bindings::convert::serde_to_js(ctx, f),
2238 PageEvent::FrameDetached { frame_id } => {
2239 let o = obj()?;
2240 o.set("frameId", frame_id.as_str())?;
2241 Ok(o.into_value())
2242 },
2243 PageEvent::Download(d) => {
2244 let o = obj()?;
2245 o.set("url", d.url())?;
2246 o.set("suggestedFilename", d.suggested_filename())?;
2247 Ok(o.into_value())
2248 },
2249 PageEvent::Load => {
2250 let o = obj()?;
2251 o.set("type", "load")?;
2252 Ok(o.into_value())
2253 },
2254 PageEvent::DomContentLoaded => {
2255 let o = obj()?;
2256 o.set("type", "domcontentloaded")?;
2257 Ok(o.into_value())
2258 },
2259 PageEvent::Close => {
2260 let o = obj()?;
2261 o.set("type", "close")?;
2262 Ok(o.into_value())
2263 },
2264 PageEvent::PageError(err) => {
2265 let details = err.error();
2266 let o = obj()?;
2267 o.set("name", details.name.as_str())?;
2268 o.set("message", details.message.as_str())?;
2269 o.set("stack", details.stack.as_str())?;
2270 Ok(o.into_value())
2271 },
2272 _ => Ok(rquickjs::Value::new_null(ctx.clone())),
2273 }
2274}
2275
2276fn js_truthy(v: &rquickjs::Value<'_>) -> bool {
2278 if v.is_undefined() || v.is_null() {
2279 return false;
2280 }
2281 if let Some(b) = v.as_bool() {
2282 return b;
2283 }
2284 if let Some(i) = v.as_int() {
2285 return i != 0;
2286 }
2287 if let Some(f) = v.as_float() {
2288 return f != 0.0 && !f.is_nan();
2289 }
2290 if let Some(s) = v.as_string() {
2291 return !s.to_string().unwrap_or_default().is_empty();
2292 }
2293 true
2294}
2295
2296pub(crate) async fn call_predicate_truthy<'js>(
2298 pred: &rquickjs::Function<'js>,
2299 arg: impl rquickjs::IntoJs<'js>,
2300 ctx: &rquickjs::Ctx<'js>,
2301) -> rquickjs::Result<bool> {
2302 let arg = arg.into_js(ctx)?;
2303 let mp: rquickjs::promise::MaybePromise<'js> = pred.call((arg,))?;
2304 let v: rquickjs::Value<'js> = mp.into_future().await?;
2305 Ok(js_truthy(&v))
2306}
2307
2308async fn wait_request_predicate<'js>(
2312 ctx: rquickjs::Ctx<'js>,
2313 page: Arc<Page>,
2314 pred: rquickjs::Function<'js>,
2315 timeout_ms: u64,
2316) -> rquickjs::Result<crate::bindings::network::RequestJs> {
2317 use ferridriver::events::PageEvent;
2318 use rquickjs::class::Class;
2319 let mut rx = page.events().subscribe();
2320 let deadline = tokio::time::Instant::now() + std::time::Duration::from_millis(timeout_ms);
2321 loop {
2322 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
2323 if remaining.is_zero() {
2324 return Err(rquickjs::Error::new_from_js_message(
2325 "page.waitForRequest",
2326 "TimeoutError",
2327 format!("Timeout {timeout_ms}ms exceeded while waiting for request"),
2328 ));
2329 }
2330 match tokio::time::timeout(remaining, rx.recv()).await {
2331 Ok(Ok(PageEvent::Request(req))) => {
2332 let probe = crate::bindings::network::RequestJs::new_with_page(req.clone(), page.clone());
2333 let inst = Class::instance(ctx.clone(), probe)?;
2334 if call_predicate_truthy(&pred, inst, &ctx).await? {
2335 return Ok(crate::bindings::network::RequestJs::new_with_page(req, page.clone()));
2336 }
2337 },
2338 Ok(Ok(_) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => {},
2339 Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => {
2340 return Err(rquickjs::Error::new_from_js_message(
2341 "page.waitForRequest",
2342 "Error",
2343 "page closed while waiting for request".to_string(),
2344 ));
2345 },
2346 Err(_) => {
2347 return Err(rquickjs::Error::new_from_js_message(
2348 "page.waitForRequest",
2349 "TimeoutError",
2350 format!("Timeout {timeout_ms}ms exceeded while waiting for request"),
2351 ));
2352 },
2353 }
2354 }
2355}
2356
2357async fn wait_response_predicate<'js>(
2359 ctx: rquickjs::Ctx<'js>,
2360 page: Arc<Page>,
2361 pred: rquickjs::Function<'js>,
2362 timeout_ms: u64,
2363) -> rquickjs::Result<crate::bindings::network::ResponseJs> {
2364 use ferridriver::events::PageEvent;
2365 use rquickjs::class::Class;
2366 let mut rx = page.events().subscribe();
2367 let deadline = tokio::time::Instant::now() + std::time::Duration::from_millis(timeout_ms);
2368 loop {
2369 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
2370 if remaining.is_zero() {
2371 return Err(rquickjs::Error::new_from_js_message(
2372 "page.waitForResponse",
2373 "TimeoutError",
2374 format!("Timeout {timeout_ms}ms exceeded while waiting for response"),
2375 ));
2376 }
2377 match tokio::time::timeout(remaining, rx.recv()).await {
2378 Ok(Ok(PageEvent::Response(resp))) => {
2379 let probe = crate::bindings::network::ResponseJs::new_with_page(resp.clone(), page.clone());
2380 let inst = Class::instance(ctx.clone(), probe)?;
2381 if call_predicate_truthy(&pred, inst, &ctx).await? {
2382 return Ok(crate::bindings::network::ResponseJs::new_with_page(resp, page.clone()));
2383 }
2384 },
2385 Ok(Ok(_) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => {},
2386 Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => {
2387 return Err(rquickjs::Error::new_from_js_message(
2388 "page.waitForResponse",
2389 "Error",
2390 "page closed while waiting for response".to_string(),
2391 ));
2392 },
2393 Err(_) => {
2394 return Err(rquickjs::Error::new_from_js_message(
2395 "page.waitForResponse",
2396 "TimeoutError",
2397 format!("Timeout {timeout_ms}ms exceeded while waiting for response"),
2398 ));
2399 },
2400 }
2401 }
2402}
2403
2404pub(crate) fn url_value_to_matcher<'js>(
2409 ctx: &rquickjs::Ctx<'js>,
2410 value: rquickjs::Value<'js>,
2411) -> rquickjs::Result<ferridriver::url_matcher::UrlMatcher> {
2412 use crate::bindings::convert::FerriResultExt;
2413 if let Some(s) = value.as_string() {
2414 let glob = s.to_string()?;
2415 return ferridriver::url_matcher::UrlMatcher::glob(glob).into_js();
2416 }
2417 if let Some(obj) = value.as_object() {
2418 let source: rquickjs::Result<String> = obj.get("source");
2421 let flags: rquickjs::Result<String> = obj.get("flags");
2422 if let (Ok(source), Ok(flags)) = (source, flags) {
2423 return ferridriver::url_matcher::UrlMatcher::regex_from_source(&source, &flags).into_js();
2424 }
2425 }
2426 let _ = ctx;
2427 Err(rquickjs::Error::new_from_js_message(
2428 "Page.waitFor*",
2429 "url",
2430 "expected string | RegExp".to_string(),
2431 ))
2432}
2433
2434pub(crate) fn string_or_regex_from_js(
2440 value: rquickjs::Value<'_>,
2441) -> rquickjs::Result<ferridriver::options::StringOrRegex> {
2442 if let Some(s) = value.as_string() {
2443 return Ok(ferridriver::options::StringOrRegex::String(s.to_string()?));
2444 }
2445 if let Some(obj) = value.as_object() {
2446 let source: rquickjs::Result<String> = obj.get("source");
2447 let flags: rquickjs::Result<String> = obj.get("flags");
2448 if let (Ok(source), Ok(flags)) = (source, flags) {
2449 return Ok(ferridriver::options::StringOrRegex::Regex { source, flags });
2450 }
2451 }
2452 Err(rquickjs::Error::new_from_js_message(
2453 "getBy*",
2454 "text",
2455 "expected string | RegExp".to_string(),
2456 ))
2457}
2458
2459pub(crate) fn parse_text_options(
2461 value: rquickjs::function::Opt<rquickjs::Value<'_>>,
2462) -> ferridriver::options::TextOptions {
2463 let Some(v) = value.0 else {
2464 return ferridriver::options::TextOptions::default();
2465 };
2466 if v.is_undefined() || v.is_null() {
2467 return ferridriver::options::TextOptions::default();
2468 }
2469 let Some(obj) = v.as_object() else {
2470 return ferridriver::options::TextOptions::default();
2471 };
2472 let exact: Option<bool> = obj.get("exact").ok();
2473 ferridriver::options::TextOptions { exact }
2474}
2475
2476pub(crate) fn parse_role_options<'js>(
2480 value: rquickjs::function::Opt<rquickjs::Value<'js>>,
2481) -> rquickjs::Result<ferridriver::options::RoleOptions> {
2482 let Some(v) = value.0 else {
2483 return Ok(ferridriver::options::RoleOptions::default());
2484 };
2485 if v.is_undefined() || v.is_null() {
2486 return Ok(ferridriver::options::RoleOptions::default());
2487 }
2488 let Some(obj) = v.as_object() else {
2489 return Ok(ferridriver::options::RoleOptions::default());
2490 };
2491 let name_val: Option<rquickjs::Value<'js>> = obj.get("name").ok();
2492 let name = match name_val {
2493 Some(val) if !val.is_undefined() && !val.is_null() => Some(string_or_regex_from_js(val)?),
2494 _ => None,
2495 };
2496 let exact: Option<bool> = obj.get("exact").ok();
2497 let checked: Option<bool> = obj.get("checked").ok();
2498 let disabled: Option<bool> = obj.get("disabled").ok();
2499 let expanded: Option<bool> = obj.get("expanded").ok();
2500 let level: Option<i32> = obj.get("level").ok();
2501 let pressed: Option<bool> = obj.get("pressed").ok();
2502 let selected: Option<bool> = obj.get("selected").ok();
2503 let include_hidden: Option<bool> = obj.get("includeHidden").ok();
2504 Ok(ferridriver::options::RoleOptions {
2505 name,
2506 exact,
2507 checked,
2508 disabled,
2509 expanded,
2510 level,
2511 pressed,
2512 selected,
2513 include_hidden,
2514 })
2515}