Skip to main content

ferridriver_script/bindings/
locator.rs

1//! `LocatorJs`: JS wrapper around `ferridriver::locator::Locator`.
2
3use ferridriver::locator::Locator;
4use ferridriver::options::{FilterOptions, LocatorLike};
5use rquickjs::JsLifetime;
6use rquickjs::class::Trace;
7use rquickjs::function::Opt;
8
9use crate::bindings::convert::FerriResultExt;
10
11/// Shape of filter options read out of a JS object via prototype-aware
12/// property lookup. `has`/`hasNot` may be either a selector string or a
13/// `LocatorJs` class instance — we accept both because Playwright's own
14/// JS API does (`has: Locator` officially, but users commonly pass
15/// plain `{ selector: '...' }` shapes in tests).
16pub(crate) struct ParsedLocatorOptions {
17  pub has_text: Option<String>,
18  pub has_not_text: Option<String>,
19  pub has: Option<LocatorLike>,
20  pub has_not: Option<LocatorLike>,
21  pub visible: Option<bool>,
22}
23
24/// Pull a string value from a JS object property, ignoring missing/null.
25fn get_string<'js>(obj: &rquickjs::Object<'js>, key: &str) -> rquickjs::Result<Option<String>> {
26  let v: rquickjs::Value<'js> = obj.get(key)?;
27  if v.is_undefined() || v.is_null() {
28    return Ok(None);
29  }
30  match v.as_string() {
31    Some(s) => Ok(Some(s.to_string()?)),
32    None => Err(rquickjs::Error::new_from_js_message(
33      "filter options",
34      "field",
35      format!("{key}: expected string"),
36    )),
37  }
38}
39
40/// Pull a `LocatorLike` from a JS object property. Accepts either a
41/// `LocatorJs` class instance (we read its `inner.selector()` directly
42/// and wrap as [`LocatorLike::Locator`] for same-page checks) or any
43/// object exposing a string `.selector` property.
44fn get_locator_like<'js>(
45  ctx: &rquickjs::Ctx<'js>,
46  obj: &rquickjs::Object<'js>,
47  key: &str,
48) -> rquickjs::Result<Option<LocatorLike>> {
49  let v: rquickjs::Value<'js> = obj.get(key)?;
50  if v.is_undefined() || v.is_null() {
51    return Ok(None);
52  }
53  // Preferred path: a real `LocatorJs` class instance — gives us the
54  // full `ferridriver::Locator` so `FilterOptions::has` can enforce
55  // same-page equality in the Rust core.
56  if let Ok(class) = rquickjs::Class::<LocatorJs>::from_value(&v) {
57    let inner = class.borrow();
58    return Ok(Some(LocatorLike::Locator(inner.inner.clone())));
59  }
60  // Fallback: a plain `{ selector: '...' }` object — works but skips
61  // the same-page check (no `Page` reference available).
62  let _ = ctx;
63  if let Some(obj) = v.as_object() {
64    if let Some(sel) = get_string(obj, "selector")? {
65      return Ok(Some(LocatorLike::Selector(sel)));
66    }
67  }
68  Err(rquickjs::Error::new_from_js_message(
69    "filter options",
70    "field",
71    format!("{key}: expected Locator instance or {{ selector: string }}"),
72  ))
73}
74
75fn get_bool<'js>(obj: &rquickjs::Object<'js>, key: &str) -> rquickjs::Result<Option<bool>> {
76  let v: rquickjs::Value<'js> = obj.get(key)?;
77  if v.is_undefined() || v.is_null() {
78    return Ok(None);
79  }
80  v.as_bool()
81    .map(Some)
82    .ok_or_else(|| rquickjs::Error::new_from_js_message("filter options", "field", format!("{key}: expected boolean")))
83}
84
85pub(crate) fn parse_locator_options_public<'js>(
86  ctx: &rquickjs::Ctx<'js>,
87  value: Opt<rquickjs::Value<'js>>,
88  allow_visible: bool,
89) -> rquickjs::Result<ParsedLocatorOptions> {
90  let Some(val) = value.0 else {
91    return Ok(ParsedLocatorOptions {
92      has_text: None,
93      has_not_text: None,
94      has: None,
95      has_not: None,
96      visible: None,
97    });
98  };
99  if val.is_undefined() || val.is_null() {
100    return Ok(ParsedLocatorOptions {
101      has_text: None,
102      has_not_text: None,
103      has: None,
104      has_not: None,
105      visible: None,
106    });
107  }
108  let obj = val
109    .as_object()
110    .ok_or_else(|| rquickjs::Error::new_from_js_message("locator options", "", "expected an options object"))?;
111  Ok(ParsedLocatorOptions {
112    has_text: get_string(obj, "hasText")?,
113    has_not_text: get_string(obj, "hasNotText")?,
114    has: get_locator_like(ctx, obj, "has")?,
115    has_not: get_locator_like(ctx, obj, "hasNot")?,
116    visible: if allow_visible { get_bool(obj, "visible")? } else { None },
117  })
118}
119
120/// Whether `opts` has no fields set — used by bindings to skip the
121/// redundant `Some(default)` case before forwarding to Rust core's
122/// `locator(sel, Option<FilterOptions>)`.
123pub(crate) fn is_empty_filter(opts: &FilterOptions) -> bool {
124  opts.has_text.is_none()
125    && opts.has_not_text.is_none()
126    && opts.has.is_none()
127    && opts.has_not.is_none()
128    && opts.visible.is_none()
129}
130
131#[derive(JsLifetime, Trace)]
132#[rquickjs::class(rename = "Locator")]
133pub struct LocatorJs {
134  #[qjs(skip_trace)]
135  inner: Locator,
136}
137
138impl LocatorJs {
139  #[must_use]
140  pub fn new(inner: Locator) -> Self {
141    Self { inner }
142  }
143
144  /// Read-only access to the wrapped core `Locator` for cross-binding
145  /// consumers (e.g. the `expect()` binding lifting a `LocatorJs` into
146  /// an assertion target).
147  #[must_use]
148  pub fn inner_ref(&self) -> &Locator {
149    &self.inner
150  }
151}
152
153#[rquickjs::methods]
154impl LocatorJs {
155  // ── Chain/refine (return new Locator) ─────────────────────────────────────
156
157  /// Narrow this locator's scope.
158  ///
159  /// Full Playwright signature:
160  /// `locator(selectorOrLocator: string | Locator, options?: { has?, hasNot?, hasText?, hasNotText? }): Locator`.
161  /// The `visible` flag is the one `LocatorOptions` field NOT accepted
162  /// here — Playwright restricts it to `filter()` and the `Locator`
163  /// constructor (see
164  /// `/tmp/playwright/packages/playwright-core/src/client/locator.ts:164`).
165  #[qjs(rename = "locator")]
166  pub fn locator<'js>(
167    &self,
168    ctx: rquickjs::Ctx<'js>,
169    selector_or_locator: rquickjs::Value<'js>,
170    options: Opt<rquickjs::Value<'js>>,
171  ) -> rquickjs::Result<LocatorJs> {
172    // Lower the JS argument to a `LocatorLike`: real `LocatorJs` class →
173    // `LocatorLike::Locator` (enables same-page check); string or plain
174    // `{ selector }` object → `LocatorLike::Selector`.
175    let like: ferridriver::options::LocatorLike = if let Some(s) = selector_or_locator.as_string() {
176      ferridriver::options::LocatorLike::Selector(s.to_string()?)
177    } else if let Ok(class) = rquickjs::Class::<LocatorJs>::from_value(&selector_or_locator) {
178      ferridriver::options::LocatorLike::Locator(class.borrow().inner.clone())
179    } else if let Some(obj) = selector_or_locator.as_object() {
180      match get_string(obj, "selector")? {
181        Some(sel) => ferridriver::options::LocatorLike::Selector(sel),
182        None => {
183          return Err(rquickjs::Error::new_from_js_message(
184            "Locator",
185            "locator",
186            "expected a selector string or Locator instance",
187          ));
188        },
189      }
190    } else {
191      return Err(rquickjs::Error::new_from_js_message(
192        "Locator",
193        "locator",
194        "expected a selector string or Locator instance",
195      ));
196    };
197
198    // Rust core `Locator::locator(selOrLoc, options?)` handles the
199    // `internal:chain` encoding, cross-frame sentinel, and option
200    // application in one infallible call — script binding is a thin
201    // delegator.
202    let opts = parse_locator_options_public(&ctx, options, false)?;
203    let filter_opts = ferridriver::options::FilterOptions {
204      has_text: opts.has_text,
205      has_not_text: opts.has_not_text,
206      has: opts.has,
207      has_not: opts.has_not,
208      visible: opts.visible,
209    };
210    let filter = if is_empty_filter(&filter_opts) {
211      None
212    } else {
213      Some(filter_opts)
214    };
215    Ok(LocatorJs::new(self.inner.locator(like, filter)))
216  }
217
218  /// Playwright: `locator.filter(options?: LocatorOptions): Locator`
219  /// (`/tmp/playwright/packages/playwright-core/src/client/locator.ts:204`).
220  /// Thin delegator to Rust core's `Locator::filter`.
221  #[qjs(rename = "filter")]
222  pub fn filter<'js>(
223    &self,
224    ctx: rquickjs::Ctx<'js>,
225    options: Opt<rquickjs::Value<'js>>,
226  ) -> rquickjs::Result<LocatorJs> {
227    let parsed = parse_locator_options_public(&ctx, options, true)?;
228    let opts = FilterOptions {
229      has_text: parsed.has_text,
230      has_not_text: parsed.has_not_text,
231      has: parsed.has,
232      has_not: parsed.has_not,
233      visible: parsed.visible,
234    };
235    Ok(LocatorJs::new(self.inner.filter(&opts)))
236  }
237
238  /// Playwright: `locator.and(locator: Locator): Locator`
239  /// (`/tmp/playwright/packages/playwright-core/src/client/locator.ts` —
240  /// matches elements satisfying BOTH this and `other` on the same
241  /// element). Thin delegator to core's `Locator::and`.
242  #[qjs(rename = "and")]
243  pub fn and<'js>(&self, ctx: rquickjs::Ctx<'js>, other: rquickjs::Value<'js>) -> rquickjs::Result<LocatorJs> {
244    let _ = ctx;
245    let class = rquickjs::Class::<LocatorJs>::from_value(&other)
246      .map_err(|_| rquickjs::Error::new_from_js_message("Locator", "and", "expected a Locator instance"))?;
247    Ok(LocatorJs::new(self.inner.and(&class.borrow().inner)))
248  }
249
250  /// Playwright: `locator.or(locator: Locator): Locator` — matches
251  /// elements from EITHER selector. Thin delegator to `Locator::or`.
252  #[qjs(rename = "or")]
253  pub fn or<'js>(&self, ctx: rquickjs::Ctx<'js>, other: rquickjs::Value<'js>) -> rquickjs::Result<LocatorJs> {
254    let _ = ctx;
255    let class = rquickjs::Class::<LocatorJs>::from_value(&other)
256      .map_err(|_| rquickjs::Error::new_from_js_message("Locator", "or", "expected a Locator instance"))?;
257    Ok(LocatorJs::new(self.inner.or(&class.borrow().inner)))
258  }
259
260  /// Playwright: `locator.elementHandle(): Promise<ElementHandle>`.
261  /// Resolves and returns a pinned ElementHandle.
262  #[qjs(rename = "elementHandle")]
263  pub async fn element_handle(&self) -> rquickjs::Result<crate::bindings::element_handle::ElementHandleJs> {
264    let inner = self.inner.element_handle().await.into_js()?;
265    Ok(crate::bindings::element_handle::ElementHandleJs::new(inner))
266  }
267
268  /// Playwright: `locator.elementHandles(): Promise<ElementHandle[]>`.
269  #[qjs(rename = "elementHandles")]
270  pub async fn element_handles(&self) -> rquickjs::Result<Vec<crate::bindings::element_handle::ElementHandleJs>> {
271    let inner = self.inner.element_handles().await.into_js()?;
272    Ok(
273      inner
274        .into_iter()
275        .map(crate::bindings::element_handle::ElementHandleJs::new)
276        .collect(),
277    )
278  }
279
280  #[qjs(rename = "getByRole")]
281  pub fn get_by_role(
282    &self,
283    role: String,
284    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
285  ) -> rquickjs::Result<LocatorJs> {
286    let opts = crate::bindings::page::parse_role_options(options)?;
287    Ok(LocatorJs::new(self.inner.get_by_role(&role, &opts)))
288  }
289
290  #[qjs(rename = "getByText")]
291  pub fn get_by_text(
292    &self,
293    text: rquickjs::Value<'_>,
294    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
295  ) -> rquickjs::Result<LocatorJs> {
296    let t = crate::bindings::page::string_or_regex_from_js(text)?;
297    let opts = crate::bindings::page::parse_text_options(options);
298    Ok(LocatorJs::new(self.inner.get_by_text(&t, &opts)))
299  }
300
301  #[qjs(rename = "getByLabel")]
302  pub fn get_by_label(
303    &self,
304    text: rquickjs::Value<'_>,
305    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
306  ) -> rquickjs::Result<LocatorJs> {
307    let t = crate::bindings::page::string_or_regex_from_js(text)?;
308    let opts = crate::bindings::page::parse_text_options(options);
309    Ok(LocatorJs::new(self.inner.get_by_label(&t, &opts)))
310  }
311
312  #[qjs(rename = "getByPlaceholder")]
313  pub fn get_by_placeholder(
314    &self,
315    text: rquickjs::Value<'_>,
316    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
317  ) -> rquickjs::Result<LocatorJs> {
318    let t = crate::bindings::page::string_or_regex_from_js(text)?;
319    let opts = crate::bindings::page::parse_text_options(options);
320    Ok(LocatorJs::new(self.inner.get_by_placeholder(&t, &opts)))
321  }
322
323  #[qjs(rename = "getByAltText")]
324  pub fn get_by_alt_text(
325    &self,
326    text: rquickjs::Value<'_>,
327    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
328  ) -> rquickjs::Result<LocatorJs> {
329    let t = crate::bindings::page::string_or_regex_from_js(text)?;
330    let opts = crate::bindings::page::parse_text_options(options);
331    Ok(LocatorJs::new(self.inner.get_by_alt_text(&t, &opts)))
332  }
333
334  #[qjs(rename = "getByTitle")]
335  pub fn get_by_title(
336    &self,
337    text: rquickjs::Value<'_>,
338    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
339  ) -> rquickjs::Result<LocatorJs> {
340    let t = crate::bindings::page::string_or_regex_from_js(text)?;
341    let opts = crate::bindings::page::parse_text_options(options);
342    Ok(LocatorJs::new(self.inner.get_by_title(&t, &opts)))
343  }
344
345  #[qjs(rename = "getByTestId")]
346  pub fn get_by_test_id(&self, test_id: rquickjs::Value<'_>) -> rquickjs::Result<LocatorJs> {
347    let t = crate::bindings::page::string_or_regex_from_js(test_id)?;
348    Ok(LocatorJs::new(self.inner.get_by_test_id(&t)))
349  }
350
351  /// Playwright: `locator.contentFrame(): FrameLocator`.
352  #[qjs(rename = "contentFrame")]
353  pub fn content_frame(&self) -> crate::bindings::frame_locator::FrameLocatorJs {
354    crate::bindings::frame_locator::FrameLocatorJs::new(self.inner.content_frame())
355  }
356
357  /// Playwright: `locator.frameLocator(selector): FrameLocator`.
358  #[qjs(rename = "frameLocator")]
359  pub fn frame_locator(&self, selector: String) -> crate::bindings::frame_locator::FrameLocatorJs {
360    crate::bindings::frame_locator::FrameLocatorJs::new(self.inner.frame_locator(&selector))
361  }
362
363  /// Playwright: `locator.page(): Page`. Carries the session's
364  /// `AsyncContext` (via userdata) so `page.route` /
365  /// `page.exposeFunction` work on the returned handle.
366  #[qjs(rename = "page")]
367  pub fn page(&self, ctx: rquickjs::Ctx<'_>) -> crate::bindings::page::PageJs {
368    crate::bindings::page::pagejs_for_ctx(&ctx, self.inner.page().clone())
369  }
370
371  #[qjs(rename = "first")]
372  pub fn first(&self) -> LocatorJs {
373    LocatorJs::new(self.inner.first())
374  }
375
376  #[qjs(rename = "last")]
377  pub fn last(&self) -> LocatorJs {
378    LocatorJs::new(self.inner.last())
379  }
380
381  #[qjs(rename = "nth")]
382  pub fn nth(&self, index: i32) -> LocatorJs {
383    LocatorJs::new(self.inner.nth(index))
384  }
385
386  // ── Interaction ───────────────────────────────────────────────────────────
387
388  #[qjs(rename = "click")]
389  pub async fn click<'js>(
390    &self,
391    ctx: rquickjs::Ctx<'js>,
392    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
393  ) -> rquickjs::Result<()> {
394    let opts = crate::bindings::convert::parse_click_options(&ctx, options)?;
395    self.inner.click(opts).await.into_js()
396  }
397
398  #[qjs(rename = "dblclick")]
399  pub async fn dblclick<'js>(
400    &self,
401    ctx: rquickjs::Ctx<'js>,
402    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
403  ) -> rquickjs::Result<()> {
404    let opts = crate::bindings::convert::parse_dblclick_options(&ctx, options)?;
405    self.inner.dblclick(opts).await.into_js()
406  }
407
408  #[qjs(rename = "fill")]
409  pub async fn fill<'js>(
410    &self,
411    ctx: rquickjs::Ctx<'js>,
412    value: String,
413    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
414  ) -> rquickjs::Result<()> {
415    let opts = crate::bindings::convert::parse_fill_options(&ctx, options)?;
416    self.inner.fill(&value, opts).await.into_js()
417  }
418
419  #[qjs(rename = "clear")]
420  pub async fn clear(&self) -> rquickjs::Result<()> {
421    self.inner.clear().await.into_js()
422  }
423
424  #[qjs(rename = "type")]
425  pub async fn type_<'js>(
426    &self,
427    ctx: rquickjs::Ctx<'js>,
428    text: String,
429    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
430  ) -> rquickjs::Result<()> {
431    let opts = crate::bindings::convert::parse_type_options(&ctx, options)?;
432    self.inner.r#type(&text, opts).await.into_js()
433  }
434
435  #[qjs(rename = "pressSequentially")]
436  pub async fn press_sequentially<'js>(
437    &self,
438    ctx: rquickjs::Ctx<'js>,
439    text: String,
440    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
441  ) -> rquickjs::Result<()> {
442    let opts = crate::bindings::convert::parse_type_options(&ctx, options)?;
443    self.inner.press_sequentially(&text, opts).await.into_js()
444  }
445
446  #[qjs(rename = "press")]
447  pub async fn press<'js>(
448    &self,
449    ctx: rquickjs::Ctx<'js>,
450    key: String,
451    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
452  ) -> rquickjs::Result<()> {
453    let opts = crate::bindings::convert::parse_press_options(&ctx, options)?;
454    self.inner.press(&key, opts).await.into_js()
455  }
456
457  #[qjs(rename = "hover")]
458  pub async fn hover<'js>(
459    &self,
460    ctx: rquickjs::Ctx<'js>,
461    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
462  ) -> rquickjs::Result<()> {
463    let opts = crate::bindings::convert::parse_hover_options(&ctx, options)?;
464    self.inner.hover(opts).await.into_js()
465  }
466
467  #[qjs(rename = "tap")]
468  pub async fn tap<'js>(
469    &self,
470    ctx: rquickjs::Ctx<'js>,
471    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
472  ) -> rquickjs::Result<()> {
473    let opts = crate::bindings::convert::parse_tap_options(&ctx, options)?;
474    self.inner.tap(opts).await.into_js()
475  }
476
477  #[qjs(rename = "focus")]
478  pub async fn focus(&self) -> rquickjs::Result<()> {
479    self.inner.focus().await.into_js()
480  }
481
482  #[qjs(rename = "blur")]
483  pub async fn blur(&self) -> rquickjs::Result<()> {
484    self.inner.blur().await.into_js()
485  }
486
487  #[qjs(rename = "check")]
488  pub async fn check<'js>(
489    &self,
490    ctx: rquickjs::Ctx<'js>,
491    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
492  ) -> rquickjs::Result<()> {
493    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
494    self.inner.check(opts).await.into_js()
495  }
496
497  #[qjs(rename = "uncheck")]
498  pub async fn uncheck<'js>(
499    &self,
500    ctx: rquickjs::Ctx<'js>,
501    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
502  ) -> rquickjs::Result<()> {
503    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
504    self.inner.uncheck(opts).await.into_js()
505  }
506
507  #[qjs(rename = "setChecked")]
508  pub async fn set_checked<'js>(
509    &self,
510    ctx: rquickjs::Ctx<'js>,
511    checked: bool,
512    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
513  ) -> rquickjs::Result<()> {
514    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
515    self.inner.set_checked(checked, opts).await.into_js()
516  }
517
518  #[qjs(rename = "selectOption")]
519  pub async fn select_option<'js>(
520    &self,
521    ctx: rquickjs::Ctx<'js>,
522    values: rquickjs::Value<'js>,
523    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
524  ) -> rquickjs::Result<Vec<String>> {
525    let values = crate::bindings::convert::parse_select_option_values(&ctx, values)?;
526    let opts = crate::bindings::convert::parse_select_option_options(&ctx, options)?;
527    self.inner.select_option(values, opts).await.into_js()
528  }
529
530  /// Attach files to a `<input type=file>` this locator matches.
531  /// Accepts Playwright's full
532  /// `string | string[] | FilePayload | FilePayload[]` union.
533  #[qjs(rename = "setInputFiles")]
534  pub async fn set_input_files<'js>(
535    &self,
536    ctx: rquickjs::Ctx<'js>,
537    files: rquickjs::Value<'js>,
538    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
539  ) -> rquickjs::Result<()> {
540    let files = crate::bindings::convert::parse_input_files(&ctx, files)?;
541    let opts = crate::bindings::convert::parse_set_input_files_options(&ctx, options)?;
542    self.inner.set_input_files(files, opts).await.into_js()
543  }
544
545  #[qjs(rename = "scrollIntoViewIfNeeded")]
546  pub async fn scroll_into_view_if_needed(&self) -> rquickjs::Result<()> {
547    self.inner.scroll_into_view_if_needed().await.into_js()
548  }
549
550  /// Playwright: `locator.waitFor(options?: { state?: 'attached' |
551  /// 'detached' | 'visible' | 'hidden', timeout?: number })`. Thin
552  /// delegator to core `Locator::wait_for(WaitOptions)`.
553  #[qjs(rename = "waitFor")]
554  pub async fn wait_for<'js>(
555    &self,
556    ctx: rquickjs::Ctx<'js>,
557    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
558  ) -> rquickjs::Result<()> {
559    #[derive(serde::Deserialize, Default)]
560    #[serde(rename_all = "camelCase", default)]
561    struct JsWaitOpts {
562      state: Option<String>,
563      timeout: Option<u64>,
564    }
565    let parsed: JsWaitOpts = match options.0 {
566      Some(v) if !v.is_undefined() && !v.is_null() => crate::bindings::convert::serde_from_js(&ctx, v)?,
567      _ => JsWaitOpts::default(),
568    };
569    self
570      .inner
571      .wait_for(ferridriver::options::WaitOptions {
572        state: parsed.state,
573        timeout: parsed.timeout,
574      })
575      .await
576      .into_js()
577  }
578
579  /// Dispatch a DOM event on the element. Mirrors Playwright's
580  /// `locator.dispatchEvent(type, eventInit?, options?)`.
581  #[qjs(rename = "dispatchEvent")]
582  pub async fn dispatch_event<'js>(
583    &self,
584    ctx: rquickjs::Ctx<'js>,
585    event_type: String,
586    event_init: rquickjs::function::Opt<rquickjs::Value<'js>>,
587    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
588  ) -> rquickjs::Result<()> {
589    let init_json = match event_init.0 {
590      Some(v) if !v.is_undefined() && !v.is_null() => {
591        Some(crate::bindings::convert::serde_from_js::<serde_json::Value>(&ctx, v)?)
592      },
593      _ => None,
594    };
595    let opts = crate::bindings::convert::parse_dispatch_event_options(&ctx, options)?;
596    self.inner.dispatch_event(&event_type, init_json, opts).await.into_js()
597  }
598
599  // ── Info ──────────────────────────────────────────────────────────────────
600
601  #[qjs(rename = "count")]
602  pub async fn count(&self) -> rquickjs::Result<i32> {
603    self
604      .inner
605      .count()
606      .await
607      .into_js()
608      .map(|c| i32::try_from(c).unwrap_or(i32::MAX))
609  }
610
611  /// Playwright: `locator.screenshot(options?): Promise<Buffer>`.
612  /// Thin delegator to core `Locator::screenshot` (PNG bytes).
613  #[qjs(rename = "screenshot")]
614  pub async fn screenshot(&self) -> rquickjs::Result<Vec<u8>> {
615    self.inner.screenshot().await.into_js()
616  }
617
618  #[qjs(rename = "textContent")]
619  pub async fn text_content(&self) -> rquickjs::Result<Option<String>> {
620    self.inner.text_content().await.into_js()
621  }
622
623  #[qjs(rename = "innerText")]
624  pub async fn inner_text(&self) -> rquickjs::Result<String> {
625    self.inner.inner_text().await.into_js()
626  }
627
628  #[qjs(rename = "innerHTML")]
629  pub async fn inner_html(&self) -> rquickjs::Result<String> {
630    self.inner.inner_html().await.into_js()
631  }
632
633  #[qjs(rename = "inputValue")]
634  pub async fn input_value(&self) -> rquickjs::Result<String> {
635    self.inner.input_value().await.into_js()
636  }
637
638  /// Playwright: `locator.ariaSnapshot(options?: TimeoutOptions &
639  /// { mode?: 'ai' | 'default', depth?: number }): Promise<string>`.
640  #[qjs(rename = "ariaSnapshot")]
641  pub async fn aria_snapshot<'js>(
642    &self,
643    ctx: rquickjs::Ctx<'js>,
644    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
645  ) -> rquickjs::Result<String> {
646    let core_opts = match options.0 {
647      Some(v) if !v.is_undefined() && !v.is_null() => {
648        #[derive(serde::Deserialize, Default)]
649        #[serde(rename_all = "camelCase", default)]
650        struct JsAria {
651          mode: Option<String>,
652          depth: Option<i32>,
653          timeout: Option<u64>,
654        }
655        let p: JsAria = crate::bindings::convert::serde_from_js(&ctx, v)?;
656        ferridriver::options::AriaSnapshotOptions {
657          mode: Some(ferridriver::options::AriaSnapshotMode::from_opt_str(p.mode.as_deref())),
658          depth: p.depth,
659          timeout: p.timeout,
660        }
661      },
662      _ => ferridriver::options::AriaSnapshotOptions::default(),
663    };
664    self.inner.aria_snapshot(core_opts).await.into_js()
665  }
666
667  #[qjs(rename = "getAttribute")]
668  pub async fn get_attribute(&self, name: String) -> rquickjs::Result<Option<String>> {
669    self.inner.get_attribute(&name).await.into_js()
670  }
671
672  #[qjs(rename = "isVisible")]
673  pub async fn is_visible(&self) -> rquickjs::Result<bool> {
674    self.inner.is_visible().await.into_js()
675  }
676
677  #[qjs(rename = "isHidden")]
678  pub async fn is_hidden(&self) -> rquickjs::Result<bool> {
679    self.inner.is_hidden().await.into_js()
680  }
681
682  #[qjs(rename = "isEnabled")]
683  pub async fn is_enabled(&self) -> rquickjs::Result<bool> {
684    self.inner.is_enabled().await.into_js()
685  }
686
687  #[qjs(rename = "isDisabled")]
688  pub async fn is_disabled(&self) -> rquickjs::Result<bool> {
689    self.inner.is_disabled().await.into_js()
690  }
691
692  #[qjs(rename = "isChecked")]
693  pub async fn is_checked(&self) -> rquickjs::Result<bool> {
694    self.inner.is_checked().await.into_js()
695  }
696
697  #[qjs(rename = "isEditable")]
698  pub async fn is_editable(&self) -> rquickjs::Result<bool> {
699    self.inner.is_editable().await.into_js()
700  }
701
702  #[qjs(rename = "isAttached")]
703  pub async fn is_attached(&self) -> rquickjs::Result<bool> {
704    self.inner.is_attached().await.into_js()
705  }
706
707  // ── Drag ──────────────────────────────────────────────────────────────────
708
709  /// Drag this element to `target`. Mirrors Playwright's
710  /// `locator.dragTo(target, options?)` per
711  /// `/tmp/playwright/packages/playwright-core/types/types.d.ts:13293`.
712  ///
713  /// Accepts `{ force?, noWaitAfter?, sourcePosition?, targetPosition?,
714  /// steps?, timeout?, trial? }`. `strict` is omitted here (present on
715  /// Playwright's `page.dragAndDrop` options but not `locator.dragTo`,
716  /// because the locator already carries its own strict flag).
717  #[qjs(rename = "dragTo")]
718  pub async fn drag_to<'js>(
719    &self,
720    ctx: rquickjs::Ctx<'js>,
721    target: rquickjs::Class<'js, LocatorJs>,
722    options: Opt<rquickjs::Value<'js>>,
723  ) -> rquickjs::Result<()> {
724    let target_inner = target.borrow().inner.clone();
725    let opts = crate::bindings::page::parse_drag_options(&ctx, options)?;
726    self.inner.drag_to(&target_inner, opts).await.into_js()
727  }
728
729  // ── All variants ──────────────────────────────────────────────────────────
730
731  #[qjs(rename = "allTextContents")]
732  pub async fn all_text_contents(&self) -> rquickjs::Result<Vec<String>> {
733    self.inner.all_text_contents().await.into_js()
734  }
735
736  #[qjs(rename = "allInnerTexts")]
737  pub async fn all_inner_texts(&self) -> rquickjs::Result<Vec<String>> {
738    self.inner.all_inner_texts().await.into_js()
739  }
740
741  // ── Evaluation ────────────────────────────────────────────────────────────
742
743  /// Playwright: `locator.evaluate(pageFunction, arg?, options?): Promise<R>`.
744  #[qjs(rename = "evaluate")]
745  pub async fn evaluate<'js>(
746    &self,
747    ctx: rquickjs::Ctx<'js>,
748    page_function: rquickjs::Value<'js>,
749    arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
750  ) -> rquickjs::Result<rquickjs::Value<'js>> {
751    let (source, is_fn) = crate::bindings::convert::extract_page_function(&ctx, page_function)?;
752    let serialized = crate::bindings::convert::quickjs_arg_to_serialized(&ctx, arg.0)?;
753    let result = self.inner.evaluate(&source, serialized, is_fn, None).await.into_js()?;
754    crate::bindings::convert::serialized_value_to_quickjs(&ctx, &result)
755  }
756
757  /// Playwright: `locator.evaluateHandle(pageFunction, arg?, options?): Promise<JSHandle>`.
758  #[qjs(rename = "evaluateHandle")]
759  pub async fn evaluate_handle<'js>(
760    &self,
761    ctx: rquickjs::Ctx<'js>,
762    page_function: rquickjs::Value<'js>,
763    arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
764  ) -> rquickjs::Result<crate::bindings::js_handle::JSHandleJs> {
765    let (source, is_fn) = crate::bindings::convert::extract_page_function(&ctx, page_function)?;
766    let serialized = crate::bindings::convert::quickjs_arg_to_serialized(&ctx, arg.0)?;
767    let handle = self
768      .inner
769      .evaluate_handle(&source, serialized, is_fn, None)
770      .await
771      .into_js()?;
772    Ok(crate::bindings::js_handle::JSHandleJs::new(handle))
773  }
774
775  /// Playwright: `locator.evaluateAll(pageFunction, arg?): Promise<R>`.
776  #[qjs(rename = "evaluateAll")]
777  pub async fn evaluate_all<'js>(
778    &self,
779    ctx: rquickjs::Ctx<'js>,
780    page_function: rquickjs::Value<'js>,
781    arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
782  ) -> rquickjs::Result<rquickjs::Value<'js>> {
783    let (source, is_fn) = crate::bindings::convert::extract_page_function(&ctx, page_function)?;
784    let serialized = crate::bindings::convert::quickjs_arg_to_serialized(&ctx, arg.0)?;
785    let result = self.inner.evaluate_all(&source, serialized, is_fn).await.into_js()?;
786    crate::bindings::convert::serialized_value_to_quickjs(&ctx, &result)
787  }
788}