Skip to main content

ferridriver_script/bindings/
browser_type.rs

1//! `BrowserTypeJs`: JS wrapper around [`ferridriver::BrowserType`].
2//!
3//! Exposes `chromium()` / `firefox()` / `webkit()` as global factories
4//! mirroring Playwright's
5//! `import { chromium, firefox, webkit } from 'playwright'`. Each
6//! returns a `BrowserType` carrying `name()`, `executablePath()`,
7//! `launch()`, `connect()`, `connectOverCDP()`, and
8//! `launchPersistentContext()`.
9//!
10//! Playwright reference:
11//! `/tmp/playwright/packages/playwright-core/src/client/browserType.ts`.
12
13use std::sync::Arc;
14
15use ferridriver::options::{
16  self as core_opts, BrowserTypeOptions, ChromiumTransport, ConnectOptions, ConnectOverCdpOptions, LaunchOptions,
17  LaunchPersistentContextOptions,
18};
19use ferridriver::{Browser, BrowserType};
20use rquickjs::function::Opt;
21use rquickjs::{Ctx, JsLifetime, Value, class::Class, class::Trace};
22
23use super::browser::BrowserJs;
24use super::context::BrowserContextJs;
25use crate::bindings::convert::{serde_from_js, to_rq_error};
26
27#[derive(JsLifetime, Trace)]
28#[rquickjs::class(rename = "BrowserType")]
29pub struct BrowserTypeJs {
30  #[qjs(skip_trace)]
31  inner: BrowserType,
32}
33
34impl BrowserTypeJs {
35  #[must_use]
36  pub fn new(inner: BrowserType) -> Self {
37    Self { inner }
38  }
39}
40
41#[rquickjs::methods]
42impl BrowserTypeJs {
43  /// Playwright `BrowserType.name()`.
44  #[qjs(rename = "name")]
45  pub fn name(&self) -> String {
46    self.inner.name().to_string()
47  }
48
49  /// Playwright `BrowserType.executablePath()`.
50  #[qjs(rename = "executablePath")]
51  pub fn executable_path(&self) -> Option<String> {
52    self.inner.executable_path().map(|p| p.to_string_lossy().into_owned())
53  }
54
55  /// Playwright `browserType.launch(options?)`.
56  #[qjs(rename = "launch")]
57  pub async fn launch<'js>(&self, ctx: Ctx<'js>, options: Opt<Value<'js>>) -> rquickjs::Result<Value<'js>> {
58    let core = match options.0 {
59      None => LaunchOptions::default(),
60      Some(v) if v.is_undefined() || v.is_null() => LaunchOptions::default(),
61      Some(v) => parse_launch_options(&ctx, v)?,
62    };
63    let inner = self.inner.launch(core).await.map_err(|e| to_rq_error(&e))?;
64    let wrapper = BrowserJs::new(Arc::new(inner));
65    let instance = Class::instance(ctx.clone(), wrapper)?;
66    rquickjs::IntoJs::into_js(instance, &ctx)
67  }
68
69  /// Playwright `browserType.connect(wsEndpoint, options?)`.
70  #[qjs(rename = "connect")]
71  pub async fn connect<'js>(
72    &self,
73    ctx: Ctx<'js>,
74    ws_endpoint: String,
75    options: Opt<Value<'js>>,
76  ) -> rquickjs::Result<Value<'js>> {
77    let core = match options.0 {
78      None => ConnectOptions::default(),
79      Some(v) if v.is_undefined() || v.is_null() => ConnectOptions::default(),
80      Some(v) => parse_connect_options(&ctx, v)?,
81    };
82    let inner = self
83      .inner
84      .connect(&ws_endpoint, core)
85      .await
86      .map_err(|e| to_rq_error(&e))?;
87    let wrapper = BrowserJs::new(Arc::new(inner));
88    let instance = Class::instance(ctx.clone(), wrapper)?;
89    rquickjs::IntoJs::into_js(instance, &ctx)
90  }
91
92  /// Playwright `browserType.connectOverCDP(endpointURL, options?)`. Chromium-only.
93  #[qjs(rename = "connectOverCDP")]
94  pub async fn connect_over_cdp<'js>(
95    &self,
96    ctx: Ctx<'js>,
97    endpoint_url: String,
98    options: Opt<Value<'js>>,
99  ) -> rquickjs::Result<Value<'js>> {
100    let core = match options.0 {
101      None => ConnectOverCdpOptions::default(),
102      Some(v) if v.is_undefined() || v.is_null() => ConnectOverCdpOptions::default(),
103      Some(v) => parse_connect_over_cdp_options(&ctx, v)?,
104    };
105    let inner = self
106      .inner
107      .connect_over_cdp(&endpoint_url, core)
108      .await
109      .map_err(|e| to_rq_error(&e))?;
110    let wrapper = BrowserJs::new(Arc::new(inner));
111    let instance = Class::instance(ctx.clone(), wrapper)?;
112    rquickjs::IntoJs::into_js(instance, &ctx)
113  }
114
115  /// Playwright `browserType.launchPersistentContext(userDataDir, options?)`.
116  #[qjs(rename = "launchPersistentContext")]
117  pub async fn launch_persistent_context<'js>(
118    &self,
119    ctx: Ctx<'js>,
120    user_data_dir: String,
121    options: Opt<Value<'js>>,
122  ) -> rquickjs::Result<Value<'js>> {
123    let (launch, context) = match options.0 {
124      None => (LaunchOptions::default(), core_opts::BrowserContextOptions::default()),
125      Some(v) if v.is_undefined() || v.is_null() => {
126        (LaunchOptions::default(), core_opts::BrowserContextOptions::default())
127      },
128      Some(v) => {
129        let launch = parse_launch_options(&ctx, v.clone())?;
130        let context = parse_context_options(&ctx, v)?;
131        (launch, context)
132      },
133    };
134    let core = LaunchPersistentContextOptions { launch, context };
135    let ctx_ref = self
136      .inner
137      .launch_persistent_context(std::path::Path::new(&user_data_dir), core)
138      .await
139      .map_err(|e| to_rq_error(&e))?;
140    let wrapper = BrowserContextJs::new(Arc::new(ctx_ref));
141    let instance = Class::instance(ctx.clone(), wrapper)?;
142    rquickjs::IntoJs::into_js(instance, &ctx)
143  }
144}
145
146#[derive(serde::Deserialize, Default)]
147#[serde(rename_all = "camelCase", default)]
148struct JsLaunchOptions {
149  headless: Option<bool>,
150  executable_path: Option<String>,
151  args: Option<Vec<String>>,
152  channel: Option<String>,
153  slow_mo: Option<u64>,
154  timeout: Option<u64>,
155  downloads_path: Option<String>,
156  traces_dir: Option<String>,
157}
158
159#[derive(serde::Deserialize, Default)]
160#[serde(rename_all = "camelCase", default)]
161struct JsConnectOptions {
162  headers: Option<rustc_hash::FxHashMap<String, String>>,
163  slow_mo: Option<u64>,
164  timeout: Option<u64>,
165  expose_network: Option<String>,
166}
167
168#[derive(serde::Deserialize, Default)]
169#[serde(rename_all = "camelCase", default)]
170struct JsConnectOverCdpOptions {
171  headers: Option<rustc_hash::FxHashMap<String, String>>,
172  slow_mo: Option<u64>,
173  timeout: Option<u64>,
174}
175
176fn parse_launch_options<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<LaunchOptions> {
177  let parsed: JsLaunchOptions = serde_from_js(ctx, value)?;
178  Ok(LaunchOptions {
179    headless: parsed.headless,
180    executable_path: parsed.executable_path,
181    args: parsed.args.unwrap_or_default(),
182    channel: parsed.channel,
183    env: None,
184    slow_mo: parsed.slow_mo,
185    timeout: parsed.timeout,
186    downloads_path: parsed.downloads_path.map(std::path::PathBuf::from),
187    ignore_default_args: None,
188    handle_sighup: None,
189    handle_sigint: None,
190    handle_sigterm: None,
191    chromium_sandbox: None,
192    firefox_user_prefs: None,
193    proxy: None,
194    traces_dir: parsed.traces_dir.map(std::path::PathBuf::from),
195  })
196}
197
198fn parse_connect_options<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<ConnectOptions> {
199  let parsed: JsConnectOptions = serde_from_js(ctx, value)?;
200  Ok(ConnectOptions {
201    headers: parsed.headers,
202    slow_mo: parsed.slow_mo,
203    timeout: parsed.timeout,
204    expose_network: parsed.expose_network,
205  })
206}
207
208fn parse_connect_over_cdp_options<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<ConnectOverCdpOptions> {
209  let parsed: JsConnectOverCdpOptions = serde_from_js(ctx, value)?;
210  Ok(ConnectOverCdpOptions {
211    headers: parsed.headers,
212    slow_mo: parsed.slow_mo,
213    timeout: parsed.timeout,
214  })
215}
216
217fn parse_context_options<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<core_opts::BrowserContextOptions> {
218  // Re-use the same JS-side schema as `browser.newContext(...)`. The
219  // launch-options keys overlap (e.g. `headless`) but serde ignores
220  // unknown fields per the `BrowserJs` parser.
221  let parsed: super::browser::JsBrowserContextOptions = serde_from_js(ctx, value)?;
222  Ok(parsed.into_core())
223}
224
225fn chromium_factory<'js>(ctx: Ctx<'js>, opts: Opt<Value<'js>>) -> rquickjs::Result<Class<'js, BrowserTypeJs>> {
226  let transport = match opts.0 {
227    None => None,
228    Some(v) if v.is_undefined() || v.is_null() => None,
229    Some(v) => {
230      #[derive(serde::Deserialize, Default)]
231      struct ChromiumOpts {
232        transport: Option<String>,
233      }
234      let parsed: ChromiumOpts = serde_from_js(&ctx, v)?;
235      parsed.transport.and_then(|t| match t.as_str() {
236        "ws" => Some(ChromiumTransport::Ws),
237        "pipe" => Some(ChromiumTransport::Pipe),
238        _ => None,
239      })
240    },
241  };
242  let bt = BrowserType::chromium_with(&BrowserTypeOptions { transport });
243  Class::instance(ctx, BrowserTypeJs::new(bt))
244}
245
246fn firefox_factory(ctx: Ctx<'_>) -> rquickjs::Result<Class<'_, BrowserTypeJs>> {
247  Class::instance(ctx, BrowserTypeJs::new(BrowserType::firefox()))
248}
249
250fn webkit_factory(ctx: Ctx<'_>) -> rquickjs::Result<Class<'_, BrowserTypeJs>> {
251  Class::instance(ctx, BrowserTypeJs::new(BrowserType::webkit()))
252}
253
254/// Install the top-level `chromium`, `firefox`, and `webkit` globals.
255/// Mirrors Playwright's `import { chromium, firefox, webkit }` exactly:
256/// `chromium()` is ALWAYS Chromium, `firefox()` ALWAYS Firefox,
257/// `webkit()` ALWAYS WebKit. The wire backend is a per-product detail
258/// (Chromium pipe vs `chromium({transport:'ws'})`; Firefox speaks
259/// BiDi) — never a product swap.
260pub fn install_browser_type(ctx: &Ctx<'_>) -> rquickjs::Result<()> {
261  Class::<BrowserTypeJs>::define(&ctx.globals())?;
262
263  ctx
264    .globals()
265    .set("chromium", rquickjs::Function::new(ctx.clone(), chromium_factory)?)?;
266  ctx
267    .globals()
268    .set("firefox", rquickjs::Function::new(ctx.clone(), firefox_factory)?)?;
269  ctx
270    .globals()
271    .set("webkit", rquickjs::Function::new(ctx.clone(), webkit_factory)?)?;
272
273  // Suppress the unused-import warning for `Browser`, which is only
274  // here to keep doc-link references valid in a future binding.
275  let _ = std::marker::PhantomData::<Browser>;
276
277  Ok(())
278}