use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use ferridriver::Page;
use rquickjs::JsLifetime;
use rquickjs::class::Trace;
use ferridriver::options::WaitOptions;
use rquickjs::function::Opt;
use serde::Deserialize;
use crate::bindings::convert::{
FerriResultExt, extract_page_function, init_script_from_js, quickjs_arg_to_serialized, serde_from_js,
serialized_value_to_quickjs,
};
use crate::bindings::keyboard::KeyboardJs;
use crate::bindings::locator::LocatorJs;
use crate::bindings::mouse::MouseJs;
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct JsWaitOptions {
state: Option<String>,
timeout: Option<u64>,
}
pub(crate) fn parse_wait_options<'js>(
ctx: &rquickjs::Ctx<'js>,
value: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<WaitOptions> {
match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => {
let js: JsWaitOptions = serde_from_js(ctx, v)?;
Ok(WaitOptions {
state: js.state,
timeout: js.timeout,
})
},
_ => Ok(WaitOptions::default()),
}
}
#[derive(serde::Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsGotoOptions {
wait_until: Option<String>,
timeout: Option<u64>,
referer: Option<String>,
}
fn parse_goto_options<'js>(
ctx: &rquickjs::Ctx<'js>,
value: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<Option<ferridriver::options::GotoOptions>> {
match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => {
let js: JsGotoOptions = serde_from_js(ctx, v)?;
Ok(Some(ferridriver::options::GotoOptions {
wait_until: js.wait_until,
timeout: js.timeout,
referer: js.referer,
}))
},
_ => Ok(None),
}
}
#[derive(serde::Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsPageCloseOptions {
run_before_unload: Option<bool>,
reason: Option<String>,
}
#[derive(serde::Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase", default)]
pub(crate) struct JsDragAndDropOptions {
force: Option<bool>,
no_wait_after: Option<bool>,
source_position: Option<JsPoint>,
target_position: Option<JsPoint>,
steps: Option<u32>,
strict: Option<bool>,
timeout: Option<u64>,
trial: Option<bool>,
}
#[derive(serde::Deserialize, Debug, Default, Clone, Copy)]
pub(crate) struct JsPoint {
x: f64,
y: f64,
}
impl From<JsPoint> for ferridriver::options::Point {
fn from(p: JsPoint) -> Self {
Self { x: p.x, y: p.y }
}
}
fn parse_emulate_media_field<'js>(
obj: &rquickjs::Object<'js>,
key: &str,
) -> rquickjs::Result<ferridriver::options::MediaOverride> {
use ferridriver::options::MediaOverride;
if !obj.contains_key(key)? {
return Ok(MediaOverride::Unchanged);
}
let val: rquickjs::Value<'js> = obj.get(key)?;
if val.is_undefined() {
Ok(MediaOverride::Unchanged)
} else if val.is_null() {
Ok(MediaOverride::Disabled)
} else if let Some(s) = val.as_string() {
Ok(MediaOverride::Set(s.to_string()?))
} else {
Err(rquickjs::Error::new_from_js_message(
"emulateMedia options",
"field",
format!("{key}: expected null, undefined, or string"),
))
}
}
pub(crate) fn parse_emulate_media_options<'js>(
_ctx: &rquickjs::Ctx<'js>,
value: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<ferridriver::options::EmulateMediaOptions> {
let Some(v) = value.0.filter(|v| !v.is_undefined() && !v.is_null()) else {
return Ok(ferridriver::options::EmulateMediaOptions::default());
};
let Some(obj) = v.as_object() else {
return Ok(ferridriver::options::EmulateMediaOptions::default());
};
Ok(ferridriver::options::EmulateMediaOptions {
media: parse_emulate_media_field(obj, "media")?,
color_scheme: parse_emulate_media_field(obj, "colorScheme")?,
reduced_motion: parse_emulate_media_field(obj, "reducedMotion")?,
forced_colors: parse_emulate_media_field(obj, "forcedColors")?,
contrast: parse_emulate_media_field(obj, "contrast")?,
})
}
pub(crate) fn parse_drag_options<'js>(
ctx: &rquickjs::Ctx<'js>,
value: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<Option<ferridriver::options::DragAndDropOptions>> {
match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => {
let js: JsDragAndDropOptions = serde_from_js(ctx, v)?;
Ok(Some(ferridriver::options::DragAndDropOptions {
force: js.force,
no_wait_after: js.no_wait_after,
source_position: js.source_position.map(Into::into),
target_position: js.target_position.map(Into::into),
steps: js.steps,
strict: js.strict,
timeout: js.timeout,
trial: js.trial,
}))
},
_ => Ok(None),
}
}
fn parse_page_close_options<'js>(
ctx: &rquickjs::Ctx<'js>,
value: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<Option<ferridriver::options::PageCloseOptions>> {
match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => {
let js: JsPageCloseOptions = serde_from_js(ctx, v)?;
Ok(Some(ferridriver::options::PageCloseOptions {
run_before_unload: js.run_before_unload,
reason: js.reason,
}))
},
_ => Ok(None),
}
}
#[derive(Default)]
pub(crate) struct PageCallbacks {
route_handlers: rustc_hash::FxHashMap<u64, rquickjs::Persistent<rquickjs::Function<'static>>>,
route_preds: rustc_hash::FxHashMap<u64, rquickjs::Persistent<rquickjs::Function<'static>>>,
exposed: rustc_hash::FxHashMap<String, rquickjs::Persistent<rquickjs::Function<'static>>>,
screencast: Option<rquickjs::Persistent<rquickjs::Function<'static>>>,
locator_handlers: rustc_hash::FxHashMap<u64, rquickjs::Persistent<rquickjs::Function<'static>>>,
}
impl PageCallbacks {
pub(crate) fn insert_route_handler(&mut self, id: u64, f: rquickjs::Persistent<rquickjs::Function<'static>>) {
self.route_handlers.insert(id, f);
}
pub(crate) fn insert_route_pred(&mut self, id: u64, f: rquickjs::Persistent<rquickjs::Function<'static>>) {
self.route_preds.insert(id, f);
}
pub(crate) fn get_route_handler(&self, id: u64) -> Option<rquickjs::Persistent<rquickjs::Function<'static>>> {
self.route_handlers.get(&id).cloned()
}
pub(crate) fn get_route_pred(&self, id: u64) -> Option<rquickjs::Persistent<rquickjs::Function<'static>>> {
self.route_preds.get(&id).cloned()
}
pub(crate) fn route_preds_snapshot(&self) -> Vec<(u64, rquickjs::Persistent<rquickjs::Function<'static>>)> {
self.route_preds.iter().map(|(k, v)| (*k, v.clone())).collect()
}
pub(crate) fn remove_route(&mut self, id: u64) {
self.route_preds.remove(&id);
self.route_handlers.remove(&id);
}
pub(crate) fn remove_locator_handler(&mut self, id: u64) {
self.locator_handlers.remove(&id);
}
}
pub(crate) struct PageCallbacksUd(std::cell::RefCell<PageCallbacks>);
#[allow(unsafe_code)]
unsafe impl rquickjs::JsLifetime<'_> for PageCallbacksUd {
type Changed<'to> = PageCallbacksUd;
}
pub(crate) fn ensure_page_callbacks(ctx: &rquickjs::Ctx<'_>) {
if ctx.userdata::<PageCallbacksUd>().is_none() {
let _ = ctx.store_userdata(PageCallbacksUd(std::cell::RefCell::new(PageCallbacks::default())));
}
}
pub(crate) fn with_page_callbacks<R>(
ctx: &rquickjs::Ctx<'_>,
f: impl FnOnce(&mut PageCallbacks) -> R,
) -> rquickjs::Result<R> {
ensure_page_callbacks(ctx);
let ud = ctx.userdata::<PageCallbacksUd>().ok_or_else(|| {
rquickjs::Error::new_from_js_message("page", "Error", "page callbacks registry missing".to_string())
})?;
let mut reg = ud.0.borrow_mut();
Ok(f(&mut reg))
}
pub(crate) fn insert_exposed_callback(
ctx: &rquickjs::Ctx<'_>,
name: String,
cb: rquickjs::Persistent<rquickjs::Function<'static>>,
) -> rquickjs::Result<()> {
with_page_callbacks(ctx, |r| r.exposed.insert(name, cb))?;
Ok(())
}
pub(crate) fn get_exposed_callback(
ctx: &rquickjs::Ctx<'_>,
name: &str,
) -> rquickjs::Result<Option<rquickjs::Persistent<rquickjs::Function<'static>>>> {
with_page_callbacks(ctx, |r| r.exposed.get(name).cloned())
}
fn parse_unroute_behavior(behavior: &str) -> rquickjs::Result<ferridriver::options::UnrouteBehavior> {
match behavior {
"default" => Ok(ferridriver::options::UnrouteBehavior::Default),
"wait" => Ok(ferridriver::options::UnrouteBehavior::Wait),
"ignoreErrors" => Ok(ferridriver::options::UnrouteBehavior::IgnoreErrors),
other => Err(rquickjs::Error::new_from_js_message(
"unrouteAll options",
"behavior",
format!("invalid behavior {other:?} (expected 'wait', 'ignoreErrors', or 'default')"),
)),
}
}
pub(crate) fn parse_route_times(
options: &rquickjs::function::Opt<rquickjs::Value<'_>>,
) -> rquickjs::Result<Option<u32>> {
let Some(v) = options.0.as_ref() else { return Ok(None) };
if v.is_undefined() || v.is_null() {
return Ok(None);
}
let Some(obj) = v.as_object() else { return Ok(None) };
let t: rquickjs::Value<'_> = obj.get("times")?;
if t.is_undefined() || t.is_null() {
return Ok(None);
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
Ok(t.as_number().map(|n| if n < 0.0 { 0 } else { n as u32 }))
}
pub(crate) fn parse_har_options(
options: &rquickjs::function::Opt<rquickjs::Value<'_>>,
) -> rquickjs::Result<ferridriver::har::RouteFromHarOptions> {
let mut out = ferridriver::har::RouteFromHarOptions::default();
let Some(v) = options.0.as_ref() else { return Ok(out) };
let Some(obj) = v.as_object() else { return Ok(out) };
let url: rquickjs::Value<'_> = obj.get("url")?;
if let Some(s) = url.as_string() {
let glob = s.to_string()?;
out.url = Some(
ferridriver::url_matcher::UrlMatcher::glob(glob)
.map_err(|e| rquickjs::Error::new_from_js_message("routeFromHAR", "url", format!("invalid url glob: {e}")))?,
);
}
let nf: rquickjs::Value<'_> = obj.get("notFound")?;
if let Some(s) = nf.as_string() {
match s.to_string()?.as_str() {
"fallback" => out.not_found = ferridriver::har::HarNotFound::Fallback,
"abort" => out.not_found = ferridriver::har::HarNotFound::Abort,
other => {
return Err(rquickjs::Error::new_from_js_message(
"routeFromHAR",
"notFound",
format!("invalid notFound {other:?} (expected 'abort' or 'fallback')"),
));
},
}
}
Ok(out)
}
#[derive(JsLifetime, Trace)]
#[rquickjs::class(rename = "Page")]
pub struct PageJs {
#[qjs(skip_trace)]
inner: Arc<Page>,
#[qjs(skip_trace)]
async_ctx: Option<rquickjs::AsyncContext>,
#[qjs(skip_trace)]
next_route_id: Arc<AtomicU64>,
#[qjs(skip_trace)]
route_matchers: Arc<std::sync::Mutex<rustc_hash::FxHashMap<u64, ferridriver::url_matcher::UrlMatcher>>>,
#[qjs(skip_trace)]
locator_handler_ids: Arc<std::sync::Mutex<rustc_hash::FxHashMap<String, Vec<u64>>>>,
}
impl PageJs {
#[must_use]
pub fn new(inner: Arc<Page>) -> Self {
Self {
inner,
async_ctx: None,
next_route_id: Arc::new(AtomicU64::new(0)),
route_matchers: Arc::new(std::sync::Mutex::new(rustc_hash::FxHashMap::default())),
locator_handler_ids: Arc::new(std::sync::Mutex::new(rustc_hash::FxHashMap::default())),
}
}
#[must_use]
pub fn new_with_async_ctx(inner: Arc<Page>, async_ctx: rquickjs::AsyncContext) -> Self {
Self {
inner,
async_ctx: Some(async_ctx),
next_route_id: Arc::new(AtomicU64::new(0)),
route_matchers: Arc::new(std::sync::Mutex::new(rustc_hash::FxHashMap::default())),
locator_handler_ids: Arc::new(std::sync::Mutex::new(rustc_hash::FxHashMap::default())),
}
}
#[must_use]
pub fn page_arc(&self) -> Arc<Page> {
self.inner.clone()
}
#[must_use]
pub fn page(&self) -> &Arc<Page> {
&self.inner
}
}
pub(crate) fn pagejs_for_ctx(ctx: &rquickjs::Ctx<'_>, page: Arc<Page>) -> PageJs {
match ctx.userdata::<crate::engine::SessionAsyncCtx>() {
Some(ud) => PageJs::new_with_async_ctx(page, ud.0.clone()),
None => PageJs::new(page),
}
}
#[rquickjs::methods]
impl PageJs {
#[qjs(rename = "goto")]
pub async fn goto<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
url: String,
options: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<Option<crate::bindings::network::ResponseJs>> {
let opts = parse_goto_options(&ctx, options)?;
let resp = self.inner.goto(&url, opts).await.into_js()?;
Ok(resp.map(|r| crate::bindings::network::ResponseJs::new_with_page(r, self.inner.clone())))
}
#[qjs(rename = "reload")]
pub async fn reload<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
options: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<Option<crate::bindings::network::ResponseJs>> {
let opts = parse_goto_options(&ctx, options)?;
let resp = self.inner.reload(opts).await.into_js()?;
Ok(resp.map(|r| crate::bindings::network::ResponseJs::new_with_page(r, self.inner.clone())))
}
#[qjs(rename = "goBack")]
pub async fn go_back<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
options: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<Option<crate::bindings::network::ResponseJs>> {
let opts = parse_goto_options(&ctx, options)?;
let resp = self.inner.go_back(opts).await.into_js()?;
Ok(resp.map(|r| crate::bindings::network::ResponseJs::new_with_page(r, self.inner.clone())))
}
#[qjs(rename = "goForward")]
pub async fn go_forward<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
options: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<Option<crate::bindings::network::ResponseJs>> {
let opts = parse_goto_options(&ctx, options)?;
let resp = self.inner.go_forward(opts).await.into_js()?;
Ok(resp.map(|r| crate::bindings::network::ResponseJs::new_with_page(r, self.inner.clone())))
}
#[qjs(rename = "url")]
pub fn url(&self) -> String {
self.inner.url()
}
#[qjs(rename = "title")]
pub async fn title(&self) -> rquickjs::Result<String> {
self.inner.title().await.into_js()
}
#[qjs(rename = "video")]
pub fn video<'js>(&self, ctx: rquickjs::Ctx<'js>) -> rquickjs::Result<rquickjs::Value<'js>> {
use rquickjs::class::Class;
match self.inner.video() {
Some(video) => {
let wrapper = crate::bindings::video::VideoJs::new(video);
let instance = Class::instance(ctx.clone(), wrapper)?;
rquickjs::IntoJs::into_js(instance, &ctx)
},
None => Ok(rquickjs::Value::new_null(ctx)),
}
}
#[qjs(rename = "content")]
pub async fn content(&self) -> rquickjs::Result<String> {
self.inner.content().await.into_js()
}
#[qjs(rename = "setContent")]
pub async fn set_content(&self, html: String) -> rquickjs::Result<()> {
self.inner.set_content(&html).await.into_js()
}
#[qjs(rename = "addInitScript")]
pub async fn add_init_script<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
script: rquickjs::Value<'js>,
arg: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<rquickjs::Value<'js>> {
let (init, arg_json) = init_script_from_js(&ctx, script, arg.0)?;
let disposable = self.inner.add_init_script(init, arg_json).await.into_js()?;
let instance =
rquickjs::class::Class::instance(ctx.clone(), crate::bindings::disposable::DisposableJs::new(disposable))?;
rquickjs::IntoJs::into_js(instance, &ctx)
}
#[qjs(rename = "removeInitScript")]
pub async fn remove_init_script(&self, identifier: String) -> rquickjs::Result<()> {
self.inner.remove_init_script(&identifier).await.into_js()
}
#[qjs(rename = "markdown")]
pub async fn markdown(&self) -> rquickjs::Result<String> {
self.inner.markdown().await.into_js()
}
#[qjs(rename = "waitForSelector")]
pub async fn wait_for_selector<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
selector: String,
options: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<()> {
let opts = parse_wait_options(&ctx, options)?;
self.inner.wait_for_selector(&selector, opts).await.into_js()
}
#[qjs(rename = "querySelector")]
pub async fn query_selector(
&self,
selector: String,
) -> rquickjs::Result<Option<crate::bindings::element_handle::ElementHandleJs>> {
let inner = self.inner.query_selector(&selector).await.into_js()?;
Ok(inner.map(crate::bindings::element_handle::ElementHandleJs::new))
}
#[qjs(rename = "$")]
pub async fn dollar(
&self,
selector: String,
) -> rquickjs::Result<Option<crate::bindings::element_handle::ElementHandleJs>> {
self.query_selector(selector).await
}
#[qjs(rename = "querySelectorAll")]
pub async fn query_selector_all(
&self,
selector: String,
) -> rquickjs::Result<Vec<crate::bindings::element_handle::ElementHandleJs>> {
let inner_handles = self.inner.query_selector_all(&selector).await.into_js()?;
Ok(
inner_handles
.into_iter()
.map(crate::bindings::element_handle::ElementHandleJs::new)
.collect(),
)
}
#[qjs(rename = "$$")]
pub async fn dollar_dollar(
&self,
selector: String,
) -> rquickjs::Result<Vec<crate::bindings::element_handle::ElementHandleJs>> {
self.query_selector_all(selector).await
}
#[qjs(rename = "evaluate")]
pub async fn evaluate<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
page_function: rquickjs::Value<'js>,
arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<rquickjs::Value<'js>> {
let (source, is_fn) = extract_page_function(&ctx, page_function)?;
let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
let result = self.inner.evaluate(&source, serialized, is_fn).await.into_js()?;
serialized_value_to_quickjs(&ctx, &result)
}
#[qjs(rename = "evaluateHandle")]
pub async fn evaluate_handle<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
page_function: rquickjs::Value<'js>,
arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<crate::bindings::js_handle::JSHandleJs> {
let (source, is_fn) = extract_page_function(&ctx, page_function)?;
let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
let handle = self.inner.evaluate_handle(&source, serialized, is_fn).await.into_js()?;
Ok(crate::bindings::js_handle::JSHandleJs::new(handle))
}
#[qjs(rename = "locator")]
pub fn locator<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
selector: String,
options: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<LocatorJs> {
let parsed = crate::bindings::locator::parse_locator_options_public(&ctx, options, true)?;
let opts = ferridriver::options::FilterOptions {
has_text: parsed.has_text,
has_not_text: parsed.has_not_text,
has: parsed.has,
has_not: parsed.has_not,
visible: parsed.visible,
};
let filter = if crate::bindings::locator::is_empty_filter(&opts) {
None
} else {
Some(opts)
};
Ok(LocatorJs::new(self.inner.locator(&selector, filter)))
}
#[qjs(rename = "getByRole")]
pub fn get_by_role(
&self,
role: String,
options: rquickjs::function::Opt<rquickjs::Value<'_>>,
) -> rquickjs::Result<LocatorJs> {
let opts = parse_role_options(options)?;
Ok(LocatorJs::new(self.inner.get_by_role(&role, &opts)))
}
#[qjs(rename = "getByText")]
pub fn get_by_text(
&self,
text: rquickjs::Value<'_>,
options: rquickjs::function::Opt<rquickjs::Value<'_>>,
) -> rquickjs::Result<LocatorJs> {
let t = string_or_regex_from_js(text)?;
let opts = parse_text_options(options);
Ok(LocatorJs::new(self.inner.get_by_text(&t, &opts)))
}
#[qjs(rename = "getByLabel")]
pub fn get_by_label(
&self,
text: rquickjs::Value<'_>,
options: rquickjs::function::Opt<rquickjs::Value<'_>>,
) -> rquickjs::Result<LocatorJs> {
let t = string_or_regex_from_js(text)?;
let opts = parse_text_options(options);
Ok(LocatorJs::new(self.inner.get_by_label(&t, &opts)))
}
#[qjs(rename = "getByPlaceholder")]
pub fn get_by_placeholder(
&self,
text: rquickjs::Value<'_>,
options: rquickjs::function::Opt<rquickjs::Value<'_>>,
) -> rquickjs::Result<LocatorJs> {
let t = string_or_regex_from_js(text)?;
let opts = parse_text_options(options);
Ok(LocatorJs::new(self.inner.get_by_placeholder(&t, &opts)))
}
#[qjs(rename = "getByAltText")]
pub fn get_by_alt_text(
&self,
text: rquickjs::Value<'_>,
options: rquickjs::function::Opt<rquickjs::Value<'_>>,
) -> rquickjs::Result<LocatorJs> {
let t = string_or_regex_from_js(text)?;
let opts = parse_text_options(options);
Ok(LocatorJs::new(self.inner.get_by_alt_text(&t, &opts)))
}
#[qjs(rename = "getByTitle")]
pub fn get_by_title(
&self,
text: rquickjs::Value<'_>,
options: rquickjs::function::Opt<rquickjs::Value<'_>>,
) -> rquickjs::Result<LocatorJs> {
let t = string_or_regex_from_js(text)?;
let opts = parse_text_options(options);
Ok(LocatorJs::new(self.inner.get_by_title(&t, &opts)))
}
#[qjs(rename = "getByTestId")]
pub fn get_by_test_id(&self, test_id: rquickjs::Value<'_>) -> rquickjs::Result<LocatorJs> {
let t = string_or_regex_from_js(test_id)?;
Ok(LocatorJs::new(self.inner.get_by_test_id(&t)))
}
#[qjs(rename = "click")]
pub async fn click<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
selector: String,
options: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<()> {
let opts = crate::bindings::convert::parse_click_options(&ctx, options)?;
self.inner.click(&selector, opts).await.into_js()
}
#[qjs(rename = "dblclick")]
pub async fn dblclick<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
selector: String,
options: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<()> {
let opts = crate::bindings::convert::parse_dblclick_options(&ctx, options)?;
self.inner.dblclick(&selector, opts).await.into_js()
}
#[qjs(rename = "fill")]
pub async fn fill<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
selector: String,
value: String,
options: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<()> {
let opts = crate::bindings::convert::parse_fill_options(&ctx, options)?;
self.inner.fill(&selector, &value, opts).await.into_js()
}
#[qjs(rename = "type")]
pub async fn type_<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
selector: String,
text: String,
options: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<()> {
let opts = crate::bindings::convert::parse_type_options(&ctx, options)?;
self.inner.r#type(&selector, &text, opts).await.into_js()
}
#[qjs(rename = "press")]
pub async fn press<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
selector: String,
key: String,
options: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<()> {
let opts = crate::bindings::convert::parse_press_options(&ctx, options)?;
self.inner.press(&selector, &key, opts).await.into_js()
}
#[qjs(rename = "focus")]
pub async fn focus(
&self,
selector: String,
_options: rquickjs::function::Opt<rquickjs::Value<'_>>,
) -> rquickjs::Result<()> {
self.inner.focus(&selector).await.into_js()
}
#[qjs(rename = "hover")]
pub async fn hover<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
selector: String,
options: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<()> {
let opts = crate::bindings::convert::parse_hover_options(&ctx, options)?;
self.inner.hover(&selector, opts).await.into_js()
}
#[qjs(rename = "dispatchEvent")]
pub async fn dispatch_event<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
selector: String,
event_type: String,
event_init: rquickjs::function::Opt<rquickjs::Value<'js>>,
options: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<()> {
let init_json = match event_init.0 {
Some(v) if !v.is_undefined() && !v.is_null() => {
Some(crate::bindings::convert::serde_from_js::<serde_json::Value>(&ctx, v)?)
},
_ => None,
};
let opts = crate::bindings::convert::parse_dispatch_event_options(&ctx, options)?;
self
.inner
.dispatch_event(&selector, &event_type, init_json, opts)
.await
.into_js()
}
#[qjs(rename = "tap")]
pub async fn tap<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
selector: String,
options: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<()> {
let opts = crate::bindings::convert::parse_tap_options(&ctx, options)?;
self.inner.tap(&selector, opts).await.into_js()
}
#[qjs(rename = "check")]
pub async fn check<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
selector: String,
options: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<()> {
let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
self.inner.check(&selector, opts).await.into_js()
}
#[qjs(rename = "uncheck")]
pub async fn uncheck<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
selector: String,
options: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<()> {
let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
self.inner.uncheck(&selector, opts).await.into_js()
}
#[qjs(rename = "setChecked")]
pub async fn set_checked<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
selector: String,
checked: bool,
options: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<()> {
let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
self.inner.set_checked(&selector, checked, opts).await.into_js()
}
#[qjs(rename = "selectOption")]
pub async fn select_option<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
selector: String,
values: rquickjs::Value<'js>,
options: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<Vec<String>> {
let values = crate::bindings::convert::parse_select_option_values(&ctx, values)?;
let opts = crate::bindings::convert::parse_select_option_options(&ctx, options)?;
self.inner.select_option(&selector, values, opts).await.into_js()
}
#[qjs(rename = "textContent")]
pub async fn text_content(&self, selector: String) -> rquickjs::Result<Option<String>> {
self.inner.text_content(&selector).await.into_js()
}
#[qjs(rename = "innerText")]
pub async fn inner_text(&self, selector: String) -> rquickjs::Result<String> {
self.inner.inner_text(&selector).await.into_js()
}
#[qjs(rename = "innerHTML")]
pub async fn inner_html(&self, selector: String) -> rquickjs::Result<String> {
self.inner.inner_html(&selector).await.into_js()
}
#[qjs(rename = "inputValue")]
pub async fn input_value(&self, selector: String) -> rquickjs::Result<String> {
self.inner.input_value(&selector).await.into_js()
}
#[qjs(rename = "getAttribute")]
pub async fn get_attribute(&self, selector: String, name: String) -> rquickjs::Result<Option<String>> {
self.inner.get_attribute(&selector, &name).await.into_js()
}
#[qjs(rename = "isVisible")]
pub async fn is_visible(&self, selector: String) -> rquickjs::Result<bool> {
self.inner.is_visible(&selector).await.into_js()
}
#[qjs(rename = "isHidden")]
pub async fn is_hidden(&self, selector: String) -> rquickjs::Result<bool> {
self.inner.is_hidden(&selector).await.into_js()
}
#[qjs(rename = "isEnabled")]
pub async fn is_enabled(&self, selector: String) -> rquickjs::Result<bool> {
self.inner.is_enabled(&selector).await.into_js()
}
#[qjs(rename = "isDisabled")]
pub async fn is_disabled(&self, selector: String) -> rquickjs::Result<bool> {
self.inner.is_disabled(&selector).await.into_js()
}
#[qjs(rename = "isChecked")]
pub async fn is_checked(&self, selector: String) -> rquickjs::Result<bool> {
self.inner.is_checked(&selector).await.into_js()
}
#[qjs(get, rename = "mouse")]
pub fn mouse(&self) -> MouseJs {
MouseJs::new(self.inner.clone())
}
#[qjs(get, rename = "keyboard")]
pub fn keyboard(&self) -> KeyboardJs {
KeyboardJs::new(self.inner.clone())
}
#[qjs(rename = "clickAt")]
pub async fn click_at(&self, x: f64, y: f64) -> rquickjs::Result<()> {
self.inner.click_at(x, y).await.into_js()
}
#[qjs(rename = "moveMouseSmooth")]
pub async fn move_mouse_smooth(
&self,
from_x: f64,
from_y: f64,
to_x: f64,
to_y: f64,
steps: u32,
) -> rquickjs::Result<()> {
self
.inner
.move_mouse_smooth(from_x, from_y, to_x, to_y, steps)
.await
.into_js()
}
#[qjs(rename = "dragAndDrop")]
pub async fn drag_and_drop<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
source: String,
target: String,
options: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<()> {
let opts = parse_drag_options(&ctx, options)?;
self.inner.drag_and_drop(&source, &target, opts).await.into_js()
}
#[qjs(rename = "setInputFiles")]
pub async fn set_input_files<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
selector: String,
files: rquickjs::Value<'js>,
options: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<()> {
let files = crate::bindings::convert::parse_input_files(&ctx, files)?;
let opts = crate::bindings::convert::parse_set_input_files_options(&ctx, options)?;
self.inner.set_input_files(&selector, files, opts).await.into_js()
}
#[qjs(rename = "setViewportSize")]
pub async fn set_viewport_size<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
size: rquickjs::Value<'js>,
) -> rquickjs::Result<()> {
#[derive(serde::Deserialize)]
struct Size {
width: i64,
height: i64,
}
let s: Size = crate::bindings::convert::serde_from_js(&ctx, size)?;
self.inner.set_viewport_size(s.width, s.height).await.into_js()
}
#[qjs(rename = "emulateMedia")]
pub async fn emulate_media<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
options: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<()> {
let opts = parse_emulate_media_options(&ctx, options)?;
self.inner.emulate_media(&opts).await.into_js()
}
#[qjs(rename = "screenshot")]
pub async fn screenshot<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
options: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<Vec<u8>> {
let opts = parse_screenshot_options(&ctx, options)?;
self.inner.screenshot(opts).await.into_js()
}
#[qjs(rename = "screenshotElement")]
pub async fn screenshot_element(&self, selector: String) -> rquickjs::Result<Vec<u8>> {
self.inner.screenshot_element(&selector).await.into_js()
}
#[qjs(rename = "pdf")]
pub async fn pdf<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
options: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<Vec<u8>> {
let opts = parse_pdf_options(&ctx, options)?;
self.inner.pdf(opts).await.into_js()
}
#[qjs(rename = "close")]
pub async fn close<'js>(&self, ctx: rquickjs::Ctx<'js>, options: Opt<rquickjs::Value<'js>>) -> rquickjs::Result<()> {
let opts = parse_page_close_options(&ctx, options)?;
self.inner.close(opts).await.into_js()
}
#[qjs(rename = "setDefaultTimeout")]
pub fn set_default_timeout(&self, ms: u64) {
self.inner.set_default_timeout(ms);
}
#[qjs(rename = "setDefaultNavigationTimeout")]
pub fn set_default_navigation_timeout(&self, ms: u64) {
self.inner.set_default_navigation_timeout(ms);
}
#[qjs(rename = "isClosed")]
pub fn is_closed(&self) -> bool {
self.inner.is_closed()
}
#[qjs(rename = "route")]
pub async fn route<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
url: rquickjs::Value<'js>,
handler: rquickjs::Function<'js>,
options: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<rquickjs::Value<'js>> {
let times = parse_route_times(&options)?;
let async_ctx = self.async_ctx.clone().ok_or_else(|| {
rquickjs::Error::new_from_js_message(
"page.route",
"Error",
"page.route requires the script engine's AsyncContext (install_page)".to_string(),
)
})?;
let id = self.next_route_id.fetch_add(1, Ordering::Relaxed);
let saved_handler = rquickjs::Persistent::save(&ctx, handler);
with_page_callbacks(&ctx, |r| r.route_handlers.insert(id, saved_handler))?;
let has_predicate = url.as_function().is_some();
let matcher = if let Some(pred) = url.as_function() {
let saved_pred = rquickjs::Persistent::save(&ctx, pred.clone());
with_page_callbacks(&ctx, |r| r.route_preds.insert(id, saved_pred))?;
let m = ferridriver::url_matcher::UrlMatcher::predicate(|_| true);
self
.route_matchers
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(id, m.clone());
m
} else {
url_value_to_matcher(&ctx, url)?
};
let rust_handler: ferridriver::route::RouteHandler = std::sync::Arc::new(move |route| {
let async_ctx = async_ctx.clone();
tokio::spawn(async move {
use rquickjs::class::Class;
let _: rquickjs::Result<()> = rquickjs::async_with!(async_ctx => |ctx| {
if has_predicate {
let pred = with_page_callbacks(&ctx, |r| r.route_preds.get(&id).cloned())?
.ok_or_else(|| rquickjs::Error::new_from_js_message("page.route", "Error", "route predicate gone".to_string()))?
.restore(&ctx)?;
let url_ctor: rquickjs::function::Constructor<'_> = ctx.globals().get("URL")?;
let url_obj: rquickjs::Value<'_> = url_ctor.construct((route.request().url.clone(),))?;
if !call_predicate_truthy(&pred, url_obj, &ctx).await? {
route.continue_route(ferridriver::route::ContinueOverrides::default());
return Ok(());
}
}
let f = with_page_callbacks(&ctx, |r| r.route_handlers.get(&id).cloned())?
.ok_or_else(|| rquickjs::Error::new_from_js_message("page.route", "Error", "route handler gone".to_string()))?
.restore(&ctx)?;
let route_class = Class::instance(ctx.clone(), crate::bindings::network::RouteJs::new(route))?;
let _: rquickjs::Value<'_> = f.call((route_class,))?;
Ok(())
})
.await;
});
});
let disposable = self.inner.route(matcher, rust_handler, times).await.into_js()?;
let instance =
rquickjs::class::Class::instance(ctx.clone(), crate::bindings::disposable::DisposableJs::new(disposable))?;
rquickjs::IntoJs::into_js(instance, &ctx)
}
#[qjs(rename = "routeFromHAR")]
pub async fn route_from_har(
&self,
har: String,
options: rquickjs::function::Opt<rquickjs::Value<'_>>,
) -> rquickjs::Result<()> {
let opts = parse_har_options(&options)?;
self
.inner
.route_from_har(std::path::Path::new(&har), opts)
.await
.into_js()
}
#[qjs(rename = "unroute")]
pub async fn unroute<'js>(&self, ctx: rquickjs::Ctx<'js>, url: rquickjs::Value<'js>) -> rquickjs::Result<()> {
if let Some(pred) = url.as_function() {
let saved: Vec<(u64, rquickjs::Persistent<rquickjs::Function<'static>>)> =
with_page_callbacks(&ctx, |r| r.route_preds.iter().map(|(k, v)| (*k, v.clone())).collect())?;
let mut victims: Vec<u64> = Vec::new();
for (id, sp) in saved {
let stored = sp.restore(&ctx)?;
if stored.as_value() == pred.as_value() {
victims.push(id);
}
}
for id in victims {
let m = self
.route_matchers
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&id);
if let Some(m) = m {
self.inner.unroute(&m).await.into_js()?;
}
with_page_callbacks(&ctx, |r| {
r.route_preds.remove(&id);
r.route_handlers.remove(&id);
})?;
}
return Ok(());
}
let matcher = url_value_to_matcher(&ctx, url)?;
self.inner.unroute(&matcher).await.into_js()
}
#[qjs(rename = "unrouteAll")]
pub async fn unroute_all<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
options: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<()> {
let behavior = match options.0.and_then(rquickjs::Value::into_object) {
Some(obj) => match obj.get::<_, Option<String>>("behavior")? {
Some(b) => Some(parse_unroute_behavior(&b)?),
None => None,
},
None => None,
};
self.inner.unroute_all(behavior).await.into_js()?;
self
.route_matchers
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
with_page_callbacks(&ctx, |r| {
r.route_preds.clear();
r.route_handlers.clear();
})?;
Ok(())
}
#[qjs(rename = "addLocatorHandler")]
pub fn add_locator_handler(
&self,
_locator: rquickjs::Class<'_, LocatorJs>,
_handler: rquickjs::Function<'_>,
_options: Opt<rquickjs::Value<'_>>,
) -> rquickjs::Result<()> {
ferridriver::error::Result::<()>::Err(ferridriver::error::FerriError::unsupported(
"page.addLocatorHandler is not available in the QuickJS scripting engine \
(handlers cannot fire during an in-VM action without deadlocking the \
single-threaded VM); use the NAPI/core API for locator handlers",
))
.into_js()
}
#[qjs(rename = "removeLocatorHandler")]
pub fn remove_locator_handler<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
locator: rquickjs::Class<'js, LocatorJs>,
) -> rquickjs::Result<()> {
let core_locator = locator.borrow().inner_ref().clone();
self.inner.remove_locator_handler(&core_locator);
let ids = self
.locator_handler_ids
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(core_locator.selector())
.unwrap_or_default();
with_page_callbacks(&ctx, |r| {
for id in ids {
r.remove_locator_handler(id);
}
})?;
Ok(())
}
#[qjs(rename = "pickLocator")]
pub async fn pick_locator(&self) -> rquickjs::Result<LocatorJs> {
let loc = self.inner.pick_locator().await.into_js()?;
Ok(LocatorJs::new(loc))
}
#[qjs(rename = "cancelPickLocator")]
pub async fn cancel_pick_locator(&self) -> rquickjs::Result<()> {
self.inner.cancel_pick_locator().await.into_js()
}
#[qjs(rename = "hideHighlight")]
pub async fn hide_highlight(&self) -> rquickjs::Result<()> {
self.inner.hide_highlight().await.into_js()
}
#[qjs(rename = "waitForRequest")]
pub async fn wait_for_request<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
url: rquickjs::Value<'js>,
timeout_ms: Opt<f64>,
) -> rquickjs::Result<crate::bindings::network::RequestJs> {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let timeout = timeout_ms.0.map(|t| t as u64);
if let Some(pred) = url.as_function() {
let t = timeout.unwrap_or_else(|| self.inner.default_timeout());
return wait_request_predicate(ctx.clone(), self.inner.clone(), pred.clone(), t).await;
}
let matcher = url_value_to_matcher(&ctx, url)?;
let req = self.inner.wait_for_request(matcher, timeout).await.into_js()?;
Ok(crate::bindings::network::RequestJs::new_with_page(
req,
self.inner.clone(),
))
}
#[qjs(rename = "waitForResponse")]
pub async fn wait_for_response<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
url: rquickjs::Value<'js>,
timeout_ms: Opt<f64>,
) -> rquickjs::Result<crate::bindings::network::ResponseJs> {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let timeout = timeout_ms.0.map(|t| t as u64);
if let Some(pred) = url.as_function() {
let t = timeout.unwrap_or_else(|| self.inner.default_timeout());
return wait_response_predicate(ctx.clone(), self.inner.clone(), pred.clone(), t).await;
}
let matcher = url_value_to_matcher(&ctx, url)?;
let resp = self.inner.wait_for_response(matcher, timeout).await.into_js()?;
Ok(crate::bindings::network::ResponseJs::new_with_page(
resp,
self.inner.clone(),
))
}
#[qjs(rename = "waitForLoadState")]
pub async fn wait_for_load_state(&self, state: Opt<String>) -> rquickjs::Result<()> {
use crate::bindings::convert::FerriResultExt;
self.inner.wait_for_load_state(state.0.as_deref()).await.into_js()
}
#[qjs(rename = "waitForURL")]
pub async fn wait_for_url<'js>(&self, ctx: rquickjs::Ctx<'js>, url: rquickjs::Value<'js>) -> rquickjs::Result<()> {
use crate::bindings::convert::FerriResultExt;
let matcher = url_value_to_matcher(&ctx, url)?;
self.inner.wait_for_url(matcher).await.into_js()
}
#[qjs(rename = "waitForFunction")]
pub async fn wait_for_function<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
page_function: rquickjs::Value<'js>,
_arg: Opt<rquickjs::Value<'js>>,
options: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<rquickjs::Value<'js>> {
#[derive(serde::Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsOpts {
timeout: Option<u64>,
}
let opts: JsOpts = match options.0 {
Some(v) if !v.is_undefined() && !v.is_null() => crate::bindings::convert::serde_from_js(&ctx, v)?,
_ => JsOpts::default(),
};
let (src, is_fn) = crate::bindings::convert::extract_page_function(&ctx, page_function)?;
let expr = if is_fn.unwrap_or(false) {
format!("({src})()")
} else {
src
};
let v = self
.inner
.wait_for_function(&expr, opts.timeout)
.await
.map_err(|e| crate::bindings::convert::to_rq_error(&e))?;
crate::bindings::convert::json_to_js(&ctx, &v)
}
#[qjs(rename = "waitForEvent")]
pub async fn wait_for_event<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
event: String,
timeout_ms: Opt<f64>,
) -> rquickjs::Result<rquickjs::Value<'js>> {
use rquickjs::class::Class;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let timeout = timeout_ms.0.unwrap_or(30_000.0) as u64;
let event_lc = event.to_ascii_lowercase();
if event_lc == "dialog" {
let dialog = self
.inner
.wait_for_dialog(timeout)
.await
.map_err(|e| rquickjs::Error::new_from_js_message("Page.waitForEvent", "Error", e.to_string()))?;
let wrapper = crate::bindings::dialog::DialogJs::new(dialog);
let instance = Class::instance(ctx.clone(), wrapper)?;
return rquickjs::IntoJs::into_js(instance, &ctx);
}
if event_lc == "filechooser" {
let chooser = self
.inner
.wait_for_file_chooser(timeout)
.await
.map_err(|e| rquickjs::Error::new_from_js_message("Page.waitForEvent", "Error", e.to_string()))?;
let wrapper = crate::bindings::file_chooser::FileChooserJs::new(chooser);
let instance = Class::instance(ctx.clone(), wrapper)?;
return rquickjs::IntoJs::into_js(instance, &ctx);
}
if event_lc == "download" {
let download = self
.inner
.wait_for_download(timeout)
.await
.map_err(|e| rquickjs::Error::new_from_js_message("Page.waitForEvent", "Error", e.to_string()))?;
let wrapper = crate::bindings::download::DownloadJs::new(download);
let instance = Class::instance(ctx.clone(), wrapper)?;
return rquickjs::IntoJs::into_js(instance, &ctx);
}
let name = event_lc.clone();
let ev = self
.inner
.events()
.wait_for(move |e| match_event_name(&name, e), timeout)
.await
.map_err(|e| rquickjs::Error::new_from_js_message("Page.waitForEvent", "Error", e.to_string()))?;
match ev {
ferridriver::events::PageEvent::WebSocket(ws) => {
let wrapper = crate::bindings::network::WebSocketJs::new(ws);
let instance = Class::instance(ctx.clone(), wrapper)?;
rquickjs::IntoJs::into_js(instance, &ctx)
},
ferridriver::events::PageEvent::Request(req)
| ferridriver::events::PageEvent::RequestFinished(req)
| ferridriver::events::PageEvent::RequestFailed(req) => {
let wrapper = crate::bindings::network::RequestJs::new_with_page(req, self.inner.clone());
let instance = Class::instance(ctx.clone(), wrapper)?;
rquickjs::IntoJs::into_js(instance, &ctx)
},
ferridriver::events::PageEvent::Response(resp) => {
let wrapper = crate::bindings::network::ResponseJs::new_with_page(resp, self.inner.clone());
let instance = Class::instance(ctx.clone(), wrapper)?;
rquickjs::IntoJs::into_js(instance, &ctx)
},
ferridriver::events::PageEvent::Dialog(dialog) => {
let wrapper = crate::bindings::dialog::DialogJs::new(dialog);
let instance = Class::instance(ctx.clone(), wrapper)?;
rquickjs::IntoJs::into_js(instance, &ctx)
},
ferridriver::events::PageEvent::FileChooser(chooser) => {
let wrapper = crate::bindings::file_chooser::FileChooserJs::new(chooser);
let instance = Class::instance(ctx.clone(), wrapper)?;
rquickjs::IntoJs::into_js(instance, &ctx)
},
ferridriver::events::PageEvent::Download(download) => {
let wrapper = crate::bindings::download::DownloadJs::new(download);
let instance = Class::instance(ctx.clone(), wrapper)?;
rquickjs::IntoJs::into_js(instance, &ctx)
},
ferridriver::events::PageEvent::Console(msg) => {
let wrapper = crate::bindings::console_message::ConsoleMessageJs::new(msg);
let instance = Class::instance(ctx.clone(), wrapper)?;
rquickjs::IntoJs::into_js(instance, &ctx)
},
ferridriver::events::PageEvent::PageError(err) => {
crate::bindings::web_error::build_native_error(&ctx, err.error())
},
other => page_event_to_js(&ctx, &other),
}
}
#[qjs(rename = "mainFrame")]
pub fn main_frame(&self) -> crate::bindings::frame::FrameJs {
crate::bindings::frame::FrameJs::new(self.inner.main_frame())
}
#[qjs(rename = "frames")]
pub fn frames(&self) -> Vec<crate::bindings::frame::FrameJs> {
self
.inner
.frames()
.into_iter()
.map(crate::bindings::frame::FrameJs::new)
.collect()
}
#[qjs(rename = "frameLocator")]
pub fn frame_locator(&self, selector: String) -> crate::bindings::frame_locator::FrameLocatorJs {
crate::bindings::frame_locator::FrameLocatorJs::new(self.inner.frame_locator(&selector))
}
#[qjs(rename = "frame")]
pub fn frame<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
selector: rquickjs::Value<'js>,
) -> rquickjs::Result<Option<crate::bindings::frame::FrameJs>> {
let core_sel = if let Some(s) = selector.as_string() {
ferridriver::options::FrameSelector::by_name(s.to_string()?)
} else if let Some(obj) = selector.as_object() {
let read = |key: &str| -> rquickjs::Result<Option<String>> {
let v: rquickjs::Value<'_> = obj
.get(key)
.unwrap_or_else(|_| rquickjs::Value::new_undefined(ctx.clone()));
if v.is_undefined() || v.is_null() {
Ok(None)
} else if let Some(s) = v.as_string() {
Ok(Some(s.to_string()?))
} else {
Ok(None)
}
};
ferridriver::options::FrameSelector {
name: read("name")?,
url: read("url")?,
}
} else {
return Ok(None);
};
if core_sel.is_empty() {
return Ok(None);
}
Ok(self.inner.frame(core_sel).map(crate::bindings::frame::FrameJs::new))
}
#[qjs(rename = "touchscreen", get)]
pub fn touchscreen(&self) -> TouchscreenJs {
TouchscreenJs {
page: self.inner.clone(),
}
}
#[qjs(rename = "snapshotForAI")]
pub async fn snapshot_for_ai<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
options: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<rquickjs::Value<'js>> {
let core_opts = match options.0 {
None => ferridriver::snapshot::SnapshotOptions::default(),
Some(v) if v.is_undefined() || v.is_null() => ferridriver::snapshot::SnapshotOptions::default(),
Some(v) => {
#[derive(serde::Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsSnap {
depth: Option<i32>,
track: Option<String>,
}
let parsed: JsSnap = crate::bindings::convert::serde_from_js(&ctx, v)?;
ferridriver::snapshot::SnapshotOptions {
depth: parsed.depth,
track: parsed.track,
}
},
};
let snap = self.inner.snapshot_for_ai(core_opts).await.into_js()?;
let obj = rquickjs::Object::new(ctx.clone())?;
obj.set("full", snap.full)?;
if let Some(inc) = snap.incremental {
obj.set("incremental", inc)?;
}
let ref_map = rquickjs::Object::new(ctx.clone())?;
for (k, v) in snap.ref_map {
ref_map.set(k, v as f64)?;
}
obj.set("refMap", ref_map)?;
rquickjs::IntoJs::into_js(obj, &ctx)
}
#[qjs(rename = "ariaSnapshot")]
pub async fn aria_snapshot<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
options: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<String> {
let core_opts = match options.0 {
Some(v) if !v.is_undefined() && !v.is_null() => {
#[derive(serde::Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsSnap {
depth: Option<i32>,
track: Option<String>,
}
let p: JsSnap = crate::bindings::convert::serde_from_js(&ctx, v)?;
ferridriver::snapshot::SnapshotOptions {
depth: p.depth,
track: p.track,
}
},
_ => ferridriver::snapshot::SnapshotOptions::default(),
};
self.inner.aria_snapshot(core_opts).await.into_js()
}
#[qjs(rename = "exposeFunction")]
pub async fn expose_function<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
name: String,
callback: rquickjs::Function<'js>,
) -> rquickjs::Result<()> {
let async_ctx = self.async_ctx.clone().ok_or_else(|| {
rquickjs::Error::new_from_js_message(
"page.exposeFunction",
"Error",
"page.exposeFunction requires the script engine's AsyncContext (install_page)".to_string(),
)
})?;
let saved = rquickjs::Persistent::save(&ctx, callback);
with_page_callbacks(&ctx, |r| r.exposed.insert(name.clone(), saved))?;
let cb: ferridriver::events::ExposedFn = std::sync::Arc::new({
let name = name.clone();
move |args: Vec<serde_json::Value>| {
let async_ctx = async_ctx.clone();
let name = name.clone();
Box::pin(async move {
let out: rquickjs::Result<serde_json::Value> = rquickjs::async_with!(async_ctx => |ctx| {
let f = with_page_callbacks(&ctx, |r| r.exposed.get(&name).cloned())?
.ok_or_else(|| {
rquickjs::Error::new_from_js_message(
"page.exposeFunction",
"Error",
"exposed callback gone".to_string(),
)
})?
.restore(&ctx)?;
let mut call_args = rquickjs::function::Args::new_unsized(ctx.clone());
for v in args {
call_args.push_arg(crate::bindings::convert::json_to_js(&ctx, &v)?)?;
}
let mp: rquickjs::promise::MaybePromise<'_> = call_args.apply(&f)?;
let res = mp.into_future::<rquickjs::Value<'_>>().await?;
let json = match ctx.json_stringify(res)? {
Some(s) => serde_json::from_str(&s.to_string()?).unwrap_or(serde_json::Value::Null),
None => serde_json::Value::Null,
};
Ok(json)
})
.await;
out.unwrap_or(serde_json::Value::Null)
})
}
});
self.inner.expose_function(&name, cb).await.into_js()
}
#[qjs(rename = "startScreencast")]
pub async fn start_screencast<'js>(
&self,
ctx: rquickjs::Ctx<'js>,
quality: u8,
max_width: u32,
max_height: u32,
callback: rquickjs::Function<'js>,
) -> rquickjs::Result<()> {
let async_ctx = self.async_ctx.clone().ok_or_else(|| {
rquickjs::Error::new_from_js_message(
"page.startScreencast",
"Error",
"page.startScreencast requires the script engine's AsyncContext (install_page)".to_string(),
)
})?;
let saved = rquickjs::Persistent::save(&ctx, callback);
with_page_callbacks(&ctx, |r| r.screencast = Some(saved))?;
let (mut rx, _shutdown) = self
.inner
.start_screencast(quality, max_width, max_height)
.await
.into_js()?;
tokio::spawn(async move {
while let Some((bytes, ts)) = rx.recv().await {
let _: rquickjs::Result<()> = rquickjs::async_with!(async_ctx => |ctx| {
let f = with_page_callbacks(&ctx, |r| r.screencast.clone())?
.ok_or_else(|| rquickjs::Error::new_from_js_message("page.startScreencast", "Error", "screencast callback gone".to_string()))?
.restore(&ctx)?;
let payload = rquickjs::Object::new(ctx.clone())?;
let buf = rquickjs::TypedArray::<u8>::new(ctx.clone(), bytes)?;
payload.set("frame", buf)?;
payload.set("timestamp", ts)?;
let _: rquickjs::Value<'_> = f.call((payload,))?;
Ok(())
})
.await;
}
});
Ok(())
}
#[qjs(rename = "stopScreencast")]
pub async fn stop_screencast(&self) -> rquickjs::Result<()> {
self.inner.stop_screencast().await.into_js()
}
}
#[derive(rquickjs::JsLifetime, rquickjs::class::Trace)]
#[rquickjs::class(rename = "Touchscreen")]
pub struct TouchscreenJs {
#[qjs(skip_trace)]
page: std::sync::Arc<ferridriver::Page>,
}
#[rquickjs::methods]
impl TouchscreenJs {
#[qjs(rename = "tap")]
pub async fn tap(&self, x: f64, y: f64) -> rquickjs::Result<()> {
self.page.touchscreen().tap(x, y).await.into_js()
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
struct JsScreenshotOptions {
animations: Option<String>,
caret: Option<String>,
clip: Option<JsClipRect>,
full_page: Option<bool>,
#[serde(rename = "type")]
format: Option<String>,
#[serde(skip)]
_mask_placeholder: (),
mask_color: Option<String>,
omit_background: Option<bool>,
path: Option<String>,
quality: Option<i64>,
scale: Option<String>,
style: Option<String>,
timeout: Option<u64>,
}
#[derive(Debug, Default, Deserialize, Clone, Copy)]
struct JsClipRect {
x: f64,
y: f64,
width: f64,
height: f64,
}
impl From<JsClipRect> for ferridriver::options::ClipRect {
fn from(c: JsClipRect) -> Self {
Self {
x: c.x,
y: c.y,
width: c.width,
height: c.height,
}
}
}
fn parse_mask_locators<'js>(obj: &rquickjs::Object<'js>) -> rquickjs::Result<Vec<ferridriver::Locator>> {
let v: rquickjs::Value<'js> = obj.get("mask")?;
if v.is_undefined() || v.is_null() {
return Ok(Vec::new());
}
let arr = v.into_array().ok_or_else(|| {
rquickjs::Error::new_from_js_message("screenshot options", "mask", "expected an array of Locator")
})?;
let mut out = Vec::with_capacity(arr.len());
for item in arr.iter::<rquickjs::Value<'js>>() {
let item = item?;
if let Ok(class) = rquickjs::Class::<LocatorJs>::from_value(&item) {
out.push(class.borrow().inner_ref().clone());
} else {
return Err(rquickjs::Error::new_from_js_message(
"screenshot options",
"mask",
"each mask entry must be a Locator instance",
));
}
}
Ok(out)
}
fn parse_screenshot_options<'js>(
ctx: &rquickjs::Ctx<'js>,
value: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<ferridriver::options::ScreenshotOptions> {
match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => {
let mask = match v.as_object() {
Some(obj) => parse_mask_locators(obj)?,
None => Vec::new(),
};
let js: JsScreenshotOptions = serde_from_js(ctx, v)?;
Ok(ferridriver::options::ScreenshotOptions {
animations: js.animations,
caret: js.caret,
clip: js.clip.map(Into::into),
full_page: js.full_page,
format: js.format,
mask,
mask_color: js.mask_color,
omit_background: js.omit_background,
path: js.path.map(std::path::PathBuf::from),
quality: js.quality,
scale: js.scale,
style: js.style,
timeout: js.timeout,
})
},
_ => Ok(ferridriver::options::ScreenshotOptions::default()),
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
struct JsPdfOptions {
format: Option<String>,
landscape: Option<bool>,
print_background: Option<bool>,
scale: Option<f64>,
display_header_footer: Option<bool>,
header_template: Option<String>,
footer_template: Option<String>,
page_ranges: Option<String>,
prefer_css_page_size: Option<bool>,
outline: Option<bool>,
tagged: Option<bool>,
}
fn parse_pdf_options<'js>(
ctx: &rquickjs::Ctx<'js>,
value: Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<ferridriver::options::PdfOptions> {
match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => {
let js: JsPdfOptions = serde_from_js(ctx, v)?;
Ok(ferridriver::options::PdfOptions {
format: js.format,
path: None,
scale: js.scale,
display_header_footer: js.display_header_footer,
header_template: js.header_template,
footer_template: js.footer_template,
print_background: js.print_background,
landscape: js.landscape,
page_ranges: js.page_ranges,
width: None,
height: None,
margin: None,
prefer_css_page_size: js.prefer_css_page_size,
outline: js.outline,
tagged: js.tagged,
})
},
_ => Ok(ferridriver::options::PdfOptions::default()),
}
}
fn match_event_name(name: &str, ev: &ferridriver::events::PageEvent) -> bool {
use ferridriver::events::PageEvent;
matches!(
(name, ev),
("console", PageEvent::Console(_))
| ("request", PageEvent::Request(_))
| ("response", PageEvent::Response(_))
| ("requestfinished", PageEvent::RequestFinished(_))
| ("requestfailed", PageEvent::RequestFailed(_))
| ("websocket", PageEvent::WebSocket(_))
| ("dialog", PageEvent::Dialog(_))
| ("filechooser", PageEvent::FileChooser(_))
| ("frameattached", PageEvent::FrameAttached(_))
| ("framedetached", PageEvent::FrameDetached { .. })
| ("framenavigated", PageEvent::FrameNavigated(_))
| ("load", PageEvent::Load)
| ("domcontentloaded", PageEvent::DomContentLoaded)
| ("close", PageEvent::Close)
| ("pageerror", PageEvent::PageError(_))
| ("download", PageEvent::Download(_))
)
}
fn page_event_to_js<'js>(
ctx: &rquickjs::Ctx<'js>,
ev: &ferridriver::events::PageEvent,
) -> rquickjs::Result<rquickjs::Value<'js>> {
use ferridriver::events::PageEvent;
let obj = || rquickjs::Object::new(ctx.clone());
match ev {
PageEvent::Console(msg) => {
let loc = msg.location();
let o = obj()?;
o.set("type", msg.type_str())?;
o.set("text", msg.text())?;
let l = obj()?;
l.set("url", loc.url.as_str())?;
l.set("lineNumber", f64::from(loc.line_number))?;
l.set("columnNumber", f64::from(loc.column_number))?;
o.set("location", l)?;
o.set("timestamp", msg.timestamp())?;
o.set("argsCount", msg.args().len() as f64)?;
Ok(o.into_value())
},
PageEvent::Dialog(d) => {
let o = obj()?;
o.set("type", d.dialog_type().as_str())?;
o.set("message", d.message())?;
o.set("defaultValue", d.default_value())?;
Ok(o.into_value())
},
PageEvent::FileChooser(fc) => {
let o = obj()?;
o.set("isMultiple", fc.is_multiple())?;
Ok(o.into_value())
},
PageEvent::FrameAttached(f) | PageEvent::FrameNavigated(f) => crate::bindings::convert::serde_to_js(ctx, f),
PageEvent::FrameDetached { frame_id } => {
let o = obj()?;
o.set("frameId", frame_id.as_str())?;
Ok(o.into_value())
},
PageEvent::Download(d) => {
let o = obj()?;
o.set("url", d.url())?;
o.set("suggestedFilename", d.suggested_filename())?;
Ok(o.into_value())
},
PageEvent::Load => {
let o = obj()?;
o.set("type", "load")?;
Ok(o.into_value())
},
PageEvent::DomContentLoaded => {
let o = obj()?;
o.set("type", "domcontentloaded")?;
Ok(o.into_value())
},
PageEvent::Close => {
let o = obj()?;
o.set("type", "close")?;
Ok(o.into_value())
},
PageEvent::PageError(err) => {
let details = err.error();
let o = obj()?;
o.set("name", details.name.as_str())?;
o.set("message", details.message.as_str())?;
o.set("stack", details.stack.as_str())?;
Ok(o.into_value())
},
_ => Ok(rquickjs::Value::new_null(ctx.clone())),
}
}
fn js_truthy(v: &rquickjs::Value<'_>) -> bool {
if v.is_undefined() || v.is_null() {
return false;
}
if let Some(b) = v.as_bool() {
return b;
}
if let Some(i) = v.as_int() {
return i != 0;
}
if let Some(f) = v.as_float() {
return f != 0.0 && !f.is_nan();
}
if let Some(s) = v.as_string() {
return !s.to_string().unwrap_or_default().is_empty();
}
true
}
pub(crate) async fn call_predicate_truthy<'js>(
pred: &rquickjs::Function<'js>,
arg: impl rquickjs::IntoJs<'js>,
ctx: &rquickjs::Ctx<'js>,
) -> rquickjs::Result<bool> {
let arg = arg.into_js(ctx)?;
let mp: rquickjs::promise::MaybePromise<'js> = pred.call((arg,))?;
let v: rquickjs::Value<'js> = mp.into_future().await?;
Ok(js_truthy(&v))
}
async fn wait_request_predicate<'js>(
ctx: rquickjs::Ctx<'js>,
page: Arc<Page>,
pred: rquickjs::Function<'js>,
timeout_ms: u64,
) -> rquickjs::Result<crate::bindings::network::RequestJs> {
use ferridriver::events::PageEvent;
use rquickjs::class::Class;
let mut rx = page.events().subscribe();
let deadline = tokio::time::Instant::now() + std::time::Duration::from_millis(timeout_ms);
loop {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
return Err(rquickjs::Error::new_from_js_message(
"page.waitForRequest",
"TimeoutError",
format!("Timeout {timeout_ms}ms exceeded while waiting for request"),
));
}
match tokio::time::timeout(remaining, rx.recv()).await {
Ok(Ok(PageEvent::Request(req))) => {
let probe = crate::bindings::network::RequestJs::new_with_page(req.clone(), page.clone());
let inst = Class::instance(ctx.clone(), probe)?;
if call_predicate_truthy(&pred, inst, &ctx).await? {
return Ok(crate::bindings::network::RequestJs::new_with_page(req, page.clone()));
}
},
Ok(Ok(_) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => {},
Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => {
return Err(rquickjs::Error::new_from_js_message(
"page.waitForRequest",
"Error",
"page closed while waiting for request".to_string(),
));
},
Err(_) => {
return Err(rquickjs::Error::new_from_js_message(
"page.waitForRequest",
"TimeoutError",
format!("Timeout {timeout_ms}ms exceeded while waiting for request"),
));
},
}
}
}
async fn wait_response_predicate<'js>(
ctx: rquickjs::Ctx<'js>,
page: Arc<Page>,
pred: rquickjs::Function<'js>,
timeout_ms: u64,
) -> rquickjs::Result<crate::bindings::network::ResponseJs> {
use ferridriver::events::PageEvent;
use rquickjs::class::Class;
let mut rx = page.events().subscribe();
let deadline = tokio::time::Instant::now() + std::time::Duration::from_millis(timeout_ms);
loop {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
return Err(rquickjs::Error::new_from_js_message(
"page.waitForResponse",
"TimeoutError",
format!("Timeout {timeout_ms}ms exceeded while waiting for response"),
));
}
match tokio::time::timeout(remaining, rx.recv()).await {
Ok(Ok(PageEvent::Response(resp))) => {
let probe = crate::bindings::network::ResponseJs::new_with_page(resp.clone(), page.clone());
let inst = Class::instance(ctx.clone(), probe)?;
if call_predicate_truthy(&pred, inst, &ctx).await? {
return Ok(crate::bindings::network::ResponseJs::new_with_page(resp, page.clone()));
}
},
Ok(Ok(_) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => {},
Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => {
return Err(rquickjs::Error::new_from_js_message(
"page.waitForResponse",
"Error",
"page closed while waiting for response".to_string(),
));
},
Err(_) => {
return Err(rquickjs::Error::new_from_js_message(
"page.waitForResponse",
"TimeoutError",
format!("Timeout {timeout_ms}ms exceeded while waiting for response"),
));
},
}
}
}
pub(crate) fn url_value_to_matcher<'js>(
ctx: &rquickjs::Ctx<'js>,
value: rquickjs::Value<'js>,
) -> rquickjs::Result<ferridriver::url_matcher::UrlMatcher> {
use crate::bindings::convert::FerriResultExt;
if let Some(s) = value.as_string() {
let glob = s.to_string()?;
return ferridriver::url_matcher::UrlMatcher::glob(glob).into_js();
}
if let Some(obj) = value.as_object() {
let source: rquickjs::Result<String> = obj.get("source");
let flags: rquickjs::Result<String> = obj.get("flags");
if let (Ok(source), Ok(flags)) = (source, flags) {
return ferridriver::url_matcher::UrlMatcher::regex_from_source(&source, &flags).into_js();
}
}
let _ = ctx;
Err(rquickjs::Error::new_from_js_message(
"Page.waitFor*",
"url",
"expected string | RegExp".to_string(),
))
}
pub(crate) fn string_or_regex_from_js(
value: rquickjs::Value<'_>,
) -> rquickjs::Result<ferridriver::options::StringOrRegex> {
if let Some(s) = value.as_string() {
return Ok(ferridriver::options::StringOrRegex::String(s.to_string()?));
}
if let Some(obj) = value.as_object() {
let source: rquickjs::Result<String> = obj.get("source");
let flags: rquickjs::Result<String> = obj.get("flags");
if let (Ok(source), Ok(flags)) = (source, flags) {
return Ok(ferridriver::options::StringOrRegex::Regex { source, flags });
}
}
Err(rquickjs::Error::new_from_js_message(
"getBy*",
"text",
"expected string | RegExp".to_string(),
))
}
pub(crate) fn parse_text_options(
value: rquickjs::function::Opt<rquickjs::Value<'_>>,
) -> ferridriver::options::TextOptions {
let Some(v) = value.0 else {
return ferridriver::options::TextOptions::default();
};
if v.is_undefined() || v.is_null() {
return ferridriver::options::TextOptions::default();
}
let Some(obj) = v.as_object() else {
return ferridriver::options::TextOptions::default();
};
let exact: Option<bool> = obj.get("exact").ok();
ferridriver::options::TextOptions { exact }
}
pub(crate) fn parse_role_options<'js>(
value: rquickjs::function::Opt<rquickjs::Value<'js>>,
) -> rquickjs::Result<ferridriver::options::RoleOptions> {
let Some(v) = value.0 else {
return Ok(ferridriver::options::RoleOptions::default());
};
if v.is_undefined() || v.is_null() {
return Ok(ferridriver::options::RoleOptions::default());
}
let Some(obj) = v.as_object() else {
return Ok(ferridriver::options::RoleOptions::default());
};
let name_val: Option<rquickjs::Value<'js>> = obj.get("name").ok();
let name = match name_val {
Some(val) if !val.is_undefined() && !val.is_null() => Some(string_or_regex_from_js(val)?),
_ => None,
};
let exact: Option<bool> = obj.get("exact").ok();
let checked: Option<bool> = obj.get("checked").ok();
let disabled: Option<bool> = obj.get("disabled").ok();
let expanded: Option<bool> = obj.get("expanded").ok();
let level: Option<i32> = obj.get("level").ok();
let pressed: Option<bool> = obj.get("pressed").ok();
let selected: Option<bool> = obj.get("selected").ok();
let include_hidden: Option<bool> = obj.get("includeHidden").ok();
Ok(ferridriver::options::RoleOptions {
name,
exact,
checked,
disabled,
expanded,
level,
pressed,
selected,
include_hidden,
})
}