use ferridriver::FerriError;
use rquickjs::object::Property;
use rquickjs::{Ctx, Function, Object, Value};
use serde::Serialize;
use serde::de::DeserializeOwned;
pub fn to_rq_error(err: &FerriError) -> rquickjs::Error {
rquickjs::Error::new_from_js_message("ferridriver", err.name(), err.to_string())
}
#[must_use]
pub fn ms_f64_to_u64(ms: f64) -> u64 {
if ms <= 0.0 {
return 0;
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
{
ms.min(u64::MAX as f64) as u64
}
}
pub trait FerriResultExt<T> {
fn into_js(self) -> rquickjs::Result<T>;
}
impl<T> FerriResultExt<T> for Result<T, FerriError> {
fn into_js(self) -> rquickjs::Result<T> {
self.map_err(|e| to_rq_error(&e))
}
}
pub fn serde_to_js<'js, T: Serialize>(ctx: &Ctx<'js>, value: &T) -> rquickjs::Result<Value<'js>> {
rquickjs_serde::to_value(ctx.clone(), value)
.map_err(|e| rquickjs::Error::new_from_js_message("serde", "serialize", e.to_string()))
}
pub fn name_value_array_to_js<'js, S: AsRef<str>>(ctx: &Ctx<'js>, pairs: &[(S, S)]) -> rquickjs::Result<Value<'js>> {
#[derive(Serialize)]
struct NameValue<'a> {
name: &'a str,
value: &'a str,
}
let view: Vec<NameValue<'_>> = pairs
.iter()
.map(|(n, v)| NameValue {
name: n.as_ref(),
value: v.as_ref(),
})
.collect();
serde_to_js(ctx, &view)
}
pub fn serde_from_js<'js, T: DeserializeOwned>(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<T> {
rquickjs_serde::from_value(value)
.map_err(|e| rquickjs::Error::new_from_js_message("serde", "deserialize", e.to_string()))
}
fn define_own<'js, V: rquickjs::IntoJs<'js>>(obj: &Object<'js>, key: &str, value: V) -> rquickjs::Result<()> {
obj.prop(key, Property::from(value).writable().enumerable().configurable())
}
pub(crate) fn json_to_js<'js>(ctx: &Ctx<'js>, v: &serde_json::Value) -> rquickjs::Result<Value<'js>> {
match v {
serde_json::Value::Null => Ok(Value::new_null(ctx.clone())),
serde_json::Value::Bool(b) => Ok(Value::new_bool(ctx.clone(), *b)),
serde_json::Value::Number(n) => {
let f = n.as_f64().unwrap_or(f64::NAN);
if let Some(i) = f64_as_exact_i32(f) {
Ok(Value::new_int(ctx.clone(), i))
} else {
Ok(Value::new_number(ctx.clone(), f))
}
},
serde_json::Value::String(s) => Ok(rquickjs::String::from_str(ctx.clone(), s)?.into_value()),
serde_json::Value::Array(items) => {
let arr = rquickjs::Array::new(ctx.clone())?;
for (i, item) in items.iter().enumerate() {
arr.set(i, json_to_js(ctx, item)?)?;
}
Ok(arr.into_value())
},
serde_json::Value::Object(map) => {
let obj = Object::new(ctx.clone())?;
for (k, val) in map {
define_own(&obj, k.as_str(), json_to_js(ctx, val)?)?;
}
Ok(obj.into_value())
},
}
}
pub fn quickjs_arg_to_serialized<'js>(
_ctx: &Ctx<'js>,
value: Option<Value<'js>>,
) -> rquickjs::Result<ferridriver::protocol::SerializedArgument> {
use ferridriver::protocol::{SerializationContext, SerializedArgument, SerializedValue, SpecialValue};
let v = match value {
Some(v) if !v.is_undefined() => v,
_ => {
return Ok(SerializedArgument {
value: SerializedValue::Special(SpecialValue::Undefined),
handles: Vec::new(),
});
},
};
if v.is_null() {
return Ok(SerializedArgument {
value: SerializedValue::Special(SpecialValue::Null),
handles: Vec::new(),
});
}
let mut alloc = SerializationContext::default();
let mut handles = Vec::new();
Ok(SerializedArgument {
value: js_value_to_serialized(&v, &mut alloc, &mut handles, 0)?,
handles,
})
}
const MAX_ARG_DEPTH: u32 = 512;
fn js_value_to_serialized(
v: &Value<'_>,
alloc: &mut ferridriver::protocol::SerializationContext,
handles: &mut Vec<ferridriver::protocol::HandleId>,
depth: u32,
) -> rquickjs::Result<ferridriver::protocol::SerializedValue> {
use ferridriver::protocol::{SerializedValue, SpecialValue};
if depth > MAX_ARG_DEPTH {
return Err(rquickjs::Error::new_from_js_message(
"serde",
"serialize",
"argument too deeply nested or cyclic".to_string(),
));
}
if v.is_undefined() {
return Ok(SerializedValue::Special(SpecialValue::Undefined));
}
if v.is_null() {
return Ok(SerializedValue::Special(SpecialValue::Null));
}
if let Some(b) = v.as_bool() {
return Ok(SerializedValue::Bool(b));
}
if let Some(i) = v.as_int() {
return Ok(SerializedValue::from_f64(f64::from(i)));
}
if let Some(f) = v.as_float() {
return Ok(if f.is_finite() {
SerializedValue::from_f64(f)
} else {
SerializedValue::Special(SpecialValue::Null)
});
}
if let Some(s) = v.as_string() {
return Ok(SerializedValue::Str(s.to_string()?));
}
if let Some(bi) = v.as_big_int() {
return match bi.clone().to_i64() {
Ok(n) => Ok(SerializedValue::BigInt(n.to_string())),
Err(_) => Err(rquickjs::Error::new_from_js_message(
"serde",
"serialize",
"BigInt argument out of i64 range".to_string(),
)),
};
}
if let Some(arr) = v.as_array() {
let id = alloc.alloc_id();
let mut items = Vec::with_capacity(arr.len());
for idx in 0..arr.len() {
let el: Value<'_> = arr.get(idx)?;
items.push(if el.is_undefined() || el.is_function() {
SerializedValue::Special(SpecialValue::Null)
} else {
js_value_to_serialized(&el, alloc, handles, depth + 1)?
});
}
return Ok(SerializedValue::Array { id, items });
}
if let Some(obj) = v.as_object() {
let id = alloc.alloc_id();
let mut entries = Vec::new();
for key in obj.keys::<String>() {
let key = key?;
let val: Value<'_> = obj.get(&key)?;
if val.is_undefined() || val.is_function() || val.type_of() == rquickjs::Type::Symbol {
continue;
}
entries.push(ferridriver::protocol::PropertyEntry {
k: key,
v: js_value_to_serialized(&val, alloc, handles, depth + 1)?,
});
}
if entries.is_empty() {
if let Some(handle) = handle_value_to_serialized(v, handles)? {
return Ok(handle);
}
}
return Ok(SerializedValue::Object { id, entries });
}
Ok(SerializedValue::Special(SpecialValue::Undefined))
}
fn handle_value_to_serialized(
v: &Value<'_>,
handles: &mut Vec<ferridriver::protocol::HandleId>,
) -> rquickjs::Result<Option<ferridriver::protocol::SerializedValue>> {
if let Ok(class) = rquickjs::Class::<crate::bindings::js_handle::JSHandleJs>::from_value(v) {
let inner = class.borrow();
return Ok(Some(handle_backing_to_serialized(inner.inner().backing(), handles)?));
}
if let Ok(class) = rquickjs::Class::<crate::bindings::element_handle::ElementHandleJs>::from_value(v) {
let inner = class.borrow();
let handle = inner.inner().as_js_handle();
return Ok(Some(handle_backing_to_serialized(handle.backing(), handles)?));
}
Ok(None)
}
fn handle_backing_to_serialized(
backing: &ferridriver::js_handle::JSHandleBacking,
handles: &mut Vec<ferridriver::protocol::HandleId>,
) -> rquickjs::Result<ferridriver::protocol::SerializedValue> {
match backing {
ferridriver::js_handle::JSHandleBacking::Remote(remote) => {
let idx = u32::try_from(handles.len())
.map_err(|_| rquickjs::Error::new_from_js_message("serde", "serialize", "too many JS handles"))?;
handles.push(remote.to_handle_id());
Ok(ferridriver::protocol::SerializedValue::Handle(idx))
},
ferridriver::js_handle::JSHandleBacking::Value(value) => Ok(value.clone()),
}
}
pub fn serialized_value_to_quickjs<'js>(
ctx: &Ctx<'js>,
value: &ferridriver::protocol::SerializedValue,
) -> rquickjs::Result<Value<'js>> {
let mut refs: rustc_hash::FxHashMap<u32, Value<'js>> = rustc_hash::FxHashMap::default();
rehydrate(ctx, value, &mut refs)
}
fn rehydrate<'js>(
ctx: &Ctx<'js>,
value: &ferridriver::protocol::SerializedValue,
refs: &mut rustc_hash::FxHashMap<u32, Value<'js>>,
) -> rquickjs::Result<Value<'js>> {
use ferridriver::protocol::{ErrorValue, RegExpValue, SerializedValue, SpecialValue};
match value {
SerializedValue::Bool(b) => Ok(Value::new_bool(ctx.clone(), *b)),
SerializedValue::Number(n) => {
if let Some(i) = f64_as_exact_i32(*n) {
Ok(Value::new_int(ctx.clone(), i))
} else {
Ok(Value::new_number(ctx.clone(), *n))
}
},
SerializedValue::Str(s) => {
let js = rquickjs::String::from_str(ctx.clone(), s)?;
Ok(js.into_value())
},
SerializedValue::Special(SpecialValue::Null) => Ok(Value::new_null(ctx.clone())),
SerializedValue::Special(SpecialValue::Undefined) => Ok(Value::new_undefined(ctx.clone())),
SerializedValue::Special(SpecialValue::NaN) => Ok(Value::new_number(ctx.clone(), f64::NAN)),
SerializedValue::Special(SpecialValue::Infinity) => Ok(Value::new_number(ctx.clone(), f64::INFINITY)),
SerializedValue::Special(SpecialValue::NegInfinity) => Ok(Value::new_number(ctx.clone(), f64::NEG_INFINITY)),
SerializedValue::Special(SpecialValue::NegZero) => Ok(Value::new_number(ctx.clone(), -0.0)),
SerializedValue::Date(iso) => construct_global(ctx, "Date", (iso.clone(),)),
SerializedValue::Url(url) => construct_global(ctx, "URL", (url.clone(),)),
SerializedValue::BigInt(s) => {
let func: Function<'js> = ctx.globals().get("BigInt")?;
func.call((s.clone(),))
},
SerializedValue::RegExp(RegExpValue { p, f }) => construct_global(ctx, "RegExp", (p.clone(), f.clone())),
SerializedValue::Error(ErrorValue { m, n, s }) => {
let err: Value<'js> = construct_global(ctx, "Error", (m.clone(),))?;
let obj = err
.as_object()
.ok_or_else(|| rquickjs::Error::new_from_js_message("Error", "", "not an object"))?;
obj.set("name", n.clone())?;
obj.set("stack", s.clone())?;
Ok(err)
},
SerializedValue::TypedArray(ta) => rehydrate_typed_array(ctx, ta.k, &ta.b),
SerializedValue::ArrayBuffer(ab) => {
let len = u32::try_from(ab.b.len())
.map_err(|_| rquickjs::Error::new_from_js_message("rehydrate", "ArrayBuffer", "length exceeds u32"))?;
let buf: Value<'js> = construct_global(ctx, "ArrayBuffer", (len,))?;
let view: Value<'js> = construct_global(ctx, "Uint8Array", (buf.clone(),))?;
let view_obj = view
.as_object()
.ok_or_else(|| rquickjs::Error::new_from_js_message("ArrayBuffer", "", "view not an object"))?;
for (i, byte) in ab.b.iter().enumerate() {
view_obj.set(u32::try_from(i).unwrap_or(u32::MAX), *byte)?;
}
Ok(buf)
},
SerializedValue::Array { id, items } => {
let arr = rquickjs::Array::new(ctx.clone())?;
refs.insert(*id, arr.as_value().clone());
for (i, item) in items.iter().enumerate() {
let v = rehydrate(ctx, item, refs)?;
arr.set(i, v)?;
}
Ok(arr.into_value())
},
SerializedValue::Object { id, entries } => {
let obj = Object::new(ctx.clone())?;
refs.insert(*id, obj.as_value().clone());
for entry in entries {
let v = rehydrate(ctx, &entry.v, refs)?;
define_own(&obj, &entry.k, v)?;
}
Ok(obj.into_value())
},
SerializedValue::Reference(id) => refs
.get(id)
.cloned()
.ok_or_else(|| rquickjs::Error::new_from_js_message("rehydrate", "ref", format!("unknown back-ref id {id}"))),
SerializedValue::Handle(_) => Err(rquickjs::Error::new_from_js_message(
"rehydrate",
"handle",
"bare Handle in return value — use evaluateHandle()",
)),
}
}
fn f64_as_exact_i32(n: f64) -> Option<i32> {
if n.is_finite() && n.fract() == 0.0 && n >= f64::from(i32::MIN) && n <= f64::from(i32::MAX) {
let trunc = n.trunc();
i32::try_from(trunc as i64).ok()
} else {
None
}
}
fn construct_global<'js, Args>(ctx: &Ctx<'js>, ctor_name: &'static str, args: Args) -> rquickjs::Result<Value<'js>>
where
Args: rquickjs::function::IntoArgs<'js>,
{
let raw: Value<'js> = ctx.globals().get(ctor_name)?;
let ctor = raw
.try_into_constructor()
.map_err(|_| rquickjs::Error::new_from_js_message("construct", ctor_name, "global is not a constructor"))?;
ctor.construct(args)
}
fn rehydrate_typed_array<'js>(
ctx: &Ctx<'js>,
kind: ferridriver::protocol::TypedArrayKind,
bytes: &[u8],
) -> rquickjs::Result<Value<'js>> {
use ferridriver::protocol::TypedArrayKind;
let len = u32::try_from(bytes.len())
.map_err(|_| rquickjs::Error::new_from_js_message("rehydrate", "TypedArray", "length exceeds u32"))?;
let ab: Value<'js> = construct_global(ctx, "ArrayBuffer", (len,))?;
let view: Value<'js> = construct_global(ctx, "Uint8Array", (ab.clone(),))?;
let view_obj = view
.as_object()
.ok_or_else(|| rquickjs::Error::new_from_js_message("TypedArray", "", "view not an object"))?;
for (i, byte) in bytes.iter().enumerate() {
view_obj.set(u32::try_from(i).unwrap_or(u32::MAX), *byte)?;
}
let ctor_name = match kind {
TypedArrayKind::I8 => "Int8Array",
TypedArrayKind::U8 => "Uint8Array",
TypedArrayKind::U8Clamped => "Uint8ClampedArray",
TypedArrayKind::I16 => "Int16Array",
TypedArrayKind::U16 => "Uint16Array",
TypedArrayKind::I32 => "Int32Array",
TypedArrayKind::U32 => "Uint32Array",
TypedArrayKind::F32 => "Float32Array",
TypedArrayKind::F64 => "Float64Array",
TypedArrayKind::BI64 => "BigInt64Array",
TypedArrayKind::BUI64 => "BigUint64Array",
};
construct_global(ctx, ctor_name, (ab,))
}
pub fn extract_page_function<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<(String, Option<bool>)> {
let is_fn = value.is_function();
let s: String = if let Some(str_val) = value.clone().into_string() {
str_val.to_string()?
} else {
let string_fn: Function<'js> = ctx.globals().get("String")?;
string_fn.call((value,))?
};
Ok((s, Some(is_fn)))
}
#[derive(serde::Deserialize, Debug, Default, Clone, Copy)]
struct JsClickPosition {
x: f64,
y: f64,
}
impl From<JsClickPosition> for ferridriver::options::Point {
fn from(p: JsClickPosition) -> Self {
Self { x: p.x, y: p.y }
}
}
#[derive(serde::Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsClickOptions {
button: Option<String>,
click_count: Option<u32>,
delay: Option<u64>,
force: Option<bool>,
modifiers: Option<Vec<String>>,
no_wait_after: Option<bool>,
position: Option<JsClickPosition>,
steps: Option<u32>,
timeout: Option<u64>,
trial: Option<bool>,
}
#[derive(serde::Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsDispatchEventOptions {
timeout: Option<u64>,
}
pub fn parse_dispatch_event_options<'js>(
ctx: &Ctx<'js>,
value: rquickjs::function::Opt<Value<'js>>,
) -> rquickjs::Result<Option<ferridriver::options::DispatchEventOptions>> {
let raw = match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => v,
_ => return Ok(None),
};
let js: JsDispatchEventOptions = serde_from_js(ctx, raw)?;
Ok(Some(ferridriver::options::DispatchEventOptions { timeout: js.timeout }))
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct JsFilePayload {
name: String,
mime_type: String,
buffer: Vec<u8>,
}
impl From<JsFilePayload> for ferridriver::options::FilePayload {
fn from(p: JsFilePayload) -> Self {
Self {
name: p.name,
mime_type: p.mime_type,
buffer: p.buffer,
}
}
}
#[derive(serde::Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsSetInputFilesOptions {
no_wait_after: Option<bool>,
timeout: Option<u64>,
}
pub fn parse_input_files<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<ferridriver::options::InputFiles> {
if value.is_string() {
let s: String = value.get()?;
return Ok(ferridriver::options::InputFiles::Paths(vec![s.into()]));
}
if let Some(arr) = value.as_array() {
let len = arr.len();
if len == 0 {
return Ok(ferridriver::options::InputFiles::Paths(Vec::new()));
}
let first: Value<'js> = arr.get(0)?;
if first.is_string() {
let mut paths = Vec::with_capacity(len);
for idx in 0..len {
let el: Value<'js> = arr.get(idx)?;
let s: String = el.into_string().map_or_else(
|| {
Err(rquickjs::Error::new_from_js_message(
"ferridriver",
"setInputFiles",
"array elements must be strings",
))
},
|s| s.to_string(),
)?;
paths.push(std::path::PathBuf::from(s));
}
return Ok(ferridriver::options::InputFiles::Paths(paths));
}
let mut payloads = Vec::with_capacity(len);
for idx in 0..len {
let el: Value<'js> = arr.get(idx)?;
let p: JsFilePayload = serde_from_js(ctx, el)?;
payloads.push(p.into());
}
return Ok(ferridriver::options::InputFiles::Payloads(payloads));
}
if value.is_object() {
let p: JsFilePayload = serde_from_js(ctx, value)?;
return Ok(ferridriver::options::InputFiles::Payloads(vec![p.into()]));
}
Err(rquickjs::Error::new_from_js_message(
"ferridriver",
"setInputFiles",
"files must be string | string[] | FilePayload | FilePayload[]",
))
}
pub fn parse_set_input_files_options<'js>(
ctx: &Ctx<'js>,
value: rquickjs::function::Opt<Value<'js>>,
) -> rquickjs::Result<Option<ferridriver::options::SetInputFilesOptions>> {
let raw = match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => v,
_ => return Ok(None),
};
let js: JsSetInputFilesOptions = serde_from_js(ctx, raw)?;
Ok(Some(ferridriver::options::SetInputFilesOptions {
no_wait_after: js.no_wait_after,
timeout: js.timeout,
}))
}
#[derive(serde::Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsDropOptions {
modifiers: Option<Vec<String>>,
position: Option<JsClickPosition>,
timeout: Option<u64>,
}
pub fn parse_drop_options<'js>(
ctx: &Ctx<'js>,
value: rquickjs::function::Opt<Value<'js>>,
) -> rquickjs::Result<Option<ferridriver::options::DropOptions>> {
let raw = match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => v,
_ => return Ok(None),
};
let js: JsDropOptions = serde_from_js(ctx, raw)?;
let mut modifiers = Vec::new();
if let Some(list) = js.modifiers {
for name in list {
let m = ferridriver::options::Modifier::parse(&name).ok_or_else(|| {
rquickjs::Error::new_from_js_message("ferridriver", "drop", format!("Unknown modifier: {name}"))
})?;
modifiers.push(m);
}
}
Ok(Some(ferridriver::options::DropOptions {
modifiers,
position: js.position.map(Into::into),
timeout: js.timeout,
}))
}
pub fn parse_drop_payload<'js>(
ctx: &Ctx<'js>,
value: Value<'js>,
) -> rquickjs::Result<ferridriver::options::DropPayload> {
let obj = value
.into_object()
.ok_or_else(|| rquickjs::Error::new_from_js_message("ferridriver", "drop", "payload must be an object"))?;
let files = match obj.get::<_, Value<'js>>("files") {
Ok(v) if !v.is_undefined() && !v.is_null() => Some(parse_input_files(ctx, v)?),
_ => None,
};
let data = match obj.get::<_, Value<'js>>("data") {
Ok(v) if !v.is_undefined() && !v.is_null() => {
let map: std::collections::BTreeMap<String, String> = serde_from_js(ctx, v)?;
map.into_iter().collect()
},
_ => Vec::new(),
};
Ok(ferridriver::options::DropPayload { files, data })
}
#[derive(serde::Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsSelectOptionValue {
value: Option<String>,
label: Option<String>,
index: Option<u32>,
}
impl From<JsSelectOptionValue> for ferridriver::options::SelectOptionValue {
fn from(v: JsSelectOptionValue) -> Self {
Self {
value: v.value,
label: v.label,
index: v.index,
}
}
}
#[derive(serde::Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsSelectOptionOptions {
force: Option<bool>,
no_wait_after: Option<bool>,
timeout: Option<u64>,
}
pub fn parse_select_option_values<'js>(
ctx: &Ctx<'js>,
value: Value<'js>,
) -> rquickjs::Result<Vec<ferridriver::options::SelectOptionValue>> {
if value.is_string() {
let s: String = value.get()?;
return Ok(vec![ferridriver::options::SelectOptionValue::by_value(s)]);
}
if let Some(arr) = value.as_array() {
let len = arr.len();
let mut out = Vec::with_capacity(len);
for idx in 0..len {
let el: Value<'js> = arr.get(idx)?;
if el.is_string() {
let s: String = el.get()?;
out.push(ferridriver::options::SelectOptionValue::by_value(s));
} else if el.is_object() {
let desc: JsSelectOptionValue = serde_from_js(ctx, el)?;
out.push(desc.into());
} else {
return Err(rquickjs::Error::new_from_js_message(
"ferridriver",
"selectOption",
"array entries must be string or { value?, label?, index? } object",
));
}
}
return Ok(out);
}
if value.is_object() {
let desc: JsSelectOptionValue = serde_from_js(ctx, value)?;
return Ok(vec![desc.into()]);
}
Err(rquickjs::Error::new_from_js_message(
"ferridriver",
"selectOption",
"values must be string | string[] | object | object[]",
))
}
pub fn parse_select_option_options<'js>(
ctx: &Ctx<'js>,
value: rquickjs::function::Opt<Value<'js>>,
) -> rquickjs::Result<Option<ferridriver::options::SelectOptionOptions>> {
let raw = match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => v,
_ => return Ok(None),
};
let js: JsSelectOptionOptions = serde_from_js(ctx, raw)?;
Ok(Some(ferridriver::options::SelectOptionOptions {
force: js.force,
no_wait_after: js.no_wait_after,
timeout: js.timeout,
}))
}
#[derive(serde::Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsFillOptions {
force: Option<bool>,
no_wait_after: Option<bool>,
timeout: Option<u64>,
}
pub fn parse_fill_options<'js>(
ctx: &Ctx<'js>,
value: rquickjs::function::Opt<Value<'js>>,
) -> rquickjs::Result<Option<ferridriver::options::FillOptions>> {
let raw = match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => v,
_ => return Ok(None),
};
let js: JsFillOptions = serde_from_js(ctx, raw)?;
Ok(Some(ferridriver::options::FillOptions {
force: js.force,
no_wait_after: js.no_wait_after,
timeout: js.timeout,
}))
}
#[derive(serde::Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsPressOptions {
delay: Option<u64>,
no_wait_after: Option<bool>,
timeout: Option<u64>,
}
pub fn parse_press_options<'js>(
ctx: &Ctx<'js>,
value: rquickjs::function::Opt<Value<'js>>,
) -> rquickjs::Result<Option<ferridriver::options::PressOptions>> {
let raw = match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => v,
_ => return Ok(None),
};
let js: JsPressOptions = serde_from_js(ctx, raw)?;
Ok(Some(ferridriver::options::PressOptions {
delay: js.delay,
no_wait_after: js.no_wait_after,
timeout: js.timeout,
}))
}
pub fn parse_type_options<'js>(
ctx: &Ctx<'js>,
value: rquickjs::function::Opt<Value<'js>>,
) -> rquickjs::Result<Option<ferridriver::options::TypeOptions>> {
let raw = match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => v,
_ => return Ok(None),
};
let js: JsPressOptions = serde_from_js(ctx, raw)?;
Ok(Some(ferridriver::options::TypeOptions {
delay: js.delay,
no_wait_after: js.no_wait_after,
timeout: js.timeout,
}))
}
#[derive(serde::Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsCheckOptions {
force: Option<bool>,
no_wait_after: Option<bool>,
position: Option<JsClickPosition>,
timeout: Option<u64>,
trial: Option<bool>,
}
pub fn parse_check_options<'js>(
ctx: &Ctx<'js>,
value: rquickjs::function::Opt<Value<'js>>,
) -> rquickjs::Result<Option<ferridriver::options::CheckOptions>> {
let raw = match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => v,
_ => return Ok(None),
};
let js: JsCheckOptions = serde_from_js(ctx, raw)?;
Ok(Some(ferridriver::options::CheckOptions {
force: js.force,
no_wait_after: js.no_wait_after,
position: js.position.map(Into::into),
timeout: js.timeout,
trial: js.trial,
}))
}
#[derive(serde::Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsHoverOptions {
force: Option<bool>,
modifiers: Option<Vec<String>>,
no_wait_after: Option<bool>,
position: Option<JsClickPosition>,
timeout: Option<u64>,
trial: Option<bool>,
}
pub fn parse_hover_options<'js>(
ctx: &Ctx<'js>,
value: rquickjs::function::Opt<Value<'js>>,
) -> rquickjs::Result<Option<ferridriver::options::HoverOptions>> {
let raw = match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => v,
_ => return Ok(None),
};
let js: JsHoverOptions = serde_from_js(ctx, raw)?;
let mut modifiers = Vec::new();
if let Some(list) = js.modifiers {
for name in list {
let m = ferridriver::options::Modifier::parse(&name).ok_or_else(|| {
rquickjs::Error::new_from_js_message("ferridriver", "hover", format!("Unknown modifier: {name}"))
})?;
modifiers.push(m);
}
}
Ok(Some(ferridriver::options::HoverOptions {
force: js.force,
modifiers,
no_wait_after: js.no_wait_after,
position: js.position.map(Into::into),
timeout: js.timeout,
trial: js.trial,
}))
}
#[derive(serde::Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsTapOptions {
force: Option<bool>,
modifiers: Option<Vec<String>>,
no_wait_after: Option<bool>,
position: Option<JsClickPosition>,
timeout: Option<u64>,
trial: Option<bool>,
}
pub fn parse_tap_options<'js>(
ctx: &Ctx<'js>,
value: rquickjs::function::Opt<Value<'js>>,
) -> rquickjs::Result<Option<ferridriver::options::TapOptions>> {
let raw = match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => v,
_ => return Ok(None),
};
let js: JsTapOptions = serde_from_js(ctx, raw)?;
let mut modifiers = Vec::new();
if let Some(list) = js.modifiers {
for name in list {
let m = ferridriver::options::Modifier::parse(&name).ok_or_else(|| {
rquickjs::Error::new_from_js_message("ferridriver", "tap", format!("Unknown modifier: {name}"))
})?;
modifiers.push(m);
}
}
Ok(Some(ferridriver::options::TapOptions {
force: js.force,
modifiers,
no_wait_after: js.no_wait_after,
position: js.position.map(Into::into),
timeout: js.timeout,
trial: js.trial,
}))
}
#[derive(serde::Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase", default)]
struct JsDblClickOptions {
button: Option<String>,
delay: Option<u64>,
force: Option<bool>,
modifiers: Option<Vec<String>>,
no_wait_after: Option<bool>,
position: Option<JsClickPosition>,
steps: Option<u32>,
timeout: Option<u64>,
trial: Option<bool>,
}
pub fn parse_dblclick_options<'js>(
ctx: &Ctx<'js>,
value: rquickjs::function::Opt<Value<'js>>,
) -> rquickjs::Result<Option<ferridriver::options::DblClickOptions>> {
let raw = match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => v,
_ => return Ok(None),
};
let js: JsDblClickOptions = serde_from_js(ctx, raw)?;
let button = match js.button.as_deref() {
None => None,
Some(s) => Some(ferridriver::options::MouseButton::parse(s).ok_or_else(|| {
rquickjs::Error::new_from_js_message("ferridriver", "dblclick", format!("Unknown mouse button: {s}"))
})?),
};
let mut modifiers = Vec::new();
if let Some(list) = js.modifiers {
for name in list {
let m = ferridriver::options::Modifier::parse(&name).ok_or_else(|| {
rquickjs::Error::new_from_js_message("ferridriver", "dblclick", format!("Unknown modifier: {name}"))
})?;
modifiers.push(m);
}
}
Ok(Some(ferridriver::options::DblClickOptions {
button,
delay: js.delay,
force: js.force,
modifiers,
no_wait_after: js.no_wait_after,
position: js.position.map(Into::into),
steps: js.steps,
timeout: js.timeout,
trial: js.trial,
}))
}
pub fn parse_click_options<'js>(
ctx: &Ctx<'js>,
value: rquickjs::function::Opt<Value<'js>>,
) -> rquickjs::Result<Option<ferridriver::options::ClickOptions>> {
let raw = match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => v,
_ => return Ok(None),
};
let js: JsClickOptions = serde_from_js(ctx, raw)?;
let button = match js.button.as_deref() {
None => None,
Some(s) => Some(ferridriver::options::MouseButton::parse(s).ok_or_else(|| {
rquickjs::Error::new_from_js_message("ferridriver", "click", format!("Unknown mouse button: {s}"))
})?),
};
let mut modifiers = Vec::new();
if let Some(list) = js.modifiers {
for name in list {
let m = ferridriver::options::Modifier::parse(&name).ok_or_else(|| {
rquickjs::Error::new_from_js_message("ferridriver", "click", format!("Unknown modifier: {name}"))
})?;
modifiers.push(m);
}
}
Ok(Some(ferridriver::options::ClickOptions {
button,
click_count: js.click_count,
delay: js.delay,
force: js.force,
modifiers,
no_wait_after: js.no_wait_after,
position: js.position.map(Into::into),
steps: js.steps,
timeout: js.timeout,
trial: js.trial,
}))
}
pub fn init_script_from_js<'js>(
ctx: &Ctx<'js>,
script: Value<'js>,
arg: Option<Value<'js>>,
) -> rquickjs::Result<(ferridriver::options::InitScriptSource, Option<serde_json::Value>)> {
let arg_json = match arg {
None => None,
Some(v) if v.is_undefined() => None,
Some(v) if v.is_null() => Some(serde_json::Value::Null),
Some(v) => Some(serde_from_js::<serde_json::Value>(ctx, v)?),
};
let init = if script.is_function() {
let string_global: Function<'js> = ctx.globals().get("String")?;
let body: String = string_global.call((script,))?;
ferridriver::options::InitScriptSource::Function { body }
} else if script.is_string() {
let s: String = script.get()?;
ferridriver::options::InitScriptSource::Source(s)
} else if script.is_object() {
let obj = script
.as_object()
.ok_or_else(|| rquickjs::Error::new_from_js_message("ferridriver", "addInitScript", "expected object"))?;
if let Ok(content) = obj.get::<_, String>("content") {
ferridriver::options::InitScriptSource::Content(content)
} else if let Ok(path) = obj.get::<_, String>("path") {
ferridriver::options::InitScriptSource::Path(path.into())
} else {
return Err(rquickjs::Error::new_from_js_message(
"ferridriver",
"addInitScript",
"Either path or content property must be present",
));
}
} else {
return Err(rquickjs::Error::new_from_js_message(
"ferridriver",
"addInitScript",
"script must be Function | string | { path?, content? }",
));
};
Ok((init, arg_json))
}