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<'js>(
210    &self,
211    ctx: rquickjs::Ctx<'js>,
212    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
213  ) -> rquickjs::Result<()> {
214    let opts = crate::bindings::convert::parse_click_options(&ctx, options)?;
215    self.inner.click(opts).await.into_js()
216  }
217
218  #[qjs(rename = "dblclick")]
219  pub async fn dblclick<'js>(
220    &self,
221    ctx: rquickjs::Ctx<'js>,
222    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
223  ) -> rquickjs::Result<()> {
224    let opts = crate::bindings::convert::parse_dblclick_options(&ctx, options)?;
225    self.inner.dblclick(opts).await.into_js()
226  }
227
228  #[qjs(rename = "hover")]
229  pub async fn hover<'js>(
230    &self,
231    ctx: rquickjs::Ctx<'js>,
232    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
233  ) -> rquickjs::Result<()> {
234    let opts = crate::bindings::convert::parse_hover_options(&ctx, options)?;
235    self.inner.hover(opts).await.into_js()
236  }
237
238  #[qjs(rename = "type")]
239  pub async fn type_str<'js>(
240    &self,
241    ctx: rquickjs::Ctx<'js>,
242    text: String,
243    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
244  ) -> rquickjs::Result<()> {
245    let opts = crate::bindings::convert::parse_type_options(&ctx, options)?;
246    self.inner.type_str(&text, opts).await.into_js()
247  }
248
249  #[qjs(rename = "focus")]
250  pub async fn focus(&self) -> rquickjs::Result<()> {
251    self.inner.focus().await.into_js()
252  }
253
254  #[qjs(rename = "scrollIntoViewIfNeeded")]
255  pub async fn scroll_into_view_if_needed(&self) -> rquickjs::Result<()> {
256    self.inner.scroll_into_view_if_needed().await.into_js()
257  }
258
259  /// Playwright: `elementHandle.screenshot(opts?)`. Today accepts
260  /// `{ type?: 'png'|'jpeg'|'webp' }` via the `opts.type` field;
261  /// additional `ScreenshotOpts` fields are carried at the core layer
262  /// and take effect once the locator-level screenshot gets the full
263  /// bag.
264  #[qjs(rename = "screenshot")]
265  pub async fn screenshot<'js>(
266    &self,
267    ctx: rquickjs::Ctx<'js>,
268    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
269  ) -> rquickjs::Result<Vec<u8>> {
270    let format = parse_screenshot_format(&ctx, options)?;
271    self.inner.screenshot(format).await.into_js()
272  }
273
274  // ── $eval / $$eval (Playwright parity) ───────────────────────────────
275
276  /// Playwright: `elementHandle.$eval(selector, pageFunction, arg?)`
277  /// (`/tmp/playwright/packages/playwright-core/src/client/elementHandle.ts:215`).
278  #[qjs(rename = "$eval")]
279  pub async fn dollar_eval<'js>(
280    &self,
281    ctx: rquickjs::Ctx<'js>,
282    selector: String,
283    page_function: rquickjs::Value<'js>,
284    arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
285  ) -> rquickjs::Result<rquickjs::Value<'js>> {
286    let (source, _is_fn) = extract_page_function(&ctx, page_function)?;
287    let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
288    let result = self
289      .inner
290      .eval_on_selector(&selector, &source, serialized)
291      .await
292      .into_js()?;
293    serialized_value_to_quickjs(&ctx, &result)
294  }
295
296  /// Playwright: `elementHandle.$$eval(selector, pageFunction, arg?)`
297  /// (`/tmp/playwright/packages/playwright-core/src/client/elementHandle.ts:220`).
298  #[qjs(rename = "$$eval")]
299  pub async fn dollar_dollar_eval<'js>(
300    &self,
301    ctx: rquickjs::Ctx<'js>,
302    selector: String,
303    page_function: rquickjs::Value<'js>,
304    arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
305  ) -> rquickjs::Result<rquickjs::Value<'js>> {
306    let (source, _is_fn) = extract_page_function(&ctx, page_function)?;
307    let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
308    let result = self
309      .inner
310      .eval_on_selector_all(&selector, &source, serialized)
311      .await
312      .into_js()?;
313    serialized_value_to_quickjs(&ctx, &result)
314  }
315
316  // ── $ / $$ (query shortcuts — Playwright parity) ─────────────────────
317
318  /// Playwright: `elementHandle.$(selector): Promise<ElementHandle | null>`
319  /// (`/tmp/playwright/packages/playwright-core/src/client/elementHandle.ts:206`).
320  #[qjs(rename = "$")]
321  pub async fn query_selector(&self, selector: String) -> rquickjs::Result<Option<ElementHandleJs>> {
322    let maybe = self.inner.query_selector(&selector).await.into_js()?;
323    Ok(maybe.map(ElementHandleJs::new))
324  }
325
326  /// Playwright: `elementHandle.$$(selector): Promise<ElementHandle[]>`
327  /// (`/tmp/playwright/packages/playwright-core/src/client/elementHandle.ts:210`).
328  #[qjs(rename = "$$")]
329  pub async fn query_selector_all(&self, selector: String) -> rquickjs::Result<Vec<ElementHandleJs>> {
330    let handles = self.inner.query_selector_all(&selector).await.into_js()?;
331    Ok(handles.into_iter().map(ElementHandleJs::new).collect())
332  }
333
334  // ── Frame accessors ──────────────────────────────────────────────────
335
336  /// Playwright: `elementHandle.ownerFrame(): Promise<Frame | null>`.
337  #[qjs(rename = "ownerFrame")]
338  pub async fn owner_frame(&self) -> rquickjs::Result<Option<crate::bindings::frame::FrameJs>> {
339    let maybe = self.inner.owner_frame().await.into_js()?;
340    Ok(maybe.map(crate::bindings::frame::FrameJs::new))
341  }
342
343  /// Playwright: `elementHandle.contentFrame(): Promise<Frame | null>`.
344  #[qjs(rename = "contentFrame")]
345  pub async fn content_frame(&self) -> rquickjs::Result<Option<crate::bindings::frame::FrameJs>> {
346    let maybe = self.inner.content_frame().await.into_js()?;
347    Ok(maybe.map(crate::bindings::frame::FrameJs::new))
348  }
349
350  // ── Wait helpers ─────────────────────────────────────────────────────
351
352  /// Playwright: `elementHandle.waitForElementState(state, options?)`.
353  #[qjs(rename = "waitForElementState")]
354  pub async fn wait_for_element_state(
355    &self,
356    state: String,
357    timeout: rquickjs::function::Opt<f64>,
358  ) -> rquickjs::Result<()> {
359    let st = ferridriver::ElementState::parse(&state).into_js()?;
360    let timeout_ms = timeout.0.map(|ms| ms as u64);
361    self.inner.wait_for_element_state(st, timeout_ms).await.into_js()
362  }
363
364  /// Playwright: `elementHandle.waitForSelector(selector, options?)`.
365  #[qjs(rename = "waitForSelector")]
366  pub async fn wait_for_selector(
367    &self,
368    selector: String,
369    timeout: rquickjs::function::Opt<f64>,
370  ) -> rquickjs::Result<Option<ElementHandleJs>> {
371    let timeout_ms = timeout.0.map(|ms| ms as u64);
372    let maybe = self.inner.wait_for_selector(&selector, timeout_ms).await.into_js()?;
373    Ok(maybe.map(ElementHandleJs::new))
374  }
375
376  // ── Action methods (temp-tag bridge) ─────────────────────────────────
377
378  /// Playwright: `elementHandle.fill(value, options?)`.
379  #[qjs(rename = "fill")]
380  pub async fn fill<'js>(
381    &self,
382    ctx: rquickjs::Ctx<'js>,
383    value: String,
384    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
385  ) -> rquickjs::Result<()> {
386    let opts = crate::bindings::convert::parse_fill_options(&ctx, options)?;
387    self.inner.fill(&value, opts).await.into_js()
388  }
389
390  /// Playwright: `elementHandle.check(options?)`.
391  #[qjs(rename = "check")]
392  pub async fn check<'js>(
393    &self,
394    ctx: rquickjs::Ctx<'js>,
395    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
396  ) -> rquickjs::Result<()> {
397    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
398    self.inner.check(opts).await.into_js()
399  }
400
401  /// Playwright: `elementHandle.uncheck(options?)`.
402  #[qjs(rename = "uncheck")]
403  pub async fn uncheck<'js>(
404    &self,
405    ctx: rquickjs::Ctx<'js>,
406    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
407  ) -> rquickjs::Result<()> {
408    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
409    self.inner.uncheck(opts).await.into_js()
410  }
411
412  /// Playwright: `elementHandle.setChecked(checked, options?)`.
413  #[qjs(rename = "setChecked")]
414  pub async fn set_checked<'js>(
415    &self,
416    ctx: rquickjs::Ctx<'js>,
417    checked: bool,
418    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
419  ) -> rquickjs::Result<()> {
420    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
421    self.inner.set_checked(checked, opts).await.into_js()
422  }
423
424  /// Playwright: `elementHandle.tap(options?)`.
425  #[qjs(rename = "tap")]
426  pub async fn tap<'js>(
427    &self,
428    ctx: rquickjs::Ctx<'js>,
429    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
430  ) -> rquickjs::Result<()> {
431    let opts = crate::bindings::convert::parse_tap_options(&ctx, options)?;
432    self.inner.tap(opts).await.into_js()
433  }
434
435  /// Playwright: `elementHandle.press(key, options?)`.
436  #[qjs(rename = "press")]
437  pub async fn press<'js>(
438    &self,
439    ctx: rquickjs::Ctx<'js>,
440    key: String,
441    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
442  ) -> rquickjs::Result<()> {
443    let opts = crate::bindings::convert::parse_press_options(&ctx, options)?;
444    self.inner.press(&key, opts).await.into_js()
445  }
446
447  /// Playwright: `elementHandle.dispatchEvent(type, eventInit?)`.
448  #[qjs(rename = "dispatchEvent")]
449  pub async fn dispatch_event<'js>(
450    &self,
451    ctx: rquickjs::Ctx<'js>,
452    event_type: String,
453    event_init: rquickjs::function::Opt<rquickjs::Value<'js>>,
454    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
455  ) -> rquickjs::Result<()> {
456    let init_json = match event_init.0 {
457      Some(v) if !v.is_undefined() && !v.is_null() => {
458        Some(crate::bindings::convert::serde_from_js::<serde_json::Value>(&ctx, v)?)
459      },
460      _ => None,
461    };
462    let opts = crate::bindings::convert::parse_dispatch_event_options(&ctx, options)?;
463    self.inner.dispatch_event(&event_type, init_json, opts).await.into_js()
464  }
465
466  /// Playwright: `elementHandle.selectOption(values, options?)`.
467  #[qjs(rename = "selectOption")]
468  pub async fn select_option<'js>(
469    &self,
470    ctx: rquickjs::Ctx<'js>,
471    values: rquickjs::Value<'js>,
472    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
473  ) -> rquickjs::Result<Vec<String>> {
474    let values = crate::bindings::convert::parse_select_option_values(&ctx, values)?;
475    let opts = crate::bindings::convert::parse_select_option_options(&ctx, options)?;
476    self.inner.select_option(values, opts).await.into_js()
477  }
478
479  /// Playwright: `elementHandle.selectText(options?)`.
480  #[qjs(rename = "selectText")]
481  pub async fn select_text(&self) -> rquickjs::Result<()> {
482    self.inner.select_text().await.into_js()
483  }
484
485  /// Playwright: `elementHandle.setInputFiles(files, options?)`.
486  #[qjs(rename = "setInputFiles")]
487  pub async fn set_input_files<'js>(
488    &self,
489    ctx: rquickjs::Ctx<'js>,
490    files: rquickjs::Value<'js>,
491    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
492  ) -> rquickjs::Result<()> {
493    let files = crate::bindings::convert::parse_input_files(&ctx, files)?;
494    let opts = crate::bindings::convert::parse_set_input_files_options(&ctx, options)?;
495    self.inner.set_input_files(files, opts).await.into_js()
496  }
497}