Skip to main content

ferridriver_script/bindings/
mod.rs

1//! QuickJS bindings for ferridriver core types.
2//!
3//! Wrappers live here (one module per core type) and use `rquickjs`'s own
4//! `#[class]` / `#[methods]` proc macros to generate the JS FFI. Each wrapper
5//! is a thin delegation to the core type.
6//!
7//! # Drift-detection
8//!
9//! Because wrappers are hand-authored, new core methods are invisible until
10//! a wrapper is added. The `audit` tests (see `tests/audit.rs`) enumerate
11//! every `pub` method on the wrapped core types at build time and assert
12//! each has a corresponding wrapper (or is explicitly marked `#[skip]`).
13//!
14//! # Error mapping
15//!
16//! `ferridriver::FerriError` is converted to `rquickjs::Error` at every
17//! binding boundary via [`convert::to_rq_error`]. The resulting JS exception
18//! carries the error message and, where applicable, a `name` matching
19//! Playwright's convention (`TimeoutError`, `TargetClosedError`).
20
21pub mod abort;
22pub mod artifacts;
23pub mod bdd;
24pub mod blob;
25pub mod browser;
26pub mod browser_type;
27pub mod console_message;
28pub mod context;
29pub mod convert;
30pub mod dialog;
31pub mod disposable;
32pub mod download;
33pub mod element_handle;
34pub mod expect;
35pub mod fetch;
36pub mod file_chooser;
37pub mod form_data;
38pub mod frame;
39pub mod frame_locator;
40pub mod http_client;
41pub mod js_handle;
42pub mod keyboard;
43pub mod locator;
44pub mod mouse;
45pub mod network;
46pub mod page;
47pub mod plugins;
48pub mod process;
49pub mod streams;
50pub mod video;
51pub mod web_error;
52pub mod webapi;
53
54pub use artifacts::ArtifactsJs;
55pub use bdd::{
56  CollectedAllow, CollectedRegistry, CollectedTool, HookArg, JsArg, ScenarioWorld, ScriptAttachment, StepOutcome,
57  collect_registry, drain_attachments, install_bdd, invoke_hook, invoke_step, reset_world, set_scenario_world,
58  tools_len, tools_snapshot,
59};
60pub use browser::BrowserJs;
61pub use browser_type::{BrowserTypeJs, install_browser_type};
62pub use console_message::ConsoleMessageJs;
63pub use context::BrowserContextJs;
64pub use dialog::DialogJs;
65pub use disposable::DisposableJs;
66pub use download::DownloadJs;
67pub use element_handle::ElementHandleJs;
68pub use file_chooser::FileChooserJs;
69pub use frame::FrameJs;
70pub use frame_locator::FrameLocatorJs;
71pub use http_client::{HttpClientJs, HttpResponseJs};
72pub use js_handle::JSHandleJs;
73pub use keyboard::KeyboardJs;
74pub use locator::LocatorJs;
75pub use mouse::MouseJs;
76pub use network::{RequestJs, ResponseJs, RouteJs, WebSocketJs};
77pub use page::PageJs;
78pub use plugins::{PluginBinding, PluginCommandsJs, install_plugins};
79pub use video::VideoJs;
80pub use web_error::WebErrorJs;
81
82use rquickjs::{AsyncContext, Ctx, class::Class};
83use std::sync::Arc;
84
85/// Register every class prototype scripts can encounter so rquickjs knows how
86/// to build instances when a method returns one (e.g. `HttpResponse` from
87/// `request.get()` or `Locator` from `page.locator()`).
88///
89/// Prototype registration is idempotent and session-stable: callers
90/// invoke this ONCE at `Session::create`, not per `execute`. The
91/// per-call `install_*` helpers below only build the live instance.
92pub fn define_classes<'js>(ctx: &Ctx<'js>) -> rquickjs::Result<()> {
93  let g = ctx.globals();
94  Class::<PageJs>::define(&g)?;
95  Class::<FrameJs>::define(&g)?;
96  Class::<LocatorJs>::define(&g)?;
97  Class::<BrowserContextJs>::define(&g)?;
98  Class::<BrowserJs>::define(&g)?;
99  Class::<HttpClientJs>::define(&g)?;
100  Class::<HttpResponseJs>::define(&g)?;
101  Class::<KeyboardJs>::define(&g)?;
102  Class::<MouseJs>::define(&g)?;
103  Class::<ArtifactsJs>::define(&g)?;
104  Class::<JSHandleJs>::define(&g)?;
105  Class::<ElementHandleJs>::define(&g)?;
106  // Playwright page-network `Request`/`Response` are NOT globalised
107  // (Playwright itself never puts them on globalThis — they are only
108  // ever return values; `Class::instance` registers their prototype
109  // lazily, so `page.on('response', r => r.status())` still works). The
110  // bare `Request`/`Response` globals belong to the WHATWG fetch
111  // classes below.
112  Class::<RouteJs>::define(&g)?;
113  Class::<WebSocketJs>::define(&g)?;
114  Class::<DialogJs>::define(&g)?;
115  Class::<FileChooserJs>::define(&g)?;
116  Class::<DownloadJs>::define(&g)?;
117  Class::<DisposableJs>::define(&g)?;
118  Class::<ConsoleMessageJs>::define(&g)?;
119  Class::<WebErrorJs>::define(&g)?;
120  Class::<VideoJs>::define(&g)?;
121  Class::<BrowserTypeJs>::define(&g)?;
122  Class::<FrameLocatorJs>::define(&g)?;
123  Class::<crate::bindings::page::TouchscreenJs>::define(&g)?;
124  Class::<crate::bindings::fetch::HeadersJs>::define(&g)?;
125  Class::<crate::bindings::fetch::FetchResponseJs>::define(&g)?;
126  Class::<crate::bindings::fetch::FetchRequestJs>::define(&g)?;
127  Class::<crate::bindings::abort::AbortControllerJs<'js>>::define(&g)?;
128  Class::<crate::bindings::abort::AbortSignalJs<'js>>::define(&g)?;
129  Class::<crate::bindings::streams::ReadableStreamJs>::define(&g)?;
130  Class::<crate::bindings::streams::ReadableStreamDefaultReaderJs>::define(&g)?;
131  Class::<crate::bindings::streams::ReadableStreamDefaultControllerJs>::define(&g)?;
132  Class::<crate::bindings::blob::BlobJs>::define(&g)?;
133  Class::<crate::bindings::form_data::FormDataJs>::define(&g)?;
134  Ok(())
135}
136
137/// Install the `page` global when a page is available on the run context.
138///
139/// `async_ctx` is the `AsyncContext` driving the script — `PageJs`
140/// captures a clone so `page.route(matcher, fn)` can dispatch the JS
141/// callback back into the same context from a backend route handler
142/// (which runs on a separate tokio task, outside the script's
143/// `async_with` block).
144///
145/// Scripts that do not need browser interaction can run with
146/// `RunContext.page = None` and simply have no `page` binding.
147pub fn install_page(ctx: &Ctx<'_>, page: Arc<ferridriver::Page>, async_ctx: AsyncContext) -> rquickjs::Result<()> {
148  install_page_on(ctx, &ctx.globals(), page, async_ctx)
149}
150
151/// Install the `page` binding onto an arbitrary target object.
152///
153/// This is the single implementation; scripting passes `ctx.globals()`
154/// (so bare `page.goto(...)` keeps working) and the BDD layer passes a
155/// per-scenario World object (so cucumber `this.page` resolves to that
156/// scenario's fixtures). One binding, two install targets — no
157/// duplicate `PageJs` wiring.
158pub fn install_page_on<'js>(
159  ctx: &Ctx<'js>,
160  target: &rquickjs::Object<'js>,
161  page: Arc<ferridriver::Page>,
162  async_ctx: AsyncContext,
163) -> rquickjs::Result<()> {
164  let js_page = Class::instance(ctx.clone(), PageJs::new_with_async_ctx(page, async_ctx))?;
165  target.set("page", js_page)?;
166  // Native page-callbacks registry (context userdata): route handlers,
167  // exposeFunction callbacks, screencast — all cross-task dispatched.
168  // Idempotent; independent of the binding target.
169  page::ensure_page_callbacks(ctx);
170  Ok(())
171}
172
173/// Install the `context` global (cookies, storage, permissions, route, etc.).
174pub fn install_browser_context(ctx: &Ctx<'_>, bcx: Arc<ferridriver::context::ContextRef>) -> rquickjs::Result<()> {
175  install_browser_context_on(ctx, &ctx.globals(), bcx)
176}
177
178/// `context` binding onto an arbitrary target (see [`install_page_on`]).
179pub fn install_browser_context_on<'js>(
180  ctx: &Ctx<'js>,
181  target: &rquickjs::Object<'js>,
182  bcx: Arc<ferridriver::context::ContextRef>,
183) -> rquickjs::Result<()> {
184  let js_bcx = Class::instance(ctx.clone(), BrowserContextJs::new(bcx))?;
185  target.set("context", js_bcx)?;
186  Ok(())
187}
188
189/// Install the `browser` global — exposes `browser.newContext(options?)`
190/// so scripts can construct fresh contexts with the full Playwright
191/// [`ferridriver::options::BrowserContextOptions`] bag. Rule-9 tests
192/// for §4.1 consume this entry point.
193pub fn install_browser(ctx: &Ctx<'_>, browser: Arc<ferridriver::Browser>) -> rquickjs::Result<()> {
194  install_browser_on(ctx, &ctx.globals(), browser)
195}
196
197/// `browser` binding onto an arbitrary target (see [`install_page_on`]).
198pub fn install_browser_on<'js>(
199  ctx: &Ctx<'js>,
200  target: &rquickjs::Object<'js>,
201  browser: Arc<ferridriver::Browser>,
202) -> rquickjs::Result<()> {
203  let js_browser = Class::instance(ctx.clone(), BrowserJs::new(browser))?;
204  target.set("browser", js_browser)?;
205  Ok(())
206}
207
208/// Install the `request` global (runner-side HTTP via HttpClient).
209pub fn install_request(ctx: &Ctx<'_>, req: Arc<ferridriver::http_client::HttpClient>) -> rquickjs::Result<()> {
210  install_request_on(ctx, &ctx.globals(), req)
211}
212
213/// `request` binding onto an arbitrary target (see [`install_page_on`]).
214pub fn install_request_on<'js>(
215  ctx: &Ctx<'js>,
216  target: &rquickjs::Object<'js>,
217  req: Arc<ferridriver::http_client::HttpClient>,
218) -> rquickjs::Result<()> {
219  let js_req = Class::instance(ctx.clone(), HttpClientJs::new(req))?;
220  target.set("request", js_req)?;
221  Ok(())
222}
223
224/// Install the `artifacts` global — a dedicated sandboxed directory for
225/// script outputs (screenshots, PDFs, traces, downloaded bodies).
226pub fn install_artifacts(ctx: &Ctx<'_>, sandbox: Arc<crate::fs::PathSandbox>) -> rquickjs::Result<()> {
227  let js_art = Class::instance(ctx.clone(), ArtifactsJs::new(sandbox))?;
228  ctx.globals().set("artifacts", js_art)?;
229  Ok(())
230}