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
75/// Parse `Locator.highlight`'s optional `{ style }` bag. `style` is
76/// `string | Record<string, string | number>`. A string becomes
77/// [`ferridriver::options::HighlightStyle::Css`]; an object becomes
78/// `Object` with each value rendered to CSS text (numbers stringified,
79/// strings verbatim; other value types skipped). Parsed synchronously so
80/// no `!Send` JS value is held across the async `highlight` await.
81fn parse_highlight_style(
82  options: Opt<rquickjs::Value<'_>>,
83) -> rquickjs::Result<Option<ferridriver::options::HighlightStyle>> {
84  let Some(val) = options.0 else {
85    return Ok(None);
86  };
87  let Some(obj) = val.as_object() else {
88    return Ok(None);
89  };
90  let style: rquickjs::Value<'_> = obj.get("style")?;
91  if style.is_undefined() || style.is_null() {
92    return Ok(None);
93  }
94  if let Some(s) = style.as_string() {
95    return Ok(Some(ferridriver::options::HighlightStyle::Css(s.to_string()?)));
96  }
97  if let Some(map) = style.as_object() {
98    let mut entries = Vec::new();
99    for key_res in map.keys::<String>() {
100      let key = key_res?;
101      let v: rquickjs::Value<'_> = map.get(&key)?;
102      let rendered = if let Some(s) = v.as_string() {
103        s.to_string()?
104      } else if let Some(n) = v.as_number() {
105        // Match `cssObjectToString`'s template-literal: integers print
106        // without a trailing `.0`.
107        if n.fract() == 0.0 && n.is_finite() {
108          format!("{}", n as i64)
109        } else {
110          n.to_string()
111        }
112      } else {
113        continue;
114      };
115      entries.push((key, rendered));
116    }
117    return Ok(Some(ferridriver::options::HighlightStyle::Object(entries)));
118  }
119  Ok(None)
120}
121
122fn get_bool<'js>(obj: &rquickjs::Object<'js>, key: &str) -> rquickjs::Result<Option<bool>> {
123  let v: rquickjs::Value<'js> = obj.get(key)?;
124  if v.is_undefined() || v.is_null() {
125    return Ok(None);
126  }
127  v.as_bool()
128    .map(Some)
129    .ok_or_else(|| rquickjs::Error::new_from_js_message("filter options", "field", format!("{key}: expected boolean")))
130}
131
132pub(crate) fn parse_locator_options_public<'js>(
133  ctx: &rquickjs::Ctx<'js>,
134  value: Opt<rquickjs::Value<'js>>,
135  allow_visible: bool,
136) -> rquickjs::Result<ParsedLocatorOptions> {
137  let Some(val) = value.0 else {
138    return Ok(ParsedLocatorOptions {
139      has_text: None,
140      has_not_text: None,
141      has: None,
142      has_not: None,
143      visible: None,
144    });
145  };
146  if val.is_undefined() || val.is_null() {
147    return Ok(ParsedLocatorOptions {
148      has_text: None,
149      has_not_text: None,
150      has: None,
151      has_not: None,
152      visible: None,
153    });
154  }
155  let obj = val
156    .as_object()
157    .ok_or_else(|| rquickjs::Error::new_from_js_message("locator options", "", "expected an options object"))?;
158  Ok(ParsedLocatorOptions {
159    has_text: get_string(obj, "hasText")?,
160    has_not_text: get_string(obj, "hasNotText")?,
161    has: get_locator_like(ctx, obj, "has")?,
162    has_not: get_locator_like(ctx, obj, "hasNot")?,
163    visible: if allow_visible { get_bool(obj, "visible")? } else { None },
164  })
165}
166
167/// Whether `opts` has no fields set — used by bindings to skip the
168/// redundant `Some(default)` case before forwarding to Rust core's
169/// `locator(sel, Option<FilterOptions>)`.
170pub(crate) fn is_empty_filter(opts: &FilterOptions) -> bool {
171  opts.has_text.is_none()
172    && opts.has_not_text.is_none()
173    && opts.has.is_none()
174    && opts.has_not.is_none()
175    && opts.visible.is_none()
176}
177
178#[derive(JsLifetime, Trace)]
179#[rquickjs::class(rename = "Locator")]
180pub struct LocatorJs {
181  #[qjs(skip_trace)]
182  inner: Locator,
183}
184
185impl LocatorJs {
186  #[must_use]
187  pub fn new(inner: Locator) -> Self {
188    Self { inner }
189  }
190
191  /// Read-only access to the wrapped core `Locator` for cross-binding
192  /// consumers (e.g. the `expect()` binding lifting a `LocatorJs` into
193  /// an assertion target).
194  #[must_use]
195  pub fn inner_ref(&self) -> &Locator {
196    &self.inner
197  }
198}
199
200#[rquickjs::methods]
201impl LocatorJs {
202  /// The resolved selector string for this locator. Mirrors the NAPI
203  /// `Locator.selector` getter; used by `{ selector }`-style locator
204  /// round-tripping and by `normalize()` callers reading the canonical form.
205  #[qjs(get, rename = "selector")]
206  pub fn selector(&self) -> String {
207    self.inner.selector().to_string()
208  }
209
210  /// Whether this locator is in strict mode (mirrors NAPI `isStrict`).
211  #[qjs(get, rename = "isStrict")]
212  pub fn is_strict(&self) -> bool {
213    self.inner.is_strict()
214  }
215
216  /// Returns a copy of this locator with strict-mode toggled.
217  #[qjs(rename = "setStrict")]
218  pub fn set_strict(&self, strict: bool) -> LocatorJs {
219    LocatorJs::new(self.inner.strict(strict))
220  }
221
222  /// Playwright: `locator.selectText(options?)`. Selects the element's text.
223  #[qjs(rename = "selectText")]
224  pub async fn select_text(&self) -> rquickjs::Result<()> {
225    self.inner.select_text().await.into_js()
226  }
227
228  /// Playwright: `locator.click({ button: 'right' })` shorthand.
229  #[qjs(rename = "rightClick")]
230  pub async fn right_click(&self) -> rquickjs::Result<()> {
231    self.inner.right_click().await.into_js()
232  }
233
234  /// Playwright: `locator.boundingBox()`. Returns `{x, y, width, height}` or `null`.
235  #[qjs(rename = "boundingBox")]
236  pub async fn bounding_box<'js>(&self, ctx: rquickjs::Ctx<'js>) -> rquickjs::Result<rquickjs::Value<'js>> {
237    match self.inner.bounding_box().await.into_js()? {
238      None => Ok(rquickjs::Value::new_null(ctx)),
239      Some(b) => {
240        let obj = rquickjs::Object::new(ctx.clone())?;
241        obj.set("x", b.x)?;
242        obj.set("y", b.y)?;
243        obj.set("width", b.width)?;
244        obj.set("height", b.height)?;
245        Ok(obj.into_value())
246      },
247    }
248  }
249
250  // ── Chain/refine (return new Locator) ─────────────────────────────────────
251
252  /// Narrow this locator's scope.
253  ///
254  /// Full Playwright signature:
255  /// `locator(selectorOrLocator: string | Locator, options?: { has?, hasNot?, hasText?, hasNotText? }): Locator`.
256  /// The `visible` flag is the one `LocatorOptions` field NOT accepted
257  /// here — Playwright restricts it to `filter()` and the `Locator`
258  /// constructor (see
259  /// `/tmp/playwright/packages/playwright-core/src/client/locator.ts:164`).
260  #[qjs(rename = "locator")]
261  pub fn locator<'js>(
262    &self,
263    ctx: rquickjs::Ctx<'js>,
264    selector_or_locator: rquickjs::Value<'js>,
265    options: Opt<rquickjs::Value<'js>>,
266  ) -> rquickjs::Result<LocatorJs> {
267    // Lower the JS argument to a `LocatorLike`: real `LocatorJs` class →
268    // `LocatorLike::Locator` (enables same-page check); string or plain
269    // `{ selector }` object → `LocatorLike::Selector`.
270    let like: ferridriver::options::LocatorLike = if let Some(s) = selector_or_locator.as_string() {
271      ferridriver::options::LocatorLike::Selector(s.to_string()?)
272    } else if let Ok(class) = rquickjs::Class::<LocatorJs>::from_value(&selector_or_locator) {
273      ferridriver::options::LocatorLike::Locator(class.borrow().inner.clone())
274    } else if let Some(obj) = selector_or_locator.as_object() {
275      match get_string(obj, "selector")? {
276        Some(sel) => ferridriver::options::LocatorLike::Selector(sel),
277        None => {
278          return Err(rquickjs::Error::new_from_js_message(
279            "Locator",
280            "locator",
281            "expected a selector string or Locator instance",
282          ));
283        },
284      }
285    } else {
286      return Err(rquickjs::Error::new_from_js_message(
287        "Locator",
288        "locator",
289        "expected a selector string or Locator instance",
290      ));
291    };
292
293    // Rust core `Locator::locator(selOrLoc, options?)` handles the
294    // `internal:chain` encoding, cross-frame sentinel, and option
295    // application in one infallible call — script binding is a thin
296    // delegator.
297    let opts = parse_locator_options_public(&ctx, options, false)?;
298    let filter_opts = ferridriver::options::FilterOptions {
299      has_text: opts.has_text,
300      has_not_text: opts.has_not_text,
301      has: opts.has,
302      has_not: opts.has_not,
303      visible: opts.visible,
304    };
305    let filter = if is_empty_filter(&filter_opts) {
306      None
307    } else {
308      Some(filter_opts)
309    };
310    Ok(LocatorJs::new(self.inner.locator(like, filter)))
311  }
312
313  /// Playwright: `locator.filter(options?: LocatorOptions): Locator`
314  /// (`/tmp/playwright/packages/playwright-core/src/client/locator.ts:204`).
315  /// Thin delegator to Rust core's `Locator::filter`.
316  #[qjs(rename = "filter")]
317  pub fn filter<'js>(
318    &self,
319    ctx: rquickjs::Ctx<'js>,
320    options: Opt<rquickjs::Value<'js>>,
321  ) -> rquickjs::Result<LocatorJs> {
322    let parsed = parse_locator_options_public(&ctx, options, true)?;
323    let opts = FilterOptions {
324      has_text: parsed.has_text,
325      has_not_text: parsed.has_not_text,
326      has: parsed.has,
327      has_not: parsed.has_not,
328      visible: parsed.visible,
329    };
330    Ok(LocatorJs::new(self.inner.filter(&opts)))
331  }
332
333  /// Playwright: `locator.and(locator: Locator): Locator`
334  /// (`/tmp/playwright/packages/playwright-core/src/client/locator.ts` —
335  /// matches elements satisfying BOTH this and `other` on the same
336  /// element). Thin delegator to core's `Locator::and`.
337  #[qjs(rename = "and")]
338  pub fn and<'js>(&self, ctx: rquickjs::Ctx<'js>, other: rquickjs::Value<'js>) -> rquickjs::Result<LocatorJs> {
339    let _ = ctx;
340    let class = rquickjs::Class::<LocatorJs>::from_value(&other)
341      .map_err(|_| rquickjs::Error::new_from_js_message("Locator", "and", "expected a Locator instance"))?;
342    Ok(LocatorJs::new(self.inner.and(&class.borrow().inner)))
343  }
344
345  /// Playwright: `locator.or(locator: Locator): Locator` — matches
346  /// elements from EITHER selector. Thin delegator to `Locator::or`.
347  #[qjs(rename = "or")]
348  pub fn or<'js>(&self, ctx: rquickjs::Ctx<'js>, other: rquickjs::Value<'js>) -> rquickjs::Result<LocatorJs> {
349    let _ = ctx;
350    let class = rquickjs::Class::<LocatorJs>::from_value(&other)
351      .map_err(|_| rquickjs::Error::new_from_js_message("Locator", "or", "expected a Locator instance"))?;
352    Ok(LocatorJs::new(self.inner.or(&class.borrow().inner)))
353  }
354
355  /// Playwright: `locator.elementHandle(): Promise<ElementHandle>`.
356  /// Resolves and returns a pinned ElementHandle.
357  #[qjs(rename = "elementHandle")]
358  pub async fn element_handle(&self) -> rquickjs::Result<crate::bindings::element_handle::ElementHandleJs> {
359    let inner = self.inner.element_handle().await.into_js()?;
360    Ok(crate::bindings::element_handle::ElementHandleJs::new(inner))
361  }
362
363  /// Playwright: `locator.elementHandles(): Promise<ElementHandle[]>`.
364  #[qjs(rename = "elementHandles")]
365  pub async fn element_handles(&self) -> rquickjs::Result<Vec<crate::bindings::element_handle::ElementHandleJs>> {
366    let inner = self.inner.element_handles().await.into_js()?;
367    Ok(
368      inner
369        .into_iter()
370        .map(crate::bindings::element_handle::ElementHandleJs::new)
371        .collect(),
372    )
373  }
374
375  #[qjs(rename = "getByRole")]
376  pub fn get_by_role(
377    &self,
378    role: String,
379    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
380  ) -> rquickjs::Result<LocatorJs> {
381    let opts = crate::bindings::page::parse_role_options(options)?;
382    Ok(LocatorJs::new(self.inner.get_by_role(&role, &opts)))
383  }
384
385  #[qjs(rename = "getByText")]
386  pub fn get_by_text(
387    &self,
388    text: rquickjs::Value<'_>,
389    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
390  ) -> rquickjs::Result<LocatorJs> {
391    let t = crate::bindings::page::string_or_regex_from_js(text)?;
392    let opts = crate::bindings::page::parse_text_options(options);
393    Ok(LocatorJs::new(self.inner.get_by_text(&t, &opts)))
394  }
395
396  #[qjs(rename = "getByLabel")]
397  pub fn get_by_label(
398    &self,
399    text: rquickjs::Value<'_>,
400    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
401  ) -> rquickjs::Result<LocatorJs> {
402    let t = crate::bindings::page::string_or_regex_from_js(text)?;
403    let opts = crate::bindings::page::parse_text_options(options);
404    Ok(LocatorJs::new(self.inner.get_by_label(&t, &opts)))
405  }
406
407  #[qjs(rename = "getByPlaceholder")]
408  pub fn get_by_placeholder(
409    &self,
410    text: rquickjs::Value<'_>,
411    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
412  ) -> rquickjs::Result<LocatorJs> {
413    let t = crate::bindings::page::string_or_regex_from_js(text)?;
414    let opts = crate::bindings::page::parse_text_options(options);
415    Ok(LocatorJs::new(self.inner.get_by_placeholder(&t, &opts)))
416  }
417
418  #[qjs(rename = "getByAltText")]
419  pub fn get_by_alt_text(
420    &self,
421    text: rquickjs::Value<'_>,
422    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
423  ) -> rquickjs::Result<LocatorJs> {
424    let t = crate::bindings::page::string_or_regex_from_js(text)?;
425    let opts = crate::bindings::page::parse_text_options(options);
426    Ok(LocatorJs::new(self.inner.get_by_alt_text(&t, &opts)))
427  }
428
429  #[qjs(rename = "getByTitle")]
430  pub fn get_by_title(
431    &self,
432    text: rquickjs::Value<'_>,
433    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
434  ) -> rquickjs::Result<LocatorJs> {
435    let t = crate::bindings::page::string_or_regex_from_js(text)?;
436    let opts = crate::bindings::page::parse_text_options(options);
437    Ok(LocatorJs::new(self.inner.get_by_title(&t, &opts)))
438  }
439
440  #[qjs(rename = "getByTestId")]
441  pub fn get_by_test_id(&self, test_id: rquickjs::Value<'_>) -> rquickjs::Result<LocatorJs> {
442    let t = crate::bindings::page::string_or_regex_from_js(test_id)?;
443    Ok(LocatorJs::new(self.inner.get_by_test_id(&t)))
444  }
445
446  /// Playwright: `locator.contentFrame(): FrameLocator`.
447  #[qjs(rename = "contentFrame")]
448  pub fn content_frame(&self) -> crate::bindings::frame_locator::FrameLocatorJs {
449    crate::bindings::frame_locator::FrameLocatorJs::new(self.inner.content_frame())
450  }
451
452  /// Playwright: `locator.frameLocator(selector): FrameLocator`.
453  #[qjs(rename = "frameLocator")]
454  pub fn frame_locator(&self, selector: String) -> crate::bindings::frame_locator::FrameLocatorJs {
455    crate::bindings::frame_locator::FrameLocatorJs::new(self.inner.frame_locator(&selector))
456  }
457
458  /// Playwright: `locator.page(): Page`. Carries the session's
459  /// `AsyncContext` (via userdata) so `page.route` /
460  /// `page.exposeFunction` work on the returned handle.
461  #[qjs(rename = "page")]
462  pub fn page(&self, ctx: rquickjs::Ctx<'_>) -> crate::bindings::page::PageJs {
463    crate::bindings::page::pagejs_for_ctx(&ctx, self.inner.page().clone())
464  }
465
466  #[qjs(rename = "first")]
467  pub fn first(&self) -> LocatorJs {
468    LocatorJs::new(self.inner.first())
469  }
470
471  #[qjs(rename = "last")]
472  pub fn last(&self) -> LocatorJs {
473    LocatorJs::new(self.inner.last())
474  }
475
476  #[qjs(rename = "nth")]
477  pub fn nth(&self, index: i32) -> LocatorJs {
478    LocatorJs::new(self.inner.nth(index))
479  }
480
481  // ── Interaction ───────────────────────────────────────────────────────────
482
483  #[qjs(rename = "click")]
484  pub async fn click<'js>(
485    &self,
486    ctx: rquickjs::Ctx<'js>,
487    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
488  ) -> rquickjs::Result<()> {
489    let opts = crate::bindings::convert::parse_click_options(&ctx, options)?;
490    self.inner.click(opts).await.into_js()
491  }
492
493  #[qjs(rename = "dblclick")]
494  pub async fn dblclick<'js>(
495    &self,
496    ctx: rquickjs::Ctx<'js>,
497    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
498  ) -> rquickjs::Result<()> {
499    let opts = crate::bindings::convert::parse_dblclick_options(&ctx, options)?;
500    self.inner.dblclick(opts).await.into_js()
501  }
502
503  #[qjs(rename = "fill")]
504  pub async fn fill<'js>(
505    &self,
506    ctx: rquickjs::Ctx<'js>,
507    value: String,
508    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
509  ) -> rquickjs::Result<()> {
510    let opts = crate::bindings::convert::parse_fill_options(&ctx, options)?;
511    self.inner.fill(&value, opts).await.into_js()
512  }
513
514  #[qjs(rename = "clear")]
515  pub async fn clear(&self) -> rquickjs::Result<()> {
516    self.inner.clear().await.into_js()
517  }
518
519  /// Playwright: `highlight(options?: { style?: string | Record<string,
520  /// string | number> }): Promise<Disposable>`
521  /// (`/tmp/playwright/packages/playwright-core/src/client/locator.ts:158`).
522  /// Shows the element-highlight overlay; returns a `Disposable` whose
523  /// `dispose()` hides it. The optional `style` is parsed synchronously
524  /// (the JS scope is `!Send`) into [`ferridriver::options::HighlightStyle`]
525  /// before the async body forwards to core.
526  #[qjs(rename = "highlight")]
527  pub async fn highlight(
528    &self,
529    options: Opt<rquickjs::Value<'_>>,
530  ) -> rquickjs::Result<crate::bindings::disposable::DisposableJs> {
531    let style = parse_highlight_style(options)?;
532    let disposable = self.inner.highlight(style).await.into_js()?;
533    Ok(crate::bindings::disposable::DisposableJs::new(disposable))
534  }
535
536  /// Playwright: `hideHighlight(): Promise<void>`
537  /// (`/tmp/playwright/packages/playwright-core/src/client/locator.ts:164`).
538  #[qjs(rename = "hideHighlight")]
539  pub async fn hide_highlight(&self) -> rquickjs::Result<()> {
540    self.inner.hide_highlight().await.into_js()
541  }
542
543  #[qjs(rename = "type")]
544  pub async fn type_<'js>(
545    &self,
546    ctx: rquickjs::Ctx<'js>,
547    text: String,
548    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
549  ) -> rquickjs::Result<()> {
550    let opts = crate::bindings::convert::parse_type_options(&ctx, options)?;
551    self.inner.r#type(&text, opts).await.into_js()
552  }
553
554  #[qjs(rename = "pressSequentially")]
555  pub async fn press_sequentially<'js>(
556    &self,
557    ctx: rquickjs::Ctx<'js>,
558    text: String,
559    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
560  ) -> rquickjs::Result<()> {
561    let opts = crate::bindings::convert::parse_type_options(&ctx, options)?;
562    self.inner.press_sequentially(&text, opts).await.into_js()
563  }
564
565  #[qjs(rename = "press")]
566  pub async fn press<'js>(
567    &self,
568    ctx: rquickjs::Ctx<'js>,
569    key: String,
570    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
571  ) -> rquickjs::Result<()> {
572    let opts = crate::bindings::convert::parse_press_options(&ctx, options)?;
573    self.inner.press(&key, opts).await.into_js()
574  }
575
576  #[qjs(rename = "hover")]
577  pub async fn hover<'js>(
578    &self,
579    ctx: rquickjs::Ctx<'js>,
580    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
581  ) -> rquickjs::Result<()> {
582    let opts = crate::bindings::convert::parse_hover_options(&ctx, options)?;
583    self.inner.hover(opts).await.into_js()
584  }
585
586  #[qjs(rename = "tap")]
587  pub async fn tap<'js>(
588    &self,
589    ctx: rquickjs::Ctx<'js>,
590    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
591  ) -> rquickjs::Result<()> {
592    let opts = crate::bindings::convert::parse_tap_options(&ctx, options)?;
593    self.inner.tap(opts).await.into_js()
594  }
595
596  #[qjs(rename = "focus")]
597  pub async fn focus(&self) -> rquickjs::Result<()> {
598    self.inner.focus().await.into_js()
599  }
600
601  #[qjs(rename = "blur")]
602  pub async fn blur(&self) -> rquickjs::Result<()> {
603    self.inner.blur().await.into_js()
604  }
605
606  #[qjs(rename = "check")]
607  pub async fn check<'js>(
608    &self,
609    ctx: rquickjs::Ctx<'js>,
610    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
611  ) -> rquickjs::Result<()> {
612    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
613    self.inner.check(opts).await.into_js()
614  }
615
616  #[qjs(rename = "uncheck")]
617  pub async fn uncheck<'js>(
618    &self,
619    ctx: rquickjs::Ctx<'js>,
620    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
621  ) -> rquickjs::Result<()> {
622    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
623    self.inner.uncheck(opts).await.into_js()
624  }
625
626  #[qjs(rename = "setChecked")]
627  pub async fn set_checked<'js>(
628    &self,
629    ctx: rquickjs::Ctx<'js>,
630    checked: bool,
631    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
632  ) -> rquickjs::Result<()> {
633    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
634    self.inner.set_checked(checked, opts).await.into_js()
635  }
636
637  #[qjs(rename = "selectOption")]
638  pub async fn select_option<'js>(
639    &self,
640    ctx: rquickjs::Ctx<'js>,
641    values: rquickjs::Value<'js>,
642    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
643  ) -> rquickjs::Result<Vec<String>> {
644    let values = crate::bindings::convert::parse_select_option_values(&ctx, values)?;
645    let opts = crate::bindings::convert::parse_select_option_options(&ctx, options)?;
646    self.inner.select_option(values, opts).await.into_js()
647  }
648
649  /// Attach files to a `<input type=file>` this locator matches.
650  /// Accepts Playwright's full
651  /// `string | string[] | FilePayload | FilePayload[]` union.
652  #[qjs(rename = "setInputFiles")]
653  pub async fn set_input_files<'js>(
654    &self,
655    ctx: rquickjs::Ctx<'js>,
656    files: rquickjs::Value<'js>,
657    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
658  ) -> rquickjs::Result<()> {
659    let files = crate::bindings::convert::parse_input_files(&ctx, files)?;
660    let opts = crate::bindings::convert::parse_set_input_files_options(&ctx, options)?;
661    self.inner.set_input_files(files, opts).await.into_js()
662  }
663
664  /// Drop a file/data payload onto this element. Mirrors Playwright's
665  /// `locator.drop(payload, options?)` per `client/locator.ts:129`.
666  /// `payload` is the native `{ files?, data? }` shape; `options` is the
667  /// trimmed `{ modifiers?, position?, timeout? }` bag.
668  #[qjs(rename = "drop")]
669  pub async fn drop<'js>(
670    &self,
671    ctx: rquickjs::Ctx<'js>,
672    payload: rquickjs::Value<'js>,
673    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
674  ) -> rquickjs::Result<()> {
675    let payload = crate::bindings::convert::parse_drop_payload(&ctx, payload)?;
676    let opts = crate::bindings::convert::parse_drop_options(&ctx, options)?;
677    self.inner.drop(payload, opts).await.into_js()
678  }
679
680  #[qjs(rename = "scrollIntoViewIfNeeded")]
681  pub async fn scroll_into_view_if_needed(&self) -> rquickjs::Result<()> {
682    self.inner.scroll_into_view_if_needed().await.into_js()
683  }
684
685  /// Playwright: `locator.waitFor(options?: { state?: 'attached' |
686  /// 'detached' | 'visible' | 'hidden', timeout?: number })`. Thin
687  /// delegator to core `Locator::wait_for(WaitOptions)`.
688  #[qjs(rename = "waitFor")]
689  pub async fn wait_for<'js>(
690    &self,
691    ctx: rquickjs::Ctx<'js>,
692    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
693  ) -> rquickjs::Result<()> {
694    #[derive(serde::Deserialize, Default)]
695    #[serde(rename_all = "camelCase", default)]
696    struct JsWaitOpts {
697      state: Option<String>,
698      timeout: Option<u64>,
699    }
700    let parsed: JsWaitOpts = match options.0 {
701      Some(v) if !v.is_undefined() && !v.is_null() => crate::bindings::convert::serde_from_js(&ctx, v)?,
702      _ => JsWaitOpts::default(),
703    };
704    self
705      .inner
706      .wait_for(ferridriver::options::WaitOptions {
707        state: parsed.state,
708        timeout: parsed.timeout,
709      })
710      .await
711      .into_js()
712  }
713
714  /// Dispatch a DOM event on the element. Mirrors Playwright's
715  /// `locator.dispatchEvent(type, eventInit?, options?)`.
716  #[qjs(rename = "dispatchEvent")]
717  pub async fn dispatch_event<'js>(
718    &self,
719    ctx: rquickjs::Ctx<'js>,
720    event_type: String,
721    event_init: rquickjs::function::Opt<rquickjs::Value<'js>>,
722    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
723  ) -> rquickjs::Result<()> {
724    let init_json = match event_init.0 {
725      Some(v) if !v.is_undefined() && !v.is_null() => {
726        Some(crate::bindings::convert::serde_from_js::<serde_json::Value>(&ctx, v)?)
727      },
728      _ => None,
729    };
730    let opts = crate::bindings::convert::parse_dispatch_event_options(&ctx, options)?;
731    self.inner.dispatch_event(&event_type, init_json, opts).await.into_js()
732  }
733
734  // ── Info ──────────────────────────────────────────────────────────────────
735
736  #[qjs(rename = "count")]
737  pub async fn count(&self) -> rquickjs::Result<i32> {
738    self
739      .inner
740      .count()
741      .await
742      .into_js()
743      .map(|c| i32::try_from(c).unwrap_or(i32::MAX))
744  }
745
746  /// Playwright: `locator.normalize(): Promise<Locator>`. Resolves the
747  /// selector to its canonical recorder/codegen form and returns a new
748  /// locator built from it.
749  #[qjs(rename = "normalize")]
750  pub async fn normalize(&self) -> rquickjs::Result<LocatorJs> {
751    self.inner.normalize().await.into_js().map(LocatorJs::new)
752  }
753
754  /// Playwright: `locator.screenshot(options?): Promise<Buffer>`.
755  /// Thin delegator to core `Locator::screenshot` (PNG bytes).
756  #[qjs(rename = "screenshot")]
757  pub async fn screenshot(&self) -> rquickjs::Result<Vec<u8>> {
758    self.inner.screenshot().await.into_js()
759  }
760
761  #[qjs(rename = "textContent")]
762  pub async fn text_content(&self) -> rquickjs::Result<Option<String>> {
763    self.inner.text_content().await.into_js()
764  }
765
766  #[qjs(rename = "innerText")]
767  pub async fn inner_text(&self) -> rquickjs::Result<String> {
768    self.inner.inner_text().await.into_js()
769  }
770
771  #[qjs(rename = "innerHTML")]
772  pub async fn inner_html(&self) -> rquickjs::Result<String> {
773    self.inner.inner_html().await.into_js()
774  }
775
776  #[qjs(rename = "inputValue")]
777  pub async fn input_value(&self) -> rquickjs::Result<String> {
778    self.inner.input_value().await.into_js()
779  }
780
781  /// Playwright: `locator.ariaSnapshot(options?: TimeoutOptions &
782  /// { mode?: 'ai' | 'default', depth?: number }): Promise<string>`.
783  #[qjs(rename = "ariaSnapshot")]
784  pub async fn aria_snapshot<'js>(
785    &self,
786    ctx: rquickjs::Ctx<'js>,
787    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
788  ) -> rquickjs::Result<String> {
789    let core_opts = match options.0 {
790      Some(v) if !v.is_undefined() && !v.is_null() => {
791        #[derive(serde::Deserialize, Default)]
792        #[serde(rename_all = "camelCase", default)]
793        struct JsAria {
794          mode: Option<String>,
795          depth: Option<i32>,
796          timeout: Option<u64>,
797        }
798        let p: JsAria = crate::bindings::convert::serde_from_js(&ctx, v)?;
799        ferridriver::options::AriaSnapshotOptions {
800          mode: Some(ferridriver::options::AriaSnapshotMode::from_opt_str(p.mode.as_deref())),
801          depth: p.depth,
802          timeout: p.timeout,
803        }
804      },
805      _ => ferridriver::options::AriaSnapshotOptions::default(),
806    };
807    self.inner.aria_snapshot(core_opts).await.into_js()
808  }
809
810  #[qjs(rename = "getAttribute")]
811  pub async fn get_attribute(&self, name: String) -> rquickjs::Result<Option<String>> {
812    self.inner.get_attribute(&name).await.into_js()
813  }
814
815  #[qjs(rename = "isVisible")]
816  pub async fn is_visible(&self) -> rquickjs::Result<bool> {
817    self.inner.is_visible().await.into_js()
818  }
819
820  #[qjs(rename = "isHidden")]
821  pub async fn is_hidden(&self) -> rquickjs::Result<bool> {
822    self.inner.is_hidden().await.into_js()
823  }
824
825  #[qjs(rename = "isEnabled")]
826  pub async fn is_enabled(&self) -> rquickjs::Result<bool> {
827    self.inner.is_enabled().await.into_js()
828  }
829
830  #[qjs(rename = "isDisabled")]
831  pub async fn is_disabled(&self) -> rquickjs::Result<bool> {
832    self.inner.is_disabled().await.into_js()
833  }
834
835  #[qjs(rename = "isChecked")]
836  pub async fn is_checked(&self) -> rquickjs::Result<bool> {
837    self.inner.is_checked().await.into_js()
838  }
839
840  #[qjs(rename = "isEditable")]
841  pub async fn is_editable(&self) -> rquickjs::Result<bool> {
842    self.inner.is_editable().await.into_js()
843  }
844
845  #[qjs(rename = "isAttached")]
846  pub async fn is_attached(&self) -> rquickjs::Result<bool> {
847    self.inner.is_attached().await.into_js()
848  }
849
850  // ── Drag ──────────────────────────────────────────────────────────────────
851
852  /// Drag this element to `target`. Mirrors Playwright's
853  /// `locator.dragTo(target, options?)` per
854  /// `/tmp/playwright/packages/playwright-core/types/types.d.ts:13293`.
855  ///
856  /// Accepts `{ force?, noWaitAfter?, sourcePosition?, targetPosition?,
857  /// steps?, timeout?, trial? }`. `strict` is omitted here (present on
858  /// Playwright's `page.dragAndDrop` options but not `locator.dragTo`,
859  /// because the locator already carries its own strict flag).
860  #[qjs(rename = "dragTo")]
861  pub async fn drag_to<'js>(
862    &self,
863    ctx: rquickjs::Ctx<'js>,
864    target: rquickjs::Class<'js, LocatorJs>,
865    options: Opt<rquickjs::Value<'js>>,
866  ) -> rquickjs::Result<()> {
867    let target_inner = target.borrow().inner.clone();
868    let opts = crate::bindings::page::parse_drag_options(&ctx, options)?;
869    self.inner.drag_to(&target_inner, opts).await.into_js()
870  }
871
872  // ── All variants ──────────────────────────────────────────────────────────
873
874  #[qjs(rename = "allTextContents")]
875  pub async fn all_text_contents(&self) -> rquickjs::Result<Vec<String>> {
876    self.inner.all_text_contents().await.into_js()
877  }
878
879  #[qjs(rename = "allInnerTexts")]
880  pub async fn all_inner_texts(&self) -> rquickjs::Result<Vec<String>> {
881    self.inner.all_inner_texts().await.into_js()
882  }
883
884  // ── Evaluation ────────────────────────────────────────────────────────────
885
886  /// Playwright: `locator.evaluate(pageFunction, arg?, options?): Promise<R>`.
887  #[qjs(rename = "evaluate")]
888  pub async fn evaluate<'js>(
889    &self,
890    ctx: rquickjs::Ctx<'js>,
891    page_function: rquickjs::Value<'js>,
892    arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
893  ) -> rquickjs::Result<rquickjs::Value<'js>> {
894    let (source, is_fn) = crate::bindings::convert::extract_page_function(&ctx, page_function)?;
895    let serialized = crate::bindings::convert::quickjs_arg_to_serialized(&ctx, arg.0)?;
896    let result = self.inner.evaluate(&source, serialized, is_fn, None).await.into_js()?;
897    crate::bindings::convert::serialized_value_to_quickjs(&ctx, &result)
898  }
899
900  /// Playwright: `locator.evaluateHandle(pageFunction, arg?, options?): Promise<JSHandle>`.
901  #[qjs(rename = "evaluateHandle")]
902  pub async fn evaluate_handle<'js>(
903    &self,
904    ctx: rquickjs::Ctx<'js>,
905    page_function: rquickjs::Value<'js>,
906    arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
907  ) -> rquickjs::Result<crate::bindings::js_handle::JSHandleJs> {
908    let (source, is_fn) = crate::bindings::convert::extract_page_function(&ctx, page_function)?;
909    let serialized = crate::bindings::convert::quickjs_arg_to_serialized(&ctx, arg.0)?;
910    let handle = self
911      .inner
912      .evaluate_handle(&source, serialized, is_fn, None)
913      .await
914      .into_js()?;
915    Ok(crate::bindings::js_handle::JSHandleJs::new(handle))
916  }
917
918  /// Playwright: `locator.evaluateAll(pageFunction, arg?): Promise<R>`.
919  #[qjs(rename = "evaluateAll")]
920  pub async fn evaluate_all<'js>(
921    &self,
922    ctx: rquickjs::Ctx<'js>,
923    page_function: rquickjs::Value<'js>,
924    arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
925  ) -> rquickjs::Result<rquickjs::Value<'js>> {
926    let (source, is_fn) = crate::bindings::convert::extract_page_function(&ctx, page_function)?;
927    let serialized = crate::bindings::convert::quickjs_arg_to_serialized(&ctx, arg.0)?;
928    let result = self.inner.evaluate_all(&source, serialized, is_fn).await.into_js()?;
929    crate::bindings::convert::serialized_value_to_quickjs(&ctx, &result)
930  }
931}