Skip to main content

ferridriver_script/bindings/
js_handle.rs

1//! `JSHandleJs`: QuickJS wrapper around `ferridriver::JSHandle`.
2//!
3//! Mirrors the NAPI surface in `crates/ferridriver-node/src/js_handle.rs`
4//! and Playwright's `JSHandle` TS interface. Phase-C surface covers lifecycle
5//! only — `dispose`, `isDisposed`, `asElement`. Phase D extends with
6//! `evaluate`, `evaluateHandle`, `getProperties`, `getProperty`, `jsonValue`.
7
8use ferridriver::JSHandle;
9use rquickjs::JsLifetime;
10use rquickjs::class::Trace;
11
12use crate::bindings::convert::{
13  FerriResultExt, extract_page_function, quickjs_arg_to_serialized, serialized_value_to_quickjs,
14};
15
16/// QuickJS-visible wrapper around a core [`JSHandle`].
17///
18/// Held without `Arc` because [`JSHandle`] is itself `Clone` and shares
19/// its dispose flag through an internal `Arc<AtomicBool>`.
20#[derive(JsLifetime, Trace)]
21#[rquickjs::class(rename = "JSHandle")]
22pub struct JSHandleJs {
23  #[qjs(skip_trace)]
24  inner: JSHandle,
25}
26
27impl JSHandleJs {
28  #[must_use]
29  pub fn new(inner: JSHandle) -> Self {
30    Self { inner }
31  }
32
33  #[must_use]
34  pub fn inner(&self) -> &JSHandle {
35    &self.inner
36  }
37}
38
39#[rquickjs::methods]
40impl JSHandleJs {
41  /// Playwright `jsHandle.isDisposed(): boolean` — METHOD (not
42  /// property): callers write `h.isDisposed()` with parens.
43  #[qjs(rename = "isDisposed")]
44  pub fn is_disposed(&self) -> bool {
45    self.inner.is_disposed()
46  }
47
48  /// Release the underlying remote object. Playwright:
49  /// `jsHandle.dispose(): Promise<void>`. Idempotent — calling twice
50  /// short-circuits the second time.
51  #[qjs(rename = "dispose")]
52  pub async fn dispose(&self) -> rquickjs::Result<()> {
53    self.inner.dispose().await.into_js()
54  }
55
56  /// Playwright: `jsHandle.asElement(): ElementHandle | null`
57  /// (`/tmp/playwright/packages/playwright-core/src/client/jsHandle.ts:65`).
58  /// Inspects the remote value and returns a fresh `ElementHandle`
59  /// (sharing this handle's dispose flag) when the value is a DOM
60  /// Node, otherwise `null`.
61  /// Playwright: `jsHandle.asElement(): ElementHandle | null`.
62  /// Explicit `null` (NOT `undefined`) — rquickjs maps `Option::None` to
63  /// JS `undefined`, so we hand back a JS Value carrying a real `null`.
64  #[qjs(rename = "asElement")]
65  pub fn as_element<'js>(&self, ctx: rquickjs::Ctx<'js>) -> rquickjs::Result<rquickjs::Value<'js>> {
66    use rquickjs::class::Class;
67    use rquickjs::{IntoJs, Value};
68    match self.inner.as_element() {
69      Some(e) => {
70        let wrapper = crate::bindings::element_handle::ElementHandleJs::new(e);
71        let inst = Class::instance(ctx.clone(), wrapper)?;
72        inst.into_js(&ctx)
73      },
74      None => Ok(Value::new_null(ctx)),
75    }
76  }
77
78  /// Playwright: `jsHandle.jsonValue(): Promise<T>`. Rich types
79  /// (`Date` / `RegExp` / `BigInt` / `URL` / `Error` / typed arrays /
80  /// `NaN` / `±Infinity` / `undefined` / `-0`) arrive as native JS —
81  /// matches Playwright's `parseSerializedValue` at
82  /// `/tmp/playwright/packages/playwright-core/src/protocol/serializers.ts:19`.
83  #[qjs(rename = "jsonValue")]
84  pub async fn json_value<'js>(&self, ctx: rquickjs::Ctx<'js>) -> rquickjs::Result<rquickjs::Value<'js>> {
85    let v = self.inner.json_value().await.into_js()?;
86    serialized_value_to_quickjs(&ctx, &v)
87  }
88
89  /// Playwright: `jsHandle.getProperty(propertyName): Promise<JSHandle>`.
90  #[qjs(rename = "getProperty")]
91  pub async fn get_property(&self, name: String) -> rquickjs::Result<JSHandleJs> {
92    let h = self.inner.get_property(&name).await.into_js()?;
93    Ok(JSHandleJs::new(h))
94  }
95
96  /// Playwright: `jsHandle.getProperties(): Promise<Map<string, JSHandle>>`.
97  /// The QuickJS surface returns a plain object `{ [key]: JSHandle }`
98  /// mirroring the NAPI `Record<string, JSHandle>` shape — ergonomic
99  /// on the JS side without losing per-key handle identity.
100  #[qjs(rename = "getProperties")]
101  pub async fn get_properties<'js>(&self, ctx: rquickjs::Ctx<'js>) -> rquickjs::Result<rquickjs::Value<'js>> {
102    let pairs = self.inner.get_properties().await.into_js()?;
103    let obj = rquickjs::Object::new(ctx.clone())?;
104    for (k, h) in pairs {
105      let handle_js = rquickjs::Class::instance(ctx.clone(), JSHandleJs::new(h))?;
106      obj.set(k, handle_js)?;
107    }
108    Ok(obj.into_value())
109  }
110
111  /// Playwright: `jsHandle.evaluate(pageFunction, arg?): Promise<R>`.
112  /// `pageFunction` accepts a string or a JS function — matches
113  /// Playwright's `String(pageFunction)` + `typeof fn === 'function'`.
114  #[qjs(rename = "evaluate")]
115  pub async fn evaluate<'js>(
116    &self,
117    ctx: rquickjs::Ctx<'js>,
118    page_function: rquickjs::Value<'js>,
119    arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
120  ) -> rquickjs::Result<rquickjs::Value<'js>> {
121    let (source, is_fn) = extract_page_function(&ctx, page_function)?;
122    let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
123    let result = self.inner.evaluate(&source, serialized, is_fn).await.into_js()?;
124    serialized_value_to_quickjs(&ctx, &result)
125  }
126
127  /// Playwright: `jsHandle.evaluateHandle(pageFunction, arg?): Promise<JSHandle>`.
128  #[qjs(rename = "evaluateHandle")]
129  pub async fn evaluate_handle<'js>(
130    &self,
131    ctx: rquickjs::Ctx<'js>,
132    page_function: rquickjs::Value<'js>,
133    arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
134  ) -> rquickjs::Result<JSHandleJs> {
135    let (source, is_fn) = extract_page_function(&ctx, page_function)?;
136    let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
137    let handle = self.inner.evaluate_handle(&source, serialized, is_fn).await.into_js()?;
138    Ok(JSHandleJs::new(handle))
139  }
140}