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  // ── Locator (frame-scoped) ─────────────────────────────────────────
133
134  /// Create a locator scoped to this frame.
135  #[qjs(rename = "locator")]
136  pub fn locator(&self, selector: String) -> LocatorJs {
137    LocatorJs::new(self.inner.locator(&selector, None))
138  }
139
140  #[qjs(rename = "getByRole")]
141  pub fn get_by_role(
142    &self,
143    role: String,
144    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
145  ) -> rquickjs::Result<LocatorJs> {
146    let opts = crate::bindings::page::parse_role_options(options)?;
147    Ok(LocatorJs::new(self.inner.get_by_role(&role, &opts)))
148  }
149
150  #[qjs(rename = "getByText")]
151  pub fn get_by_text(
152    &self,
153    text: rquickjs::Value<'_>,
154    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
155  ) -> rquickjs::Result<LocatorJs> {
156    let t = crate::bindings::page::string_or_regex_from_js(text)?;
157    let opts = crate::bindings::page::parse_text_options(options);
158    Ok(LocatorJs::new(self.inner.get_by_text(&t, &opts)))
159  }
160
161  #[qjs(rename = "getByLabel")]
162  pub fn get_by_label(
163    &self,
164    text: rquickjs::Value<'_>,
165    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
166  ) -> rquickjs::Result<LocatorJs> {
167    let t = crate::bindings::page::string_or_regex_from_js(text)?;
168    let opts = crate::bindings::page::parse_text_options(options);
169    Ok(LocatorJs::new(self.inner.get_by_label(&t, &opts)))
170  }
171
172  #[qjs(rename = "getByPlaceholder")]
173  pub fn get_by_placeholder(
174    &self,
175    text: rquickjs::Value<'_>,
176    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
177  ) -> rquickjs::Result<LocatorJs> {
178    let t = crate::bindings::page::string_or_regex_from_js(text)?;
179    let opts = crate::bindings::page::parse_text_options(options);
180    Ok(LocatorJs::new(self.inner.get_by_placeholder(&t, &opts)))
181  }
182
183  #[qjs(rename = "getByAltText")]
184  pub fn get_by_alt_text(
185    &self,
186    text: rquickjs::Value<'_>,
187    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
188  ) -> rquickjs::Result<LocatorJs> {
189    let t = crate::bindings::page::string_or_regex_from_js(text)?;
190    let opts = crate::bindings::page::parse_text_options(options);
191    Ok(LocatorJs::new(self.inner.get_by_alt_text(&t, &opts)))
192  }
193
194  #[qjs(rename = "getByTitle")]
195  pub fn get_by_title(
196    &self,
197    text: rquickjs::Value<'_>,
198    options: rquickjs::function::Opt<rquickjs::Value<'_>>,
199  ) -> rquickjs::Result<LocatorJs> {
200    let t = crate::bindings::page::string_or_regex_from_js(text)?;
201    let opts = crate::bindings::page::parse_text_options(options);
202    Ok(LocatorJs::new(self.inner.get_by_title(&t, &opts)))
203  }
204
205  #[qjs(rename = "getByTestId")]
206  pub fn get_by_test_id(&self, test_id: rquickjs::Value<'_>) -> rquickjs::Result<LocatorJs> {
207    let t = crate::bindings::page::string_or_regex_from_js(test_id)?;
208    Ok(LocatorJs::new(self.inner.get_by_test_id(&t)))
209  }
210
211  /// Playwright: `frame.frameLocator(selector): FrameLocator`.
212  #[qjs(rename = "frameLocator")]
213  pub fn frame_locator(&self, selector: String) -> crate::bindings::frame_locator::FrameLocatorJs {
214    crate::bindings::frame_locator::FrameLocatorJs::new(self.inner.frame_locator(&selector))
215  }
216
217  /// Playwright: `frame.page(): Page`. Returns the owning page; it
218  /// carries the session's `AsyncContext` (via userdata) so
219  /// `page.route` / `page.exposeFunction` work on the returned handle.
220  #[qjs(rename = "page")]
221  pub fn page(&self, ctx: rquickjs::Ctx<'_>) -> crate::bindings::page::PageJs {
222    crate::bindings::page::pagejs_for_ctx(&ctx, self.inner.page_arc().clone())
223  }
224
225  // ── Action methods (Playwright parity — task 3.9) ──────────────────
226  //
227  // Mirror the surface from
228  // `/tmp/playwright/packages/playwright-core/src/client/frame.ts:296-447`.
229  // Each delegates to the Rust core's `Frame::<action>`, which scopes
230  // to this frame's execution context via `Frame::locator`.
231
232  #[qjs(rename = "click")]
233  pub async fn click<'js>(
234    &self,
235    ctx: rquickjs::Ctx<'js>,
236    selector: String,
237    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
238  ) -> rquickjs::Result<()> {
239    let opts = crate::bindings::convert::parse_click_options(&ctx, options)?;
240    self.inner.click(&selector, opts).await.into_js()
241  }
242
243  #[qjs(rename = "dblclick")]
244  pub async fn dblclick<'js>(
245    &self,
246    ctx: rquickjs::Ctx<'js>,
247    selector: String,
248    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
249  ) -> rquickjs::Result<()> {
250    let opts = crate::bindings::convert::parse_dblclick_options(&ctx, options)?;
251    self.inner.dblclick(&selector, opts).await.into_js()
252  }
253
254  #[qjs(rename = "hover")]
255  pub async fn hover<'js>(
256    &self,
257    ctx: rquickjs::Ctx<'js>,
258    selector: String,
259    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
260  ) -> rquickjs::Result<()> {
261    let opts = crate::bindings::convert::parse_hover_options(&ctx, options)?;
262    self.inner.hover(&selector, opts).await.into_js()
263  }
264
265  #[qjs(rename = "tap")]
266  pub async fn tap<'js>(
267    &self,
268    ctx: rquickjs::Ctx<'js>,
269    selector: String,
270    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
271  ) -> rquickjs::Result<()> {
272    let opts = crate::bindings::convert::parse_tap_options(&ctx, options)?;
273    self.inner.tap(&selector, opts).await.into_js()
274  }
275
276  #[qjs(rename = "focus")]
277  pub async fn focus(&self, selector: String) -> rquickjs::Result<()> {
278    self.inner.focus(&selector).await.into_js()
279  }
280
281  #[qjs(rename = "fill")]
282  pub async fn fill<'js>(
283    &self,
284    ctx: rquickjs::Ctx<'js>,
285    selector: String,
286    value: String,
287    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
288  ) -> rquickjs::Result<()> {
289    let opts = crate::bindings::convert::parse_fill_options(&ctx, options)?;
290    self.inner.fill(&selector, &value, opts).await.into_js()
291  }
292
293  #[qjs(rename = "type")]
294  pub async fn type_text<'js>(
295    &self,
296    ctx: rquickjs::Ctx<'js>,
297    selector: String,
298    text: String,
299    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
300  ) -> rquickjs::Result<()> {
301    let opts = crate::bindings::convert::parse_type_options(&ctx, options)?;
302    self.inner.r#type(&selector, &text, opts).await.into_js()
303  }
304
305  #[qjs(rename = "press")]
306  pub async fn press<'js>(
307    &self,
308    ctx: rquickjs::Ctx<'js>,
309    selector: String,
310    key: String,
311    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
312  ) -> rquickjs::Result<()> {
313    let opts = crate::bindings::convert::parse_press_options(&ctx, options)?;
314    self.inner.press(&selector, &key, opts).await.into_js()
315  }
316
317  #[qjs(rename = "check")]
318  pub async fn check<'js>(
319    &self,
320    ctx: rquickjs::Ctx<'js>,
321    selector: String,
322    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
323  ) -> rquickjs::Result<()> {
324    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
325    self.inner.check(&selector, opts).await.into_js()
326  }
327
328  #[qjs(rename = "uncheck")]
329  pub async fn uncheck<'js>(
330    &self,
331    ctx: rquickjs::Ctx<'js>,
332    selector: String,
333    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
334  ) -> rquickjs::Result<()> {
335    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
336    self.inner.uncheck(&selector, opts).await.into_js()
337  }
338
339  #[qjs(rename = "setChecked")]
340  pub async fn set_checked<'js>(
341    &self,
342    ctx: rquickjs::Ctx<'js>,
343    selector: String,
344    checked: bool,
345    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
346  ) -> rquickjs::Result<()> {
347    let opts = crate::bindings::convert::parse_check_options(&ctx, options)?;
348    self.inner.set_checked(&selector, checked, opts).await.into_js()
349  }
350
351  #[qjs(rename = "selectOption")]
352  pub async fn select_option<'js>(
353    &self,
354    ctx: rquickjs::Ctx<'js>,
355    selector: String,
356    values: rquickjs::Value<'js>,
357    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
358  ) -> rquickjs::Result<Vec<String>> {
359    let values = crate::bindings::convert::parse_select_option_values(&ctx, values)?;
360    let opts = crate::bindings::convert::parse_select_option_options(&ctx, options)?;
361    self.inner.select_option(&selector, values, opts).await.into_js()
362  }
363
364  #[qjs(rename = "setInputFiles")]
365  pub async fn set_input_files<'js>(
366    &self,
367    ctx: rquickjs::Ctx<'js>,
368    selector: String,
369    files: rquickjs::Value<'js>,
370    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
371  ) -> rquickjs::Result<()> {
372    let files = crate::bindings::convert::parse_input_files(&ctx, files)?;
373    let opts = crate::bindings::convert::parse_set_input_files_options(&ctx, options)?;
374    self.inner.set_input_files(&selector, files, opts).await.into_js()
375  }
376
377  /// Drag from `source` to `target` selectors within this frame.
378  /// Options ride on Locator's drag option bag.
379  #[qjs(rename = "dragAndDrop")]
380  pub async fn drag_and_drop(&self, source: String, target: String) -> rquickjs::Result<()> {
381    self.inner.drag_and_drop(&source, &target, None).await.into_js()
382  }
383
384  #[qjs(rename = "dispatchEvent")]
385  pub async fn dispatch_event<'js>(
386    &self,
387    ctx: rquickjs::Ctx<'js>,
388    selector: String,
389    event_type: String,
390    event_init: rquickjs::function::Opt<rquickjs::Value<'js>>,
391    options: rquickjs::function::Opt<rquickjs::Value<'js>>,
392  ) -> rquickjs::Result<()> {
393    let init_json = match event_init.0 {
394      Some(v) if !v.is_undefined() && !v.is_null() => {
395        Some(crate::bindings::convert::serde_from_js::<serde_json::Value>(&ctx, v)?)
396      },
397      _ => None,
398    };
399    let opts = crate::bindings::convert::parse_dispatch_event_options(&ctx, options)?;
400    self
401      .inner
402      .dispatch_event(&selector, &event_type, init_json, opts)
403      .await
404      .into_js()
405  }
406
407  #[qjs(rename = "textContent")]
408  pub async fn text_content(&self, selector: String) -> rquickjs::Result<Option<String>> {
409    self.inner.text_content(&selector).await.into_js()
410  }
411
412  #[qjs(rename = "innerText")]
413  pub async fn inner_text(&self, selector: String) -> rquickjs::Result<String> {
414    self.inner.inner_text(&selector).await.into_js()
415  }
416
417  #[qjs(rename = "innerHTML")]
418  pub async fn inner_html(&self, selector: String) -> rquickjs::Result<String> {
419    self.inner.inner_html(&selector).await.into_js()
420  }
421
422  #[qjs(rename = "getAttribute")]
423  pub async fn get_attribute(&self, selector: String, name: String) -> rquickjs::Result<Option<String>> {
424    self.inner.get_attribute(&selector, &name).await.into_js()
425  }
426
427  #[qjs(rename = "inputValue")]
428  pub async fn input_value(&self, selector: String) -> rquickjs::Result<String> {
429    self.inner.input_value(&selector).await.into_js()
430  }
431
432  #[qjs(rename = "isVisible")]
433  pub async fn is_visible(&self, selector: String) -> rquickjs::Result<bool> {
434    self.inner.is_visible(&selector).await.into_js()
435  }
436
437  #[qjs(rename = "isHidden")]
438  pub async fn is_hidden(&self, selector: String) -> rquickjs::Result<bool> {
439    self.inner.is_hidden(&selector).await.into_js()
440  }
441
442  #[qjs(rename = "isEnabled")]
443  pub async fn is_enabled(&self, selector: String) -> rquickjs::Result<bool> {
444    self.inner.is_enabled(&selector).await.into_js()
445  }
446
447  #[qjs(rename = "isDisabled")]
448  pub async fn is_disabled(&self, selector: String) -> rquickjs::Result<bool> {
449    self.inner.is_disabled(&selector).await.into_js()
450  }
451
452  #[qjs(rename = "isEditable")]
453  pub async fn is_editable(&self, selector: String) -> rquickjs::Result<bool> {
454    self.inner.is_editable(&selector).await.into_js()
455  }
456
457  #[qjs(rename = "isChecked")]
458  pub async fn is_checked(&self, selector: String) -> rquickjs::Result<bool> {
459    self.inner.is_checked(&selector).await.into_js()
460  }
461}