1use ferridriver::FerriError;
4use rquickjs::object::Property;
5use rquickjs::{Ctx, Function, Object, Value};
6use serde::Serialize;
7use serde::de::DeserializeOwned;
8
9pub fn to_rq_error(err: &FerriError) -> rquickjs::Error {
17 rquickjs::Error::new_from_js_message("ferridriver", err.name(), err.to_string())
18}
19
20#[must_use]
24pub fn ms_f64_to_u64(ms: f64) -> u64 {
25 if ms <= 0.0 {
26 return 0;
27 }
28 #[allow(
32 clippy::cast_possible_truncation,
33 clippy::cast_sign_loss,
34 clippy::cast_precision_loss
35 )]
36 {
37 ms.min(u64::MAX as f64) as u64
38 }
39}
40
41pub trait FerriResultExt<T> {
43 fn into_js(self) -> rquickjs::Result<T>;
44}
45
46impl<T> FerriResultExt<T> for Result<T, FerriError> {
47 fn into_js(self) -> rquickjs::Result<T> {
48 self.map_err(|e| to_rq_error(&e))
49 }
50}
51
52pub fn serde_to_js<'js, T: Serialize>(ctx: &Ctx<'js>, value: &T) -> rquickjs::Result<Value<'js>> {
57 rquickjs_serde::to_value(ctx.clone(), value)
58 .map_err(|e| rquickjs::Error::new_from_js_message("serde", "serialize", e.to_string()))
59}
60
61pub fn name_value_array_to_js<'js, S: AsRef<str>>(ctx: &Ctx<'js>, pairs: &[(S, S)]) -> rquickjs::Result<Value<'js>> {
66 #[derive(Serialize)]
67 struct NameValue<'a> {
68 name: &'a str,
69 value: &'a str,
70 }
71 let view: Vec<NameValue<'_>> = pairs
72 .iter()
73 .map(|(n, v)| NameValue {
74 name: n.as_ref(),
75 value: v.as_ref(),
76 })
77 .collect();
78 serde_to_js(ctx, &view)
79}
80
81pub fn serde_from_js<'js, T: DeserializeOwned>(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<T> {
88 rquickjs_serde::from_value(value)
89 .map_err(|e| rquickjs::Error::new_from_js_message("serde", "deserialize", e.to_string()))
90}
91
92fn define_own<'js, V: rquickjs::IntoJs<'js>>(obj: &Object<'js>, key: &str, value: V) -> rquickjs::Result<()> {
103 obj.prop(key, Property::from(value).writable().enumerable().configurable())
104}
105
106pub(crate) fn json_to_js<'js>(ctx: &Ctx<'js>, v: &serde_json::Value) -> rquickjs::Result<Value<'js>> {
110 match v {
117 serde_json::Value::Null => Ok(Value::new_null(ctx.clone())),
118 serde_json::Value::Bool(b) => Ok(Value::new_bool(ctx.clone(), *b)),
119 serde_json::Value::Number(n) => {
120 let f = n.as_f64().unwrap_or(f64::NAN);
121 if let Some(i) = f64_as_exact_i32(f) {
122 Ok(Value::new_int(ctx.clone(), i))
123 } else {
124 Ok(Value::new_number(ctx.clone(), f))
125 }
126 },
127 serde_json::Value::String(s) => Ok(rquickjs::String::from_str(ctx.clone(), s)?.into_value()),
128 serde_json::Value::Array(items) => {
129 let arr = rquickjs::Array::new(ctx.clone())?;
130 for (i, item) in items.iter().enumerate() {
131 arr.set(i, json_to_js(ctx, item)?)?;
132 }
133 Ok(arr.into_value())
134 },
135 serde_json::Value::Object(map) => {
136 let obj = Object::new(ctx.clone())?;
137 for (k, val) in map {
138 define_own(&obj, k.as_str(), json_to_js(ctx, val)?)?;
139 }
140 Ok(obj.into_value())
141 },
142 }
143}
144
145pub fn quickjs_arg_to_serialized<'js>(
162 _ctx: &Ctx<'js>,
163 value: Option<Value<'js>>,
164) -> rquickjs::Result<ferridriver::protocol::SerializedArgument> {
165 use ferridriver::protocol::{SerializationContext, SerializedArgument, SerializedValue, SpecialValue};
166
167 let v = match value {
168 Some(v) if !v.is_undefined() => v,
169 _ => {
170 return Ok(SerializedArgument {
171 value: SerializedValue::Special(SpecialValue::Undefined),
172 handles: Vec::new(),
173 });
174 },
175 };
176
177 if v.is_null() {
178 return Ok(SerializedArgument {
179 value: SerializedValue::Special(SpecialValue::Null),
180 handles: Vec::new(),
181 });
182 }
183
184 let mut alloc = SerializationContext::default();
192 let mut handles = Vec::new();
193 Ok(SerializedArgument {
194 value: js_value_to_serialized(&v, &mut alloc, &mut handles, 0)?,
195 handles,
196 })
197}
198
199const MAX_ARG_DEPTH: u32 = 512;
203
204fn js_value_to_serialized(
207 v: &Value<'_>,
208 alloc: &mut ferridriver::protocol::SerializationContext,
209 handles: &mut Vec<ferridriver::protocol::HandleId>,
210 depth: u32,
211) -> rquickjs::Result<ferridriver::protocol::SerializedValue> {
212 use ferridriver::protocol::{SerializedValue, SpecialValue};
213
214 if depth > MAX_ARG_DEPTH {
215 return Err(rquickjs::Error::new_from_js_message(
216 "serde",
217 "serialize",
218 "argument too deeply nested or cyclic".to_string(),
219 ));
220 }
221
222 if v.is_undefined() {
223 return Ok(SerializedValue::Special(SpecialValue::Undefined));
224 }
225 if v.is_null() {
226 return Ok(SerializedValue::Special(SpecialValue::Null));
227 }
228 if let Some(b) = v.as_bool() {
229 return Ok(SerializedValue::Bool(b));
230 }
231 if let Some(i) = v.as_int() {
232 return Ok(SerializedValue::from_f64(f64::from(i)));
233 }
234 if let Some(f) = v.as_float() {
235 return Ok(if f.is_finite() {
237 SerializedValue::from_f64(f)
238 } else {
239 SerializedValue::Special(SpecialValue::Null)
240 });
241 }
242 if let Some(s) = v.as_string() {
243 return Ok(SerializedValue::Str(s.to_string()?));
244 }
245 if let Some(bi) = v.as_big_int() {
246 return match bi.clone().to_i64() {
250 Ok(n) => Ok(SerializedValue::BigInt(n.to_string())),
251 Err(_) => Err(rquickjs::Error::new_from_js_message(
252 "serde",
253 "serialize",
254 "BigInt argument out of i64 range".to_string(),
255 )),
256 };
257 }
258 if let Some(arr) = v.as_array() {
259 let id = alloc.alloc_id();
260 let mut items = Vec::with_capacity(arr.len());
261 for idx in 0..arr.len() {
262 let el: Value<'_> = arr.get(idx)?;
263 items.push(if el.is_undefined() || el.is_function() {
265 SerializedValue::Special(SpecialValue::Null)
266 } else {
267 js_value_to_serialized(&el, alloc, handles, depth + 1)?
268 });
269 }
270 return Ok(SerializedValue::Array { id, items });
271 }
272 if let Some(obj) = v.as_object() {
273 let id = alloc.alloc_id();
276 let mut entries = Vec::new();
277 for key in obj.keys::<String>() {
278 let key = key?;
279 let val: Value<'_> = obj.get(&key)?;
280 if val.is_undefined() || val.is_function() || val.type_of() == rquickjs::Type::Symbol {
281 continue;
282 }
283 entries.push(ferridriver::protocol::PropertyEntry {
284 k: key,
285 v: js_value_to_serialized(&val, alloc, handles, depth + 1)?,
286 });
287 }
288 if entries.is_empty() {
289 if let Some(handle) = handle_value_to_serialized(v, handles)? {
290 return Ok(handle);
291 }
292 }
293 return Ok(SerializedValue::Object { id, entries });
294 }
295
296 Ok(SerializedValue::Special(SpecialValue::Undefined))
299}
300
301fn handle_value_to_serialized(
302 v: &Value<'_>,
303 handles: &mut Vec<ferridriver::protocol::HandleId>,
304) -> rquickjs::Result<Option<ferridriver::protocol::SerializedValue>> {
305 if let Ok(class) = rquickjs::Class::<crate::bindings::js_handle::JSHandleJs>::from_value(v) {
306 let inner = class.borrow();
307 return Ok(Some(handle_backing_to_serialized(inner.inner().backing(), handles)?));
308 }
309 if let Ok(class) = rquickjs::Class::<crate::bindings::element_handle::ElementHandleJs>::from_value(v) {
310 let inner = class.borrow();
311 let handle = inner.inner().as_js_handle();
312 return Ok(Some(handle_backing_to_serialized(handle.backing(), handles)?));
313 }
314 Ok(None)
315}
316
317fn handle_backing_to_serialized(
318 backing: &ferridriver::js_handle::JSHandleBacking,
319 handles: &mut Vec<ferridriver::protocol::HandleId>,
320) -> rquickjs::Result<ferridriver::protocol::SerializedValue> {
321 match backing {
322 ferridriver::js_handle::JSHandleBacking::Remote(remote) => {
323 let idx = u32::try_from(handles.len())
324 .map_err(|_| rquickjs::Error::new_from_js_message("serde", "serialize", "too many JS handles"))?;
325 handles.push(remote.to_handle_id());
326 Ok(ferridriver::protocol::SerializedValue::Handle(idx))
327 },
328 ferridriver::js_handle::JSHandleBacking::Value(value) => Ok(value.clone()),
329 }
330}
331
332pub fn serialized_value_to_quickjs<'js>(
338 ctx: &Ctx<'js>,
339 value: &ferridriver::protocol::SerializedValue,
340) -> rquickjs::Result<Value<'js>> {
341 let mut refs: rustc_hash::FxHashMap<u32, Value<'js>> = rustc_hash::FxHashMap::default();
342 rehydrate(ctx, value, &mut refs)
343}
344
345fn rehydrate<'js>(
346 ctx: &Ctx<'js>,
347 value: &ferridriver::protocol::SerializedValue,
348 refs: &mut rustc_hash::FxHashMap<u32, Value<'js>>,
349) -> rquickjs::Result<Value<'js>> {
350 use ferridriver::protocol::{ErrorValue, RegExpValue, SerializedValue, SpecialValue};
351
352 match value {
353 SerializedValue::Bool(b) => Ok(Value::new_bool(ctx.clone(), *b)),
354 SerializedValue::Number(n) => {
355 if let Some(i) = f64_as_exact_i32(*n) {
356 Ok(Value::new_int(ctx.clone(), i))
357 } else {
358 Ok(Value::new_number(ctx.clone(), *n))
359 }
360 },
361 SerializedValue::Str(s) => {
362 let js = rquickjs::String::from_str(ctx.clone(), s)?;
363 Ok(js.into_value())
364 },
365 SerializedValue::Special(SpecialValue::Null) => Ok(Value::new_null(ctx.clone())),
366 SerializedValue::Special(SpecialValue::Undefined) => Ok(Value::new_undefined(ctx.clone())),
367 SerializedValue::Special(SpecialValue::NaN) => Ok(Value::new_number(ctx.clone(), f64::NAN)),
368 SerializedValue::Special(SpecialValue::Infinity) => Ok(Value::new_number(ctx.clone(), f64::INFINITY)),
369 SerializedValue::Special(SpecialValue::NegInfinity) => Ok(Value::new_number(ctx.clone(), f64::NEG_INFINITY)),
370 SerializedValue::Special(SpecialValue::NegZero) => Ok(Value::new_number(ctx.clone(), -0.0)),
371 SerializedValue::Date(iso) => construct_global(ctx, "Date", (iso.clone(),)),
372 SerializedValue::Url(url) => construct_global(ctx, "URL", (url.clone(),)),
373 SerializedValue::BigInt(s) => {
374 let func: Function<'js> = ctx.globals().get("BigInt")?;
376 func.call((s.clone(),))
377 },
378 SerializedValue::RegExp(RegExpValue { p, f }) => construct_global(ctx, "RegExp", (p.clone(), f.clone())),
379 SerializedValue::Error(ErrorValue { m, n, s }) => {
380 let err: Value<'js> = construct_global(ctx, "Error", (m.clone(),))?;
381 let obj = err
382 .as_object()
383 .ok_or_else(|| rquickjs::Error::new_from_js_message("Error", "", "not an object"))?;
384 obj.set("name", n.clone())?;
385 obj.set("stack", s.clone())?;
386 Ok(err)
387 },
388 SerializedValue::TypedArray(ta) => rehydrate_typed_array(ctx, ta.k, &ta.b),
389 SerializedValue::ArrayBuffer(ab) => {
390 let len = u32::try_from(ab.b.len())
391 .map_err(|_| rquickjs::Error::new_from_js_message("rehydrate", "ArrayBuffer", "length exceeds u32"))?;
392 let buf: Value<'js> = construct_global(ctx, "ArrayBuffer", (len,))?;
393 let view: Value<'js> = construct_global(ctx, "Uint8Array", (buf.clone(),))?;
394 let view_obj = view
395 .as_object()
396 .ok_or_else(|| rquickjs::Error::new_from_js_message("ArrayBuffer", "", "view not an object"))?;
397 for (i, byte) in ab.b.iter().enumerate() {
398 view_obj.set(u32::try_from(i).unwrap_or(u32::MAX), *byte)?;
399 }
400 Ok(buf)
401 },
402 SerializedValue::Array { id, items } => {
403 let arr = rquickjs::Array::new(ctx.clone())?;
409 refs.insert(*id, arr.as_value().clone());
410 for (i, item) in items.iter().enumerate() {
411 let v = rehydrate(ctx, item, refs)?;
412 arr.set(i, v)?;
413 }
414 Ok(arr.into_value())
415 },
416 SerializedValue::Object { id, entries } => {
417 let obj = Object::new(ctx.clone())?;
418 refs.insert(*id, obj.as_value().clone());
419 for entry in entries {
420 let v = rehydrate(ctx, &entry.v, refs)?;
421 define_own(&obj, &entry.k, v)?;
422 }
423 Ok(obj.into_value())
424 },
425 SerializedValue::Reference(id) => refs
426 .get(id)
427 .cloned()
428 .ok_or_else(|| rquickjs::Error::new_from_js_message("rehydrate", "ref", format!("unknown back-ref id {id}"))),
429 SerializedValue::Handle(_) => Err(rquickjs::Error::new_from_js_message(
430 "rehydrate",
431 "handle",
432 "bare Handle in return value — use evaluateHandle()",
433 )),
434 }
435}
436
437fn f64_as_exact_i32(n: f64) -> Option<i32> {
438 if n.is_finite() && n.fract() == 0.0 && n >= f64::from(i32::MIN) && n <= f64::from(i32::MAX) {
439 let trunc = n.trunc();
441 i32::try_from(trunc as i64).ok()
442 } else {
443 None
444 }
445}
446
447fn construct_global<'js, Args>(ctx: &Ctx<'js>, ctor_name: &'static str, args: Args) -> rquickjs::Result<Value<'js>>
448where
449 Args: rquickjs::function::IntoArgs<'js>,
450{
451 let raw: Value<'js> = ctx.globals().get(ctor_name)?;
452 let ctor = raw
453 .try_into_constructor()
454 .map_err(|_| rquickjs::Error::new_from_js_message("construct", ctor_name, "global is not a constructor"))?;
455 ctor.construct(args)
456}
457
458fn rehydrate_typed_array<'js>(
459 ctx: &Ctx<'js>,
460 kind: ferridriver::protocol::TypedArrayKind,
461 bytes: &[u8],
462) -> rquickjs::Result<Value<'js>> {
463 use ferridriver::protocol::TypedArrayKind;
464 let len = u32::try_from(bytes.len())
468 .map_err(|_| rquickjs::Error::new_from_js_message("rehydrate", "TypedArray", "length exceeds u32"))?;
469 let ab: Value<'js> = construct_global(ctx, "ArrayBuffer", (len,))?;
470 let view: Value<'js> = construct_global(ctx, "Uint8Array", (ab.clone(),))?;
471 let view_obj = view
472 .as_object()
473 .ok_or_else(|| rquickjs::Error::new_from_js_message("TypedArray", "", "view not an object"))?;
474 for (i, byte) in bytes.iter().enumerate() {
475 view_obj.set(u32::try_from(i).unwrap_or(u32::MAX), *byte)?;
476 }
477 let ctor_name = match kind {
478 TypedArrayKind::I8 => "Int8Array",
479 TypedArrayKind::U8 => "Uint8Array",
480 TypedArrayKind::U8Clamped => "Uint8ClampedArray",
481 TypedArrayKind::I16 => "Int16Array",
482 TypedArrayKind::U16 => "Uint16Array",
483 TypedArrayKind::I32 => "Int32Array",
484 TypedArrayKind::U32 => "Uint32Array",
485 TypedArrayKind::F32 => "Float32Array",
486 TypedArrayKind::F64 => "Float64Array",
487 TypedArrayKind::BI64 => "BigInt64Array",
488 TypedArrayKind::BUI64 => "BigUint64Array",
489 };
490 construct_global(ctx, ctor_name, (ab,))
491}
492
493pub fn extract_page_function<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<(String, Option<bool>)> {
499 let is_fn = value.is_function();
500 let s: String = if let Some(str_val) = value.clone().into_string() {
501 str_val.to_string()?
502 } else {
503 let string_fn: Function<'js> = ctx.globals().get("String")?;
507 string_fn.call((value,))?
508 };
509 Ok((s, Some(is_fn)))
510}
511
512#[derive(serde::Deserialize, Debug, Default, Clone, Copy)]
515struct JsClickPosition {
516 x: f64,
517 y: f64,
518}
519
520impl From<JsClickPosition> for ferridriver::options::Point {
521 fn from(p: JsClickPosition) -> Self {
522 Self { x: p.x, y: p.y }
523 }
524}
525
526#[derive(serde::Deserialize, Debug, Default)]
533#[serde(rename_all = "camelCase", default)]
534struct JsClickOptions {
535 button: Option<String>,
536 click_count: Option<u32>,
537 delay: Option<u64>,
538 force: Option<bool>,
539 modifiers: Option<Vec<String>>,
540 no_wait_after: Option<bool>,
541 position: Option<JsClickPosition>,
542 steps: Option<u32>,
543 timeout: Option<u64>,
544 trial: Option<bool>,
545}
546
547#[derive(serde::Deserialize, Debug, Default)]
549#[serde(rename_all = "camelCase", default)]
550struct JsDispatchEventOptions {
551 timeout: Option<u64>,
552}
553
554pub fn parse_dispatch_event_options<'js>(
556 ctx: &Ctx<'js>,
557 value: rquickjs::function::Opt<Value<'js>>,
558) -> rquickjs::Result<Option<ferridriver::options::DispatchEventOptions>> {
559 let raw = match value.0 {
560 Some(v) if !v.is_undefined() && !v.is_null() => v,
561 _ => return Ok(None),
562 };
563 let js: JsDispatchEventOptions = serde_from_js(ctx, raw)?;
564 Ok(Some(ferridriver::options::DispatchEventOptions { timeout: js.timeout }))
565}
566
567#[derive(serde::Deserialize, Debug)]
569#[serde(rename_all = "camelCase")]
570struct JsFilePayload {
571 name: String,
572 mime_type: String,
573 buffer: Vec<u8>,
577}
578
579impl From<JsFilePayload> for ferridriver::options::FilePayload {
580 fn from(p: JsFilePayload) -> Self {
581 Self {
582 name: p.name,
583 mime_type: p.mime_type,
584 buffer: p.buffer,
585 }
586 }
587}
588
589#[derive(serde::Deserialize, Debug, Default)]
591#[serde(rename_all = "camelCase", default)]
592struct JsSetInputFilesOptions {
593 no_wait_after: Option<bool>,
594 timeout: Option<u64>,
595}
596
597pub fn parse_input_files<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<ferridriver::options::InputFiles> {
600 if value.is_string() {
601 let s: String = value.get()?;
602 return Ok(ferridriver::options::InputFiles::Paths(vec![s.into()]));
603 }
604 if let Some(arr) = value.as_array() {
605 let len = arr.len();
606 if len == 0 {
607 return Ok(ferridriver::options::InputFiles::Paths(Vec::new()));
608 }
609 let first: Value<'js> = arr.get(0)?;
613 if first.is_string() {
614 let mut paths = Vec::with_capacity(len);
615 for idx in 0..len {
616 let el: Value<'js> = arr.get(idx)?;
617 let s: String = el.into_string().map_or_else(
618 || {
619 Err(rquickjs::Error::new_from_js_message(
620 "ferridriver",
621 "setInputFiles",
622 "array elements must be strings",
623 ))
624 },
625 |s| s.to_string(),
626 )?;
627 paths.push(std::path::PathBuf::from(s));
628 }
629 return Ok(ferridriver::options::InputFiles::Paths(paths));
630 }
631 let mut payloads = Vec::with_capacity(len);
632 for idx in 0..len {
633 let el: Value<'js> = arr.get(idx)?;
634 let p: JsFilePayload = serde_from_js(ctx, el)?;
635 payloads.push(p.into());
636 }
637 return Ok(ferridriver::options::InputFiles::Payloads(payloads));
638 }
639 if value.is_object() {
640 let p: JsFilePayload = serde_from_js(ctx, value)?;
641 return Ok(ferridriver::options::InputFiles::Payloads(vec![p.into()]));
642 }
643 Err(rquickjs::Error::new_from_js_message(
644 "ferridriver",
645 "setInputFiles",
646 "files must be string | string[] | FilePayload | FilePayload[]",
647 ))
648}
649
650pub fn parse_set_input_files_options<'js>(
652 ctx: &Ctx<'js>,
653 value: rquickjs::function::Opt<Value<'js>>,
654) -> rquickjs::Result<Option<ferridriver::options::SetInputFilesOptions>> {
655 let raw = match value.0 {
656 Some(v) if !v.is_undefined() && !v.is_null() => v,
657 _ => return Ok(None),
658 };
659 let js: JsSetInputFilesOptions = serde_from_js(ctx, raw)?;
660 Ok(Some(ferridriver::options::SetInputFilesOptions {
661 no_wait_after: js.no_wait_after,
662 timeout: js.timeout,
663 }))
664}
665
666#[derive(serde::Deserialize, Debug, Default)]
670#[serde(rename_all = "camelCase", default)]
671struct JsDropOptions {
672 modifiers: Option<Vec<String>>,
673 position: Option<JsClickPosition>,
674 timeout: Option<u64>,
675}
676
677pub fn parse_drop_options<'js>(
679 ctx: &Ctx<'js>,
680 value: rquickjs::function::Opt<Value<'js>>,
681) -> rquickjs::Result<Option<ferridriver::options::DropOptions>> {
682 let raw = match value.0 {
683 Some(v) if !v.is_undefined() && !v.is_null() => v,
684 _ => return Ok(None),
685 };
686 let js: JsDropOptions = serde_from_js(ctx, raw)?;
687 let mut modifiers = Vec::new();
688 if let Some(list) = js.modifiers {
689 for name in list {
690 let m = ferridriver::options::Modifier::parse(&name).ok_or_else(|| {
691 rquickjs::Error::new_from_js_message("ferridriver", "drop", format!("Unknown modifier: {name}"))
692 })?;
693 modifiers.push(m);
694 }
695 }
696 Ok(Some(ferridriver::options::DropOptions {
697 modifiers,
698 position: js.position.map(Into::into),
699 timeout: js.timeout,
700 }))
701}
702
703pub fn parse_drop_payload<'js>(
707 ctx: &Ctx<'js>,
708 value: Value<'js>,
709) -> rquickjs::Result<ferridriver::options::DropPayload> {
710 let obj = value
711 .into_object()
712 .ok_or_else(|| rquickjs::Error::new_from_js_message("ferridriver", "drop", "payload must be an object"))?;
713
714 let files = match obj.get::<_, Value<'js>>("files") {
715 Ok(v) if !v.is_undefined() && !v.is_null() => Some(parse_input_files(ctx, v)?),
716 _ => None,
717 };
718
719 let data = match obj.get::<_, Value<'js>>("data") {
720 Ok(v) if !v.is_undefined() && !v.is_null() => {
721 let map: std::collections::BTreeMap<String, String> = serde_from_js(ctx, v)?;
722 map.into_iter().collect()
723 },
724 _ => Vec::new(),
725 };
726
727 Ok(ferridriver::options::DropPayload { files, data })
728}
729
730#[derive(serde::Deserialize, Debug, Default)]
733#[serde(rename_all = "camelCase", default)]
734struct JsSelectOptionValue {
735 value: Option<String>,
736 label: Option<String>,
737 index: Option<u32>,
738}
739
740impl From<JsSelectOptionValue> for ferridriver::options::SelectOptionValue {
741 fn from(v: JsSelectOptionValue) -> Self {
742 Self {
743 value: v.value,
744 label: v.label,
745 index: v.index,
746 }
747 }
748}
749
750#[derive(serde::Deserialize, Debug, Default)]
752#[serde(rename_all = "camelCase", default)]
753struct JsSelectOptionOptions {
754 force: Option<bool>,
755 no_wait_after: Option<bool>,
756 timeout: Option<u64>,
757}
758
759pub fn parse_select_option_values<'js>(
762 ctx: &Ctx<'js>,
763 value: Value<'js>,
764) -> rquickjs::Result<Vec<ferridriver::options::SelectOptionValue>> {
765 if value.is_string() {
766 let s: String = value.get()?;
767 return Ok(vec![ferridriver::options::SelectOptionValue::by_value(s)]);
768 }
769 if let Some(arr) = value.as_array() {
770 let len = arr.len();
771 let mut out = Vec::with_capacity(len);
772 for idx in 0..len {
773 let el: Value<'js> = arr.get(idx)?;
774 if el.is_string() {
775 let s: String = el.get()?;
776 out.push(ferridriver::options::SelectOptionValue::by_value(s));
777 } else if el.is_object() {
778 let desc: JsSelectOptionValue = serde_from_js(ctx, el)?;
780 out.push(desc.into());
781 } else {
782 return Err(rquickjs::Error::new_from_js_message(
783 "ferridriver",
784 "selectOption",
785 "array entries must be string or { value?, label?, index? } object",
786 ));
787 }
788 }
789 return Ok(out);
790 }
791 if value.is_object() {
792 let desc: JsSelectOptionValue = serde_from_js(ctx, value)?;
793 return Ok(vec![desc.into()]);
794 }
795 Err(rquickjs::Error::new_from_js_message(
796 "ferridriver",
797 "selectOption",
798 "values must be string | string[] | object | object[]",
799 ))
800}
801
802pub fn parse_select_option_options<'js>(
804 ctx: &Ctx<'js>,
805 value: rquickjs::function::Opt<Value<'js>>,
806) -> rquickjs::Result<Option<ferridriver::options::SelectOptionOptions>> {
807 let raw = match value.0 {
808 Some(v) if !v.is_undefined() && !v.is_null() => v,
809 _ => return Ok(None),
810 };
811 let js: JsSelectOptionOptions = serde_from_js(ctx, raw)?;
812 Ok(Some(ferridriver::options::SelectOptionOptions {
813 force: js.force,
814 no_wait_after: js.no_wait_after,
815 timeout: js.timeout,
816 }))
817}
818
819#[derive(serde::Deserialize, Debug, Default)]
821#[serde(rename_all = "camelCase", default)]
822struct JsFillOptions {
823 force: Option<bool>,
824 no_wait_after: Option<bool>,
825 timeout: Option<u64>,
826}
827
828pub fn parse_fill_options<'js>(
830 ctx: &Ctx<'js>,
831 value: rquickjs::function::Opt<Value<'js>>,
832) -> rquickjs::Result<Option<ferridriver::options::FillOptions>> {
833 let raw = match value.0 {
834 Some(v) if !v.is_undefined() && !v.is_null() => v,
835 _ => return Ok(None),
836 };
837 let js: JsFillOptions = serde_from_js(ctx, raw)?;
838 Ok(Some(ferridriver::options::FillOptions {
839 force: js.force,
840 no_wait_after: js.no_wait_after,
841 timeout: js.timeout,
842 }))
843}
844
845#[derive(serde::Deserialize, Debug, Default)]
847#[serde(rename_all = "camelCase", default)]
848struct JsPressOptions {
849 delay: Option<u64>,
850 no_wait_after: Option<bool>,
851 timeout: Option<u64>,
852}
853
854pub fn parse_press_options<'js>(
856 ctx: &Ctx<'js>,
857 value: rquickjs::function::Opt<Value<'js>>,
858) -> rquickjs::Result<Option<ferridriver::options::PressOptions>> {
859 let raw = match value.0 {
860 Some(v) if !v.is_undefined() && !v.is_null() => v,
861 _ => return Ok(None),
862 };
863 let js: JsPressOptions = serde_from_js(ctx, raw)?;
864 Ok(Some(ferridriver::options::PressOptions {
865 delay: js.delay,
866 no_wait_after: js.no_wait_after,
867 timeout: js.timeout,
868 }))
869}
870
871pub fn parse_type_options<'js>(
873 ctx: &Ctx<'js>,
874 value: rquickjs::function::Opt<Value<'js>>,
875) -> rquickjs::Result<Option<ferridriver::options::TypeOptions>> {
876 let raw = match value.0 {
877 Some(v) if !v.is_undefined() && !v.is_null() => v,
878 _ => return Ok(None),
879 };
880 let js: JsPressOptions = serde_from_js(ctx, raw)?;
881 Ok(Some(ferridriver::options::TypeOptions {
882 delay: js.delay,
883 no_wait_after: js.no_wait_after,
884 timeout: js.timeout,
885 }))
886}
887
888#[derive(serde::Deserialize, Debug, Default)]
892#[serde(rename_all = "camelCase", default)]
893struct JsCheckOptions {
894 force: Option<bool>,
895 no_wait_after: Option<bool>,
896 position: Option<JsClickPosition>,
897 timeout: Option<u64>,
898 trial: Option<bool>,
899}
900
901pub fn parse_check_options<'js>(
904 ctx: &Ctx<'js>,
905 value: rquickjs::function::Opt<Value<'js>>,
906) -> rquickjs::Result<Option<ferridriver::options::CheckOptions>> {
907 let raw = match value.0 {
908 Some(v) if !v.is_undefined() && !v.is_null() => v,
909 _ => return Ok(None),
910 };
911 let js: JsCheckOptions = serde_from_js(ctx, raw)?;
912 Ok(Some(ferridriver::options::CheckOptions {
913 force: js.force,
914 no_wait_after: js.no_wait_after,
915 position: js.position.map(Into::into),
916 timeout: js.timeout,
917 trial: js.trial,
918 }))
919}
920
921#[derive(serde::Deserialize, Debug, Default)]
925#[serde(rename_all = "camelCase", default)]
926struct JsHoverOptions {
927 force: Option<bool>,
928 modifiers: Option<Vec<String>>,
929 no_wait_after: Option<bool>,
930 position: Option<JsClickPosition>,
931 timeout: Option<u64>,
932 trial: Option<bool>,
933}
934
935pub fn parse_hover_options<'js>(
937 ctx: &Ctx<'js>,
938 value: rquickjs::function::Opt<Value<'js>>,
939) -> rquickjs::Result<Option<ferridriver::options::HoverOptions>> {
940 let raw = match value.0 {
941 Some(v) if !v.is_undefined() && !v.is_null() => v,
942 _ => return Ok(None),
943 };
944 let js: JsHoverOptions = serde_from_js(ctx, raw)?;
945 let mut modifiers = Vec::new();
946 if let Some(list) = js.modifiers {
947 for name in list {
948 let m = ferridriver::options::Modifier::parse(&name).ok_or_else(|| {
949 rquickjs::Error::new_from_js_message("ferridriver", "hover", format!("Unknown modifier: {name}"))
950 })?;
951 modifiers.push(m);
952 }
953 }
954 Ok(Some(ferridriver::options::HoverOptions {
955 force: js.force,
956 modifiers,
957 no_wait_after: js.no_wait_after,
958 position: js.position.map(Into::into),
959 timeout: js.timeout,
960 trial: js.trial,
961 }))
962}
963
964#[derive(serde::Deserialize, Debug, Default)]
968#[serde(rename_all = "camelCase", default)]
969struct JsTapOptions {
970 force: Option<bool>,
971 modifiers: Option<Vec<String>>,
972 no_wait_after: Option<bool>,
973 position: Option<JsClickPosition>,
974 timeout: Option<u64>,
975 trial: Option<bool>,
976}
977
978pub fn parse_tap_options<'js>(
980 ctx: &Ctx<'js>,
981 value: rquickjs::function::Opt<Value<'js>>,
982) -> rquickjs::Result<Option<ferridriver::options::TapOptions>> {
983 let raw = match value.0 {
984 Some(v) if !v.is_undefined() && !v.is_null() => v,
985 _ => return Ok(None),
986 };
987 let js: JsTapOptions = serde_from_js(ctx, raw)?;
988 let mut modifiers = Vec::new();
989 if let Some(list) = js.modifiers {
990 for name in list {
991 let m = ferridriver::options::Modifier::parse(&name).ok_or_else(|| {
992 rquickjs::Error::new_from_js_message("ferridriver", "tap", format!("Unknown modifier: {name}"))
993 })?;
994 modifiers.push(m);
995 }
996 }
997 Ok(Some(ferridriver::options::TapOptions {
998 force: js.force,
999 modifiers,
1000 no_wait_after: js.no_wait_after,
1001 position: js.position.map(Into::into),
1002 timeout: js.timeout,
1003 trial: js.trial,
1004 }))
1005}
1006
1007#[derive(serde::Deserialize, Debug, Default)]
1011#[serde(rename_all = "camelCase", default)]
1012struct JsDblClickOptions {
1013 button: Option<String>,
1014 delay: Option<u64>,
1015 force: Option<bool>,
1016 modifiers: Option<Vec<String>>,
1017 no_wait_after: Option<bool>,
1018 position: Option<JsClickPosition>,
1019 steps: Option<u32>,
1020 timeout: Option<u64>,
1021 trial: Option<bool>,
1022}
1023
1024pub fn parse_dblclick_options<'js>(
1026 ctx: &Ctx<'js>,
1027 value: rquickjs::function::Opt<Value<'js>>,
1028) -> rquickjs::Result<Option<ferridriver::options::DblClickOptions>> {
1029 let raw = match value.0 {
1030 Some(v) if !v.is_undefined() && !v.is_null() => v,
1031 _ => return Ok(None),
1032 };
1033 let js: JsDblClickOptions = serde_from_js(ctx, raw)?;
1034 let button = match js.button.as_deref() {
1035 None => None,
1036 Some(s) => Some(ferridriver::options::MouseButton::parse(s).ok_or_else(|| {
1037 rquickjs::Error::new_from_js_message("ferridriver", "dblclick", format!("Unknown mouse button: {s}"))
1038 })?),
1039 };
1040 let mut modifiers = Vec::new();
1041 if let Some(list) = js.modifiers {
1042 for name in list {
1043 let m = ferridriver::options::Modifier::parse(&name).ok_or_else(|| {
1044 rquickjs::Error::new_from_js_message("ferridriver", "dblclick", format!("Unknown modifier: {name}"))
1045 })?;
1046 modifiers.push(m);
1047 }
1048 }
1049 Ok(Some(ferridriver::options::DblClickOptions {
1050 button,
1051 delay: js.delay,
1052 force: js.force,
1053 modifiers,
1054 no_wait_after: js.no_wait_after,
1055 position: js.position.map(Into::into),
1056 steps: js.steps,
1057 timeout: js.timeout,
1058 trial: js.trial,
1059 }))
1060}
1061
1062pub fn parse_click_options<'js>(
1069 ctx: &Ctx<'js>,
1070 value: rquickjs::function::Opt<Value<'js>>,
1071) -> rquickjs::Result<Option<ferridriver::options::ClickOptions>> {
1072 let raw = match value.0 {
1073 Some(v) if !v.is_undefined() && !v.is_null() => v,
1074 _ => return Ok(None),
1075 };
1076 let js: JsClickOptions = serde_from_js(ctx, raw)?;
1077 let button = match js.button.as_deref() {
1078 None => None,
1079 Some(s) => Some(ferridriver::options::MouseButton::parse(s).ok_or_else(|| {
1080 rquickjs::Error::new_from_js_message("ferridriver", "click", format!("Unknown mouse button: {s}"))
1081 })?),
1082 };
1083 let mut modifiers = Vec::new();
1084 if let Some(list) = js.modifiers {
1085 for name in list {
1086 let m = ferridriver::options::Modifier::parse(&name).ok_or_else(|| {
1087 rquickjs::Error::new_from_js_message("ferridriver", "click", format!("Unknown modifier: {name}"))
1088 })?;
1089 modifiers.push(m);
1090 }
1091 }
1092 Ok(Some(ferridriver::options::ClickOptions {
1093 button,
1094 click_count: js.click_count,
1095 delay: js.delay,
1096 force: js.force,
1097 modifiers,
1098 no_wait_after: js.no_wait_after,
1099 position: js.position.map(Into::into),
1100 steps: js.steps,
1101 timeout: js.timeout,
1102 trial: js.trial,
1103 }))
1104}
1105
1106pub fn init_script_from_js<'js>(
1121 ctx: &Ctx<'js>,
1122 script: Value<'js>,
1123 arg: Option<Value<'js>>,
1124) -> rquickjs::Result<(ferridriver::options::InitScriptSource, Option<serde_json::Value>)> {
1125 let arg_json = match arg {
1126 None => None,
1127 Some(v) if v.is_undefined() => None,
1128 Some(v) if v.is_null() => Some(serde_json::Value::Null),
1129 Some(v) => Some(serde_from_js::<serde_json::Value>(ctx, v)?),
1130 };
1131
1132 let init = if script.is_function() {
1133 let string_global: Function<'js> = ctx.globals().get("String")?;
1136 let body: String = string_global.call((script,))?;
1137 ferridriver::options::InitScriptSource::Function { body }
1138 } else if script.is_string() {
1139 let s: String = script.get()?;
1140 ferridriver::options::InitScriptSource::Source(s)
1141 } else if script.is_object() {
1142 let obj = script
1143 .as_object()
1144 .ok_or_else(|| rquickjs::Error::new_from_js_message("ferridriver", "addInitScript", "expected object"))?;
1145 if let Ok(content) = obj.get::<_, String>("content") {
1146 ferridriver::options::InitScriptSource::Content(content)
1147 } else if let Ok(path) = obj.get::<_, String>("path") {
1148 ferridriver::options::InitScriptSource::Path(path.into())
1149 } else {
1150 return Err(rquickjs::Error::new_from_js_message(
1151 "ferridriver",
1152 "addInitScript",
1153 "Either path or content property must be present",
1154 ));
1155 }
1156 } else {
1157 return Err(rquickjs::Error::new_from_js_message(
1158 "ferridriver",
1159 "addInitScript",
1160 "script must be Function | string | { path?, content? }",
1161 ));
1162 };
1163
1164 Ok((init, arg_json))
1165}