Skip to main content

ferridriver_script/bindings/
frame.rs

1//! `FrameJs`: JS wrapper around `ferridriver::Frame`.
2//!
3//! Mirrors Playwright's
4//! [Frame](https://playwright.dev/docs/api/class-frame) sync navigation
5//! tree API — `name()`, `url()`, `isMainFrame()`, `parentFrame()`,
6//! `childFrames()`, `isDetached()` — plus the small set of async
7//! accessors needed for writing scripts that deal with iframes
8//! (evaluate / title / content, locator).
9//!
10//! Action methods (`click`, `fill`, `hover`, etc.) and the full
11//! getBy* option surface ship in **task 3.9** (Frame action methods).
12//!
13//! The underlying `ferridriver::Frame` is a cheap `(Arc<Page>, Arc<str>)`
14//! handle — cloning it is free. All name/url/parent/children/detached
15//! state is read live from the page-owned frame cache (see
16//! `crate::frame_cache::FrameCache`) seeded at `Page::init_frame_cache`.
17
18use ferridriver::Frame;
19use rquickjs::JsLifetime;
20use rquickjs::class::Trace;
21
22use crate::bindings::convert::{
23  FerriResultExt, extract_page_function, quickjs_arg_to_serialized, serialized_value_to_quickjs,
24};
25use crate::bindings::locator::LocatorJs;
26
27/// JS-visible wrapper around [`ferridriver::Frame`]. Constructed only by
28/// `PageJs` / other `FrameJs` instances (`mainFrame`, `frames`, `frame`,
29/// `parentFrame`, `childFrames`).
30#[derive(JsLifetime, Trace)]
31#[rquickjs::class(rename = "Frame")]
32pub struct FrameJs {
33  #[qjs(skip_trace)]
34  inner: Frame,
35}
36
37impl FrameJs {
38  #[must_use]
39  pub(crate) fn new(inner: Frame) -> Self {
40    Self { inner }
41  }
42}
43
44#[rquickjs::methods]
45impl FrameJs {
46  // ── Sync frame-tree accessors (Playwright parity, task 3.8) ────────
47
48  /// Frame name (from the `<iframe name=...>` attribute). Sync.
49  #[qjs(rename = "name")]
50  pub fn name(&self) -> String {
51    self.inner.name()
52  }
53
54  /// Frame URL. Sync.
55  #[qjs(rename = "url")]
56  pub fn url(&self) -> String {
57    self.inner.url()
58  }
59
60  /// True when this is the top-level page frame. Sync.
61  #[qjs(rename = "isMainFrame")]
62  pub fn is_main_frame(&self) -> bool {
63    self.inner.is_main_frame()
64  }
65
66  /// Parent frame (null for the main frame). Sync.
67  #[qjs(rename = "parentFrame")]
68  pub fn parent_frame(&self) -> Option<FrameJs> {
69    self.inner.parent_frame().map(FrameJs::new)
70  }
71
72  /// Child frames of this frame. Sync.
73  #[qjs(rename = "childFrames")]
74  pub fn child_frames(&self) -> Vec<FrameJs> {
75    self.inner.child_frames().into_iter().map(FrameJs::new).collect()
76  }
77
78  /// Whether this frame has been detached from the page. Sync.
79  #[qjs(rename = "isDetached")]
80  pub fn is_detached(&self) -> bool {
81    self.inner.is_detached()
82  }
83
84  // ── Evaluation (frame-scoped) ──────────────────────────────────────
85
86  /// Playwright: `frame.evaluate(pageFunction, arg?): Promise<R>`.
87  #[qjs(rename = "evaluate")]
88  pub async fn evaluate<'js>(
89    &self,
90    ctx: rquickjs::Ctx<'js>,
91    page_function: rquickjs::Value<'js>,
92    arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
93  ) -> rquickjs::Result<rquickjs::Value<'js>> {
94    let (source, is_fn) = extract_page_function(&ctx, page_function)?;
95    let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
96    let result = self.inner.evaluate(&source, serialized, is_fn).await.into_js()?;
97    serialized_value_to_quickjs(&ctx, &result)
98  }
99
100  /// Playwright: `frame.evaluateHandle(pageFunction, arg?): Promise<JSHandle>`.
101  #[qjs(rename = "evaluateHandle")]
102  pub async fn evaluate_handle<'js>(
103    &self,
104    ctx: rquickjs::Ctx<'js>,
105    page_function: rquickjs::Value<'js>,
106    arg: rquickjs::function::Opt<rquickjs::Value<'js>>,
107  ) -> rquickjs::Result<crate::bindings::js_handle::JSHandleJs> {
108    let (source, is_fn) = extract_page_function(&ctx, page_function)?;
109    let serialized = quickjs_arg_to_serialized(&ctx, arg.0)?;
110    let handle = self.inner.evaluate_handle(&source, serialized, is_fn).await.into_js()?;
111    Ok(crate::bindings::js_handle::JSHandleJs::new(handle))
112  }
113
114  #[qjs(rename = "title")]
115  pub async fn title(&self) -> rquickjs::Result<String> {
116    self.inner.title().await.into_js()
117  }
118
119  #[qjs(rename = "content")]
120  pub async fn content(&self) -> rquickjs::Result<String> {
121    self.inner.content().await.into_js()
122  }
123
124  /// Playwright: `frame.waitForLoadState(state?, options?)`. ferridriver
125  /// core's `Frame::wait_for_load_state` takes no args (defaults to
126  /// `load`); thin delegator.
127  #[qjs(rename = "waitForLoadState")]
128  pub async fn wait_for_load_state(&self) -> rquickjs::Result<()> {
129    self.inner.wait_for_load_state().await.into_js()
130  }
131
132  /// Playwright: `frame.waitForSelector(selector, options?)`
133  /// (`/tmp/playwright/packages/playwright-core/src/client/frame.ts:217`).
134  /// Options: `{ state?: 'visible'|'hidden'|'attached'|'detached', timeout?: ms }`.
135  /// Resolves to the matched element handle for `state: 'attached'` /
136  /// `'visible'` (default), or `null` for `state: 'hidden'` / `'detached'`.
137  #[qjs(rename = "waitForSelector")]
138  pub async fn wait_for_selector<'js>(
139    &self,
140    ctx: rquickjs::Ctx<'js>,
141    selector: String,
142    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
143  ) -> rquickjs::Result<Option<crate::bindings::element_handle::ElementHandleJs>> {
144    let opts = crate::bindings::page::parse_wait_options(&ctx, options)?;
145    let handle = self.inner.wait_for_selector(&selector, opts).await.into_js()?;
146    Ok(handle.map(crate::bindings::element_handle::ElementHandleJs::new))
147  }
148
149  // ── Locator (frame-scoped) ─────────────────────────────────────────
150
151  /// Create a locator scoped to this frame.
152  #[qjs(rename = "locator")]
153  pub fn locator(&self, selector: String) -> LocatorJs {
154    LocatorJs::new(self.inner.locator(&selector, None))
155  }
156
157  #[qjs(rename = "getByRole")]
158  pub fn get_by_role(
159    &self,
160    role: String,
161    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
162  ) -> rquickjs::Result<LocatorJs> {
163    let opts = crate::bindings::page::parse_role_options(options)?;
164    Ok(LocatorJs::new(self.inner.get_by_role(&role, &opts)))
165  }
166
167  #[qjs(rename = "getByText")]
168  pub fn get_by_text(
169    &self,
170    text: rquickjs::Value<'_>,
171    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
172  ) -> rquickjs::Result<LocatorJs> {
173    let t = crate::bindings::page::string_or_regex_from_js(text)?;
174    let opts = crate::bindings::page::parse_text_options(options);
175    Ok(LocatorJs::new(self.inner.get_by_text(&t, &opts)))
176  }
177
178  #[qjs(rename = "getByLabel")]
179  pub fn get_by_label(
180    &self,
181    text: rquickjs::Value<'_>,
182    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
183  ) -> rquickjs::Result<LocatorJs> {
184    let t = crate::bindings::page::string_or_regex_from_js(text)?;
185    let opts = crate::bindings::page::parse_text_options(options);
186    Ok(LocatorJs::new(self.inner.get_by_label(&t, &opts)))
187  }
188
189  #[qjs(rename = "getByPlaceholder")]
190  pub fn get_by_placeholder(
191    &self,
192    text: rquickjs::Value<'_>,
193    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
194  ) -> rquickjs::Result<LocatorJs> {
195    let t = crate::bindings::page::string_or_regex_from_js(text)?;
196    let opts = crate::bindings::page::parse_text_options(options);
197    Ok(LocatorJs::new(self.inner.get_by_placeholder(&t, &opts)))
198  }
199
200  #[qjs(rename = "getByAltText")]
201  pub fn get_by_alt_text(
202    &self,
203    text: rquickjs::Value<'_>,
204    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
205  ) -> rquickjs::Result<LocatorJs> {
206    let t = crate::bindings::page::string_or_regex_from_js(text)?;
207    let opts = crate::bindings::page::parse_text_options(options);
208    Ok(LocatorJs::new(self.inner.get_by_alt_text(&t, &opts)))
209  }
210
211  #[qjs(rename = "getByTitle")]
212  pub fn get_by_title(
213    &self,
214    text: rquickjs::Value<'_>,
215    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
216  ) -> rquickjs::Result<LocatorJs> {
217    let t = crate::bindings::page::string_or_regex_from_js(text)?;
218    let opts = crate::bindings::page::parse_text_options(options);
219    Ok(LocatorJs::new(self.inner.get_by_title(&t, &opts)))
220  }
221
222  #[qjs(rename = "getByTestId")]
223  pub fn get_by_test_id(&self, test_id: rquickjs::Value<'_>) -> rquickjs::Result<LocatorJs> {
224    let t = crate::bindings::page::string_or_regex_from_js(test_id)?;
225    Ok(LocatorJs::new(self.inner.get_by_test_id(&t)))
226  }
227
228  /// Playwright: `frame.frameLocator(selector): FrameLocator`.
229  #[qjs(rename = "frameLocator")]
230  pub fn frame_locator(&self, selector: String) -> crate::bindings::frame_locator::FrameLocatorJs {
231    crate::bindings::frame_locator::FrameLocatorJs::new(self.inner.frame_locator(&selector))
232  }
233
234  /// Playwright: `frame.page(): Page`. Returns the owning page; it
235  /// carries the session's `AsyncContext` (via userdata) so
236  /// `page.route` / `page.exposeFunction` work on the returned handle.
237  #[qjs(rename = "page")]
238  pub fn page(&self, ctx: rquickjs::Ctx<'_>) -> crate::bindings::page::PageJs {
239    crate::bindings::page::pagejs_for_ctx(&ctx, self.inner.page_arc().clone())
240  }
241
242  // ── Action methods (Playwright parity — task 3.9) ──────────────────
243  //
244  // Mirror the surface from
245  // `/tmp/playwright/packages/playwright-core/src/client/frame.ts:296-447`.
246  // Each delegates to the Rust core's `Frame::<action>`, which scopes
247  // to this frame's execution context via `Frame::locator`.
248
249  #[qjs(rename = "click")]
250  pub async fn click<'js>(
251    &self,
252    ctx: rquickjs::Ctx<'js>,
253    selector: String,
254    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
255  ) -> rquickjs::Result<()> {
256    let opts = crate::bindings::convert::parse_click_options(&ctx, options)?;
257    self.inner.click(&selector, opts).await.into_js()
258  }
259
260  #[qjs(rename = "dblclick")]
261  pub async fn dblclick<'js>(
262    &self,
263    ctx: rquickjs::Ctx<'js>,
264    selector: String,
265    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
266  ) -> rquickjs::Result<()> {
267    let opts = crate::bindings::convert::parse_dblclick_options(&ctx, options)?;
268    self.inner.dblclick(&selector, opts).await.into_js()
269  }
270
271  #[qjs(rename = "hover")]
272  pub async fn hover<'js>(
273    &self,
274    ctx: rquickjs::Ctx<'js>,
275    selector: String,
276    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
277  ) -> rquickjs::Result<()> {
278    let opts = crate::bindings::convert::parse_hover_options(&ctx, options)?;
279    self.inner.hover(&selector, opts).await.into_js()
280  }
281
282  #[qjs(rename = "tap")]
283  pub async fn tap<'js>(
284    &self,
285    ctx: rquickjs::Ctx<'js>,
286    selector: String,
287    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
288  ) -> rquickjs::Result<()> {
289    let opts = crate::bindings::convert::parse_tap_options(&ctx, options)?;
290    self.inner.tap(&selector, opts).await.into_js()
291  }
292
293  #[qjs(rename = "focus")]
294  pub async fn focus(&self, selector: String) -> rquickjs::Result<()> {
295    self.inner.focus(&selector).await.into_js()
296  }
297
298  #[qjs(rename = "fill")]
299  pub async fn fill<'js>(
300    &self,
301    ctx: rquickjs::Ctx<'js>,
302    selector: String,
303    value: String,
304    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
305  ) -> rquickjs::Result<()> {
306    let opts = crate::bindings::convert::parse_fill_options(&ctx, options)?;
307    self.inner.fill(&selector, &value, opts).await.into_js()
308  }
309
310  #[qjs(rename = "type")]
311  pub async fn type_text<'js>(
312    &self,
313    ctx: rquickjs::Ctx<'js>,
314    selector: String,
315    text: String,
316    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
317  ) -> rquickjs::Result<()> {
318    let opts = crate::bindings::convert::parse_type_options(&ctx, options)?;
319    self.inner.r#type(&selector, &text, opts).await.into_js()
320  }
321
322  #[qjs(rename = "press")]
323  pub async fn press<'js>(
324    &self,
325    ctx: rquickjs::Ctx<'js>,
326    selector: String,
327    key: String,
328    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
329  ) -> rquickjs::Result<()> {
330    let opts = crate::bindings::convert::parse_press_options(&ctx, options)?;
331    self.inner.press(&selector, &key, opts).await.into_js()
332  }
333
334  #[qjs(rename = "check")]
335  pub async fn check<'js>(
336    &self,
337    ctx: rquickjs::Ctx<'js>,
338    selector: String,
339    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
340  ) -> rquickjs::Result<()> {
341    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
342    self.inner.check(&selector, opts).await.into_js()
343  }
344
345  #[qjs(rename = "uncheck")]
346  pub async fn uncheck<'js>(
347    &self,
348    ctx: rquickjs::Ctx<'js>,
349    selector: String,
350    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
351  ) -> rquickjs::Result<()> {
352    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
353    self.inner.uncheck(&selector, opts).await.into_js()
354  }
355
356  #[qjs(rename = "setChecked")]
357  pub async fn set_checked<'js>(
358    &self,
359    ctx: rquickjs::Ctx<'js>,
360    selector: String,
361    checked: bool,
362    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
363  ) -> rquickjs::Result<()> {
364    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
365    self.inner.set_checked(&selector, checked, opts).await.into_js()
366  }
367
368  #[qjs(rename = "selectOption")]
369  pub async fn select_option<'js>(
370    &self,
371    ctx: rquickjs::Ctx<'js>,
372    selector: String,
373    values: rquickjs::Value<'js>,
374    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
375  ) -> rquickjs::Result<Vec<String>> {
376    let values = crate::bindings::convert::parse_select_option_values(&ctx, values)?;
377    let opts = crate::bindings::convert::parse_select_option_options(&ctx, options)?;
378    self.inner.select_option(&selector, values, opts).await.into_js()
379  }
380
381  #[qjs(rename = "setInputFiles")]
382  pub async fn set_input_files<'js>(
383    &self,
384    ctx: rquickjs::Ctx<'js>,
385    selector: String,
386    files: rquickjs::Value<'js>,
387    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
388  ) -> rquickjs::Result<()> {
389    let files = crate::bindings::convert::parse_input_files(&ctx, files)?;
390    let opts = crate::bindings::convert::parse_set_input_files_options(&ctx, options)?;
391    self.inner.set_input_files(&selector, files, opts).await.into_js()
392  }
393
394  /// Drag from `source` to `target` selectors within this frame.
395  /// Options ride on Locator's drag option bag.
396  #[qjs(rename = "dragAndDrop")]
397  pub async fn drag_and_drop(&self, source: String, target: String) -> rquickjs::Result<()> {
398    self.inner.drag_and_drop(&source, &target, None).await.into_js()
399  }
400
401  #[qjs(rename = "dispatchEvent")]
402  pub async fn dispatch_event<'js>(
403    &self,
404    ctx: rquickjs::Ctx<'js>,
405    selector: String,
406    event_type: String,
407    event_init: rquickjs::function::Opt<rquickjs::Value<'js>>,
408    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
409  ) -> rquickjs::Result<()> {
410    let init_json = match event_init.0 {
411      Some(v) if !v.is_undefined() && !v.is_null() => {
412        Some(crate::bindings::convert::serde_from_js::<serde_json::Value>(&ctx, v)?)
413      },
414      _ => None,
415    };
416    let opts = crate::bindings::convert::parse_dispatch_event_options(&ctx, options)?;
417    self
418      .inner
419      .dispatch_event(&selector, &event_type, init_json, opts)
420      .await
421      .into_js()
422  }
423
424  #[qjs(rename = "textContent")]
425  pub async fn text_content(&self, selector: String) -> rquickjs::Result<Option<String>> {
426    self.inner.text_content(&selector).await.into_js()
427  }
428
429  #[qjs(rename = "innerText")]
430  pub async fn inner_text(&self, selector: String) -> rquickjs::Result<String> {
431    self.inner.inner_text(&selector).await.into_js()
432  }
433
434  #[qjs(rename = "innerHTML")]
435  pub async fn inner_html(&self, selector: String) -> rquickjs::Result<String> {
436    self.inner.inner_html(&selector).await.into_js()
437  }
438
439  #[qjs(rename = "getAttribute")]
440  pub async fn get_attribute(&self, selector: String, name: String) -> rquickjs::Result<Option<String>> {
441    self.inner.get_attribute(&selector, &name).await.into_js()
442  }
443
444  #[qjs(rename = "inputValue")]
445  pub async fn input_value(&self, selector: String) -> rquickjs::Result<String> {
446    self.inner.input_value(&selector).await.into_js()
447  }
448
449  #[qjs(rename = "isVisible")]
450  pub async fn is_visible(&self, selector: String) -> rquickjs::Result<bool> {
451    self.inner.is_visible(&selector).await.into_js()
452  }
453
454  #[qjs(rename = "isHidden")]
455  pub async fn is_hidden(&self, selector: String) -> rquickjs::Result<bool> {
456    self.inner.is_hidden(&selector).await.into_js()
457  }
458
459  #[qjs(rename = "isEnabled")]
460  pub async fn is_enabled(&self, selector: String) -> rquickjs::Result<bool> {
461    self.inner.is_enabled(&selector).await.into_js()
462  }
463
464  #[qjs(rename = "isDisabled")]
465  pub async fn is_disabled(&self, selector: String) -> rquickjs::Result<bool> {
466    self.inner.is_disabled(&selector).await.into_js()
467  }
468
469  #[qjs(rename = "isEditable")]
470  pub async fn is_editable(&self, selector: String) -> rquickjs::Result<bool> {
471    self.inner.is_editable(&selector).await.into_js()
472  }
473
474  #[qjs(rename = "isChecked")]
475  pub async fn is_checked(&self, selector: String) -> rquickjs::Result<bool> {
476    self.inner.is_checked(&selector).await.into_js()
477  }
478}