ferridriver_script/bindings/
browser.rs1use std::sync::Arc;
15
16use ferridriver::Browser;
17use rquickjs::function::Opt;
18use rquickjs::{Ctx, JsLifetime, Value, class::Class, class::Trace};
19
20use super::context::BrowserContextJs;
21use crate::bindings::convert::{FerriResultExt, serde_from_js};
22
23#[derive(JsLifetime, Trace)]
24#[rquickjs::class(rename = "Browser")]
25pub struct BrowserJs {
26 #[qjs(skip_trace)]
27 inner: Arc<Browser>,
28}
29
30impl BrowserJs {
31 #[must_use]
32 pub fn new(inner: Arc<Browser>) -> Self {
33 Self { inner }
34 }
35}
36
37#[rquickjs::methods]
38impl BrowserJs {
39 #[qjs(rename = "newContext")]
44 pub fn new_context<'js>(&self, ctx: Ctx<'js>, options: Opt<Value<'js>>) -> rquickjs::Result<Value<'js>> {
45 let core_opts = match options.0 {
46 None => None,
47 Some(v) if v.is_undefined() || v.is_null() => None,
48 Some(v) => {
49 let parsed: JsBrowserContextOptions = serde_from_js(&ctx, v)?;
50 Some(parsed.into_core())
51 },
52 };
53 let ctx_ref = Arc::new(self.inner.new_context(core_opts));
54 let wrapper = BrowserContextJs::new(ctx_ref);
55 let instance = Class::instance(ctx.clone(), wrapper)?;
56 rquickjs::IntoJs::into_js(instance, &ctx)
57 }
58
59 #[qjs(rename = "version")]
63 pub fn version(&self) -> String {
64 self.inner.version().to_string()
65 }
66
67 #[qjs(rename = "isConnected")]
69 pub fn is_connected(&self) -> bool {
70 self.inner.is_connected()
71 }
72
73 #[qjs(rename = "close")]
77 pub async fn close(&self) -> rquickjs::Result<()> {
78 self
79 .inner
80 .close(None)
81 .await
82 .map_err(|e| crate::bindings::convert::to_rq_error(&e))?;
83 Ok(())
84 }
85
86 #[qjs(rename = "contexts")]
89 pub fn contexts<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
90 let contexts = self.inner.contexts();
91 let arr = rquickjs::Array::new(ctx.clone())?;
92 for (i, c) in contexts.into_iter().enumerate() {
93 let wrapper = BrowserContextJs::new(std::sync::Arc::new(c));
94 let instance = Class::instance(ctx.clone(), wrapper)?;
95 arr.set(i, instance)?;
96 }
97 rquickjs::IntoJs::into_js(arr, &ctx)
98 }
99
100 #[qjs(rename = "newPage")]
102 pub async fn new_page<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
103 let page = self.inner.new_page().await.into_js()?;
104 let wrapper = crate::bindings::page::pagejs_for_ctx(&ctx, page);
105 let instance = Class::instance(ctx.clone(), wrapper)?;
106 rquickjs::IntoJs::into_js(instance, &ctx)
107 }
108
109 #[qjs(rename = "page")]
111 pub async fn page<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
112 let page = self.inner.page().await.into_js()?;
113 let wrapper = crate::bindings::page::pagejs_for_ctx(&ctx, page);
114 let instance = Class::instance(ctx.clone(), wrapper)?;
115 rquickjs::IntoJs::into_js(instance, &ctx)
116 }
117}
118
119#[derive(serde::Deserialize, Default)]
127#[serde(rename_all = "camelCase", default)]
128pub(super) struct JsBrowserContextOptions {
129 accept_downloads: Option<bool>,
130 #[serde(rename = "baseURL")]
131 base_url: Option<String>,
132 #[serde(rename = "bypassCSP")]
133 bypass_csp: Option<bool>,
134 color_scheme: Option<serde_json::Value>,
135 contrast: Option<serde_json::Value>,
136 device_scale_factor: Option<f64>,
137 #[serde(rename = "extraHTTPHeaders")]
138 extra_http_headers: Option<rustc_hash::FxHashMap<String, String>>,
139 forced_colors: Option<serde_json::Value>,
140 geolocation: Option<JsGeolocation>,
141 has_touch: Option<bool>,
142 http_credentials: Option<JsHttpCredentials>,
143 #[serde(rename = "ignoreHTTPSErrors")]
144 ignore_https_errors: Option<bool>,
145 is_mobile: Option<bool>,
146 java_script_enabled: Option<bool>,
147 locale: Option<String>,
148 offline: Option<bool>,
149 permissions: Option<Vec<String>>,
150 proxy: Option<JsProxyConfig>,
151 record_video: Option<JsRecordVideoOptions>,
152 reduced_motion: Option<serde_json::Value>,
153 screen: Option<JsScreenSize>,
154 service_workers: Option<String>,
155 storage_state: Option<serde_json::Value>,
158 strict_selectors: Option<bool>,
159 timezone_id: Option<String>,
160 user_agent: Option<String>,
161 viewport: Option<serde_json::Value>,
163}
164
165#[derive(serde::Deserialize)]
166struct JsGeolocation {
167 latitude: f64,
168 longitude: f64,
169 accuracy: Option<f64>,
170}
171
172#[derive(serde::Deserialize)]
173#[serde(rename_all = "camelCase")]
174struct JsHttpCredentials {
175 username: String,
176 password: String,
177 origin: Option<String>,
178 send: Option<String>,
179}
180
181#[derive(serde::Deserialize)]
182struct JsProxyConfig {
183 server: String,
184 bypass: Option<String>,
185 username: Option<String>,
186 password: Option<String>,
187}
188
189#[derive(serde::Deserialize)]
190struct JsScreenSize {
191 width: i64,
192 height: i64,
193}
194
195#[derive(serde::Deserialize)]
196#[serde(rename_all = "camelCase")]
197struct JsRecordVideoOptions {
198 dir: String,
199 size: Option<JsVideoSize>,
200}
201
202#[derive(serde::Deserialize)]
203struct JsVideoSize {
204 width: f64,
205 height: f64,
206}
207
208#[derive(serde::Deserialize)]
209struct JsViewportSize {
210 width: i64,
211 height: i64,
212}
213
214fn lower_media(v: Option<serde_json::Value>) -> ferridriver::options::MediaOverride {
215 match v {
216 Some(serde_json::Value::Null) => ferridriver::options::MediaOverride::Disabled,
217 Some(serde_json::Value::String(s)) => ferridriver::options::MediaOverride::Set(s),
218 None | Some(_) => ferridriver::options::MediaOverride::Unchanged,
219 }
220}
221
222impl JsBrowserContextOptions {
223 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
224 pub(super) fn into_core(self) -> ferridriver::options::BrowserContextOptions {
225 use ferridriver::options as fo;
226
227 let viewport = match self.viewport {
228 None => fo::ViewportOption::Default,
229 Some(serde_json::Value::Null) => fo::ViewportOption::Null,
230 Some(v) => {
231 let parsed: Result<JsViewportSize, _> = serde_json::from_value(v);
232 match parsed {
233 Ok(vp) => fo::ViewportOption::Size {
234 width: vp.width,
235 height: vp.height,
236 },
237 Err(_) => fo::ViewportOption::Default,
238 }
239 },
240 };
241
242 let http_credentials = self.http_credentials.map(|c| fo::HttpCredentials {
243 username: c.username,
244 password: c.password,
245 origin: c.origin,
246 send: c.send.and_then(|s| match s.as_str() {
247 "always" => Some(fo::HttpCredentialsSend::Always),
248 "unauthorized" => Some(fo::HttpCredentialsSend::Unauthorized),
249 _ => None,
250 }),
251 });
252 let proxy = self.proxy.map(|p| fo::ProxyConfig {
253 server: p.server,
254 bypass: p.bypass,
255 username: p.username,
256 password: p.password,
257 });
258 let record_video = self.record_video.map(|rv| fo::RecordVideoOptions {
259 dir: std::path::PathBuf::from(rv.dir),
260 size: rv.size.map(|s| fo::VideoSize {
261 width: s.width.max(0.0) as u32,
262 height: s.height.max(0.0) as u32,
263 }),
264 });
265 let screen = self.screen.map(|s| fo::ScreenSize {
266 width: s.width,
267 height: s.height,
268 });
269 let service_workers = self.service_workers.and_then(|s| match s.as_str() {
270 "allow" => Some(fo::ServiceWorkerPolicy::Allow),
271 "block" => Some(fo::ServiceWorkerPolicy::Block),
272 _ => None,
273 });
274
275 fo::BrowserContextOptions {
276 accept_downloads: self.accept_downloads,
277 base_url: self.base_url,
278 bypass_csp: self.bypass_csp,
279 color_scheme: lower_media(self.color_scheme),
280 contrast: lower_media(self.contrast),
281 device_scale_factor: self.device_scale_factor,
282 extra_http_headers: self.extra_http_headers,
283 forced_colors: lower_media(self.forced_colors),
284 geolocation: self.geolocation.map(|g| fo::Geolocation {
285 latitude: g.latitude,
286 longitude: g.longitude,
287 accuracy: g.accuracy.unwrap_or(0.0),
288 }),
289 has_touch: self.has_touch,
290 http_credentials,
291 ignore_https_errors: self.ignore_https_errors,
292 is_mobile: self.is_mobile,
293 java_script_enabled: self.java_script_enabled,
294 locale: self.locale,
295 offline: self.offline,
296 permissions: self.permissions,
297 proxy,
298 record_har: None,
299 record_video,
300 reduced_motion: lower_media(self.reduced_motion),
301 screen,
302 service_workers,
303 storage_state: self.storage_state.map(|v| match v {
304 serde_json::Value::String(path) => fo::StorageStateInput::Path(std::path::PathBuf::from(path)),
305 other => fo::StorageStateInput::Inline(other),
306 }),
307 strict_selectors: self.strict_selectors,
308 timezone_id: self.timezone_id,
309 user_agent: self.user_agent,
310 viewport,
311 }
312 }
313}