Skip to main content

ferridriver_script/bindings/
element_handle.rs

1//! `ElementHandleJs`: QuickJS wrapper around `ferridriver::ElementHandle`.
2//!
3//! Phase-C surface covers lifecycle — `dispose`, `isDisposed`, `asJSHandle`
4//! — enough to exercise the per-backend release paths from `run_script`.
5//! Phase E bolts the ~25 Playwright DOM methods on top of this same class.
6
7use ferridriver::ElementHandle;
8use ferridriver::backend::ImageFormat;
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/// Extract `{ type?: 'png' | 'jpeg' | 'webp' }` from a user-supplied
17/// screenshot options bag. Defaults to PNG when the caller omits the
18/// bag or the `type` field. Matches Playwright's
19/// `elementHandle.screenshot(options?)` surface.
20fn parse_screenshot_format<'js>(
21  _ctx: &rquickjs::Ctx<'js>,
22  options: rquickjs::function::Opt<rquickjs::Value<'js>>,
23) -> rquickjs::Result<ImageFormat> {
24  let Some(opts_val) = options.0 else {
25    return Ok(ImageFormat::Png);
26  };
27  if opts_val.is_undefined() || opts_val.is_null() {
28    return Ok(ImageFormat::Png);
29  }
30  let Some(obj) = opts_val.as_object() else {
31    return Ok(ImageFormat::Png);
32  };
33  let type_field: Option<String> = obj.get("type").ok();
34  match type_field.as_deref() {
35    None | Some("" | "png") => Ok(ImageFormat::Png),
36    Some("jpeg" | "jpg") => Ok(ImageFormat::Jpeg),
37    Some("webp") => Ok(ImageFormat::Webp),
38    Some(other) => Err(rquickjs::Error::new_from_js_message(
39      "screenshot",
40      "invalid format",
41      &format!("unsupported screenshot type {other:?}; expected 'png' | 'jpeg' | 'webp'"),
42    )),
43  }
44}
45
46/// QuickJS-visible wrapper around a core [`ElementHandle`].
47#[derive(JsLifetime, Trace)]
48#[rquickjs::class(rename = "ElementHandle")]
49pub struct ElementHandleJs {
50  #[qjs(skip_trace)]
51  inner: ElementHandle,
52}
53
54impl ElementHandleJs {
55  #[must_use]
56  pub fn new(inner: ElementHandle) -> Self {
57    Self { inner }
58  }
59
60  #[must_use]
61  pub fn inner(&self) -> &ElementHandle {
62    &self.inner
63  }
64}
65
66#[rquickjs::methods]
67impl ElementHandleJs {
68  /// Playwright `elementHandle.isDisposed(): boolean` — METHOD (not a
69  /// property). LLM-generated code calls `eh.isDisposed()` with parens.
70  #[qjs(rename = "isDisposed")]
71  pub fn is_disposed(&self) -> bool {
72    self.inner.is_disposed()
73  }
74
75  /// Release the underlying remote element. Idempotent.
76  #[qjs(rename = "dispose")]
77  pub async fn dispose(&self) -> rquickjs::Result<()> {
78    self.inner.dispose().await.into_js()
79  }
80
81  /// Companion [`crate::bindings::js_handle::JSHandleJs`] sharing the
82  /// same remote reference. Disposing either releases the remote and
83  /// latches both into the disposed state.
84  #[qjs(rename = "asJSHandle")]
85  pub fn as_js_handle(&self) -> crate::bindings::js_handle::JSHandleJs {
86    crate::bindings::js_handle::JSHandleJs::new(self.inner.as_js_handle().clone())
87  }
88
89  /// Playwright: `elementHandle.evaluate(pageFunction, arg?): Promise<R>`.
90  #[qjs(rename = "evaluate")]
91  pub async fn evaluate<'js>(
92    &self,
93    ctx: rquickjs::Ctx<'js>,
94    page_function: rquickjs::Value<'js>,
95    arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
96  ) -> rquickjs::Result<rquickjs::Value<'js>> {
97    let (source, is_fn) = extract_page_function(&ctx, page_function)?;
98    let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
99    let result = self
100      .inner
101      .as_js_handle()
102      .evaluate(&source, serialized, is_fn)
103      .await
104      .into_js()?;
105    serialized_value_to_quickjs(&ctx, &result)
106  }
107
108  /// Playwright: `elementHandle.evaluateHandle(pageFunction, arg?): Promise<JSHandle>`.
109  #[qjs(rename = "evaluateHandle")]
110  pub async fn evaluate_handle<'js>(
111    &self,
112    ctx: rquickjs::Ctx<'js>,
113    page_function: rquickjs::Value<'js>,
114    arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
115  ) -> rquickjs::Result<crate::bindings::js_handle::JSHandleJs> {
116    let (source, is_fn) = extract_page_function(&ctx, page_function)?;
117    let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
118    let handle = self
119      .inner
120      .as_js_handle()
121      .evaluate_handle(&source, serialized, is_fn)
122      .await
123      .into_js()?;
124    Ok(crate::bindings::js_handle::JSHandleJs::new(handle))
125  }
126
127  // ── Content reads (Phase E) ──────────────────────────────────────────
128
129  #[qjs(rename = "innerHTML")]
130  pub async fn inner_html(&self) -> rquickjs::Result<String> {
131    self.inner.inner_html().await.into_js()
132  }
133
134  #[qjs(rename = "innerText")]
135  pub async fn inner_text(&self) -> rquickjs::Result<String> {
136    self.inner.inner_text().await.into_js()
137  }
138
139  #[qjs(rename = "textContent")]
140  pub async fn text_content(&self) -> rquickjs::Result<Option<String>> {
141    self.inner.text_content().await.into_js()
142  }
143
144  #[qjs(rename = "getAttribute")]
145  pub async fn get_attribute(&self, name: String) -> rquickjs::Result<Option<String>> {
146    self.inner.get_attribute(&name).await.into_js()
147  }
148
149  #[qjs(rename = "inputValue")]
150  pub async fn input_value(&self) -> rquickjs::Result<String> {
151    self.inner.input_value().await.into_js()
152  }
153
154  // ── State predicates (Phase E) ───────────────────────────────────────
155
156  #[qjs(rename = "isVisible")]
157  pub async fn is_visible(&self) -> rquickjs::Result<bool> {
158    self.inner.is_visible().await.into_js()
159  }
160
161  #[qjs(rename = "isHidden")]
162  pub async fn is_hidden(&self) -> rquickjs::Result<bool> {
163    self.inner.is_hidden().await.into_js()
164  }
165
166  #[qjs(rename = "isDisabled")]
167  pub async fn is_disabled(&self) -> rquickjs::Result<bool> {
168    self.inner.is_disabled().await.into_js()
169  }
170
171  #[qjs(rename = "isEnabled")]
172  pub async fn is_enabled(&self) -> rquickjs::Result<bool> {
173    self.inner.is_enabled().await.into_js()
174  }
175
176  #[qjs(rename = "isChecked")]
177  pub async fn is_checked(&self) -> rquickjs::Result<bool> {
178    self.inner.is_checked().await.into_js()
179  }
180
181  #[qjs(rename = "isEditable")]
182  pub async fn is_editable(&self) -> rquickjs::Result<bool> {
183    self.inner.is_editable().await.into_js()
184  }
185
186  // ── Geometry (Phase E) ───────────────────────────────────────────────
187
188  /// Playwright: `elementHandle.boundingBox()`. Returns a plain object
189  /// `{x, y, width, height}` or `null`.
190  #[qjs(rename = "boundingBox")]
191  pub async fn bounding_box<'js>(&self, ctx: rquickjs::Ctx<'js>) -> rquickjs::Result<rquickjs::Value<'js>> {
192    let bbox = self.inner.bounding_box().await.into_js()?;
193    match bbox {
194      None => Ok(rquickjs::Value::new_null(ctx)),
195      Some(b) => {
196        let obj = rquickjs::Object::new(ctx.clone())?;
197        obj.set("x", b.x)?;
198        obj.set("y", b.y)?;
199        obj.set("width", b.width)?;
200        obj.set("height", b.height)?;
201        Ok(obj.into_value())
202      },
203    }
204  }
205
206  // ── Actions (Phase E) ────────────────────────────────────────────────
207
208  #[qjs(rename = "click")]
209  pub async fn click(&self) -> rquickjs::Result<()> {
210    self.inner.click().await.into_js()
211  }
212
213  #[qjs(rename = "dblclick")]
214  pub async fn dblclick(&self) -> rquickjs::Result<()> {
215    self.inner.dblclick().await.into_js()
216  }
217
218  #[qjs(rename = "hover")]
219  pub async fn hover(&self) -> rquickjs::Result<()> {
220    self.inner.hover().await.into_js()
221  }
222
223  #[qjs(rename = "type")]
224  pub async fn type_str(&self, text: String) -> rquickjs::Result<()> {
225    self.inner.type_str(&text).await.into_js()
226  }
227
228  #[qjs(rename = "focus")]
229  pub async fn focus(&self) -> rquickjs::Result<()> {
230    self.inner.focus().await.into_js()
231  }
232
233  #[qjs(rename = "scrollIntoViewIfNeeded")]
234  pub async fn scroll_into_view_if_needed(&self) -> rquickjs::Result<()> {
235    self.inner.scroll_into_view_if_needed().await.into_js()
236  }
237
238  /// Playwright: `elementHandle.screenshot(opts?)`. Today accepts
239  /// `{ type?: 'png'|'jpeg'|'webp' }` via the `opts.type` field;
240  /// additional `ScreenshotOpts` fields are carried at the core layer
241  /// and take effect once the locator-level screenshot gets the full
242  /// bag.
243  #[qjs(rename = "screenshot")]
244  pub async fn screenshot<'js>(
245    &self,
246    ctx: rquickjs::Ctx<'js>,
247    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
248  ) -> rquickjs::Result<Vec<u8>> {
249    let format = parse_screenshot_format(&ctx, options)?;
250    self.inner.screenshot(format).await.into_js()
251  }
252
253  // ── $eval / $$eval (Playwright parity) ───────────────────────────────
254
255  /// Playwright: `elementHandle.$eval(selector, pageFunction, arg?)`
256  /// (`/tmp/playwright/packages/playwright-core/src/client/elementHandle.ts:215`).
257  #[qjs(rename = "$eval")]
258  pub async fn dollar_eval<'js>(
259    &self,
260    ctx: rquickjs::Ctx<'js>,
261    selector: String,
262    page_function: rquickjs::Value<'js>,
263    arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
264  ) -> rquickjs::Result<rquickjs::Value<'js>> {
265    let (source, _is_fn) = extract_page_function(&ctx, page_function)?;
266    let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
267    let result = self
268      .inner
269      .eval_on_selector(&selector, &source, serialized)
270      .await
271      .into_js()?;
272    serialized_value_to_quickjs(&ctx, &result)
273  }
274
275  /// Playwright: `elementHandle.$$eval(selector, pageFunction, arg?)`
276  /// (`/tmp/playwright/packages/playwright-core/src/client/elementHandle.ts:220`).
277  #[qjs(rename = "$$eval")]
278  pub async fn dollar_dollar_eval<'js>(
279    &self,
280    ctx: rquickjs::Ctx<'js>,
281    selector: String,
282    page_function: rquickjs::Value<'js>,
283    arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
284  ) -> rquickjs::Result<rquickjs::Value<'js>> {
285    let (source, _is_fn) = extract_page_function(&ctx, page_function)?;
286    let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
287    let result = self
288      .inner
289      .eval_on_selector_all(&selector, &source, serialized)
290      .await
291      .into_js()?;
292    serialized_value_to_quickjs(&ctx, &result)
293  }
294
295  // ── Frame accessors ──────────────────────────────────────────────────
296
297  /// Playwright: `elementHandle.ownerFrame(): Promise<Frame | null>`.
298  #[qjs(rename = "ownerFrame")]
299  pub async fn owner_frame(&self) -> rquickjs::Result<Option<crate::bindings::frame::FrameJs>> {
300    let maybe = self.inner.owner_frame().await.into_js()?;
301    Ok(maybe.map(crate::bindings::frame::FrameJs::new))
302  }
303
304  /// Playwright: `elementHandle.contentFrame(): Promise<Frame | null>`.
305  #[qjs(rename = "contentFrame")]
306  pub async fn content_frame(&self) -> rquickjs::Result<Option<crate::bindings::frame::FrameJs>> {
307    let maybe = self.inner.content_frame().await.into_js()?;
308    Ok(maybe.map(crate::bindings::frame::FrameJs::new))
309  }
310
311  // ── Wait helpers ─────────────────────────────────────────────────────
312
313  /// Playwright: `elementHandle.waitForElementState(state, options?)`.
314  #[qjs(rename = "waitForElementState")]
315  pub async fn wait_for_element_state(
316    &self,
317    state: String,
318    timeout: rquickjs::function::Opt<f64>,
319  ) -> rquickjs::Result<()> {
320    let st = ferridriver::ElementState::parse(&state).into_js()?;
321    let timeout_ms = timeout.0.map(|ms| ms as u64);
322    self.inner.wait_for_element_state(st, timeout_ms).await.into_js()
323  }
324
325  /// Playwright: `elementHandle.waitForSelector(selector, options?)`.
326  #[qjs(rename = "waitForSelector")]
327  pub async fn wait_for_selector(
328    &self,
329    selector: String,
330    timeout: rquickjs::function::Opt<f64>,
331  ) -> rquickjs::Result<Option<ElementHandleJs>> {
332    let timeout_ms = timeout.0.map(|ms| ms as u64);
333    let maybe = self.inner.wait_for_selector(&selector, timeout_ms).await.into_js()?;
334    Ok(maybe.map(ElementHandleJs::new))
335  }
336
337  // ── Action methods (temp-tag bridge) ─────────────────────────────────
338
339  /// Playwright: `elementHandle.fill(value, options?)`.
340  #[qjs(rename = "fill")]
341  pub async fn fill<'js>(
342    &self,
343    ctx: rquickjs::Ctx<'js>,
344    value: String,
345    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
346  ) -> rquickjs::Result<()> {
347    let opts = crate::bindings::convert::parse_fill_options(&ctx, options)?;
348    self.inner.fill(&value, opts).await.into_js()
349  }
350
351  /// Playwright: `elementHandle.check(options?)`.
352  #[qjs(rename = "check")]
353  pub async fn check<'js>(
354    &self,
355    ctx: rquickjs::Ctx<'js>,
356    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
357  ) -> rquickjs::Result<()> {
358    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
359    self.inner.check(opts).await.into_js()
360  }
361
362  /// Playwright: `elementHandle.uncheck(options?)`.
363  #[qjs(rename = "uncheck")]
364  pub async fn uncheck<'js>(
365    &self,
366    ctx: rquickjs::Ctx<'js>,
367    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
368  ) -> rquickjs::Result<()> {
369    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
370    self.inner.uncheck(opts).await.into_js()
371  }
372
373  /// Playwright: `elementHandle.setChecked(checked, options?)`.
374  #[qjs(rename = "setChecked")]
375  pub async fn set_checked<'js>(
376    &self,
377    ctx: rquickjs::Ctx<'js>,
378    checked: bool,
379    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
380  ) -> rquickjs::Result<()> {
381    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
382    self.inner.set_checked(checked, opts).await.into_js()
383  }
384
385  /// Playwright: `elementHandle.tap(options?)`.
386  #[qjs(rename = "tap")]
387  pub async fn tap<'js>(
388    &self,
389    ctx: rquickjs::Ctx<'js>,
390    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
391  ) -> rquickjs::Result<()> {
392    let opts = crate::bindings::convert::parse_tap_options(&ctx, options)?;
393    self.inner.tap(opts).await.into_js()
394  }
395
396  /// Playwright: `elementHandle.press(key, options?)`.
397  #[qjs(rename = "press")]
398  pub async fn press<'js>(
399    &self,
400    ctx: rquickjs::Ctx<'js>,
401    key: String,
402    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
403  ) -> rquickjs::Result<()> {
404    let opts = crate::bindings::convert::parse_press_options(&ctx, options)?;
405    self.inner.press(&key, opts).await.into_js()
406  }
407
408  /// Playwright: `elementHandle.dispatchEvent(type, eventInit?)`.
409  #[qjs(rename = "dispatchEvent")]
410  pub async fn dispatch_event<'js>(
411    &self,
412    ctx: rquickjs::Ctx<'js>,
413    event_type: String,
414    event_init: rquickjs::function::Opt<rquickjs::Value<'js>>,
415    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
416  ) -> rquickjs::Result<()> {
417    let init_json = match event_init.0 {
418      Some(v) if !v.is_undefined() && !v.is_null() => {
419        Some(crate::bindings::convert::serde_from_js::<serde_json::Value>(&ctx, v)?)
420      },
421      _ => None,
422    };
423    let opts = crate::bindings::convert::parse_dispatch_event_options(&ctx, options)?;
424    self.inner.dispatch_event(&event_type, init_json, opts).await.into_js()
425  }
426
427  /// Playwright: `elementHandle.selectOption(values, options?)`.
428  #[qjs(rename = "selectOption")]
429  pub async fn select_option<'js>(
430    &self,
431    ctx: rquickjs::Ctx<'js>,
432    values: rquickjs::Value<'js>,
433    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
434  ) -> rquickjs::Result<Vec<String>> {
435    let values = crate::bindings::convert::parse_select_option_values(&ctx, values)?;
436    let opts = crate::bindings::convert::parse_select_option_options(&ctx, options)?;
437    self.inner.select_option(values, opts).await.into_js()
438  }
439
440  /// Playwright: `elementHandle.selectText(options?)`.
441  #[qjs(rename = "selectText")]
442  pub async fn select_text(&self) -> rquickjs::Result<()> {
443    self.inner.select_text().await.into_js()
444  }
445
446  /// Playwright: `elementHandle.setInputFiles(files, options?)`.
447  #[qjs(rename = "setInputFiles")]
448  pub async fn set_input_files<'js>(
449    &self,
450    ctx: rquickjs::Ctx<'js>,
451    files: rquickjs::Value<'js>,
452    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
453  ) -> rquickjs::Result<()> {
454    let files = crate::bindings::convert::parse_input_files(&ctx, files)?;
455    let opts = crate::bindings::convert::parse_set_input_files_options(&ctx, options)?;
456    self.inner.set_input_files(files, opts).await.into_js()
457  }
458}