1use super::*;
8
9#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
14pub struct PdfOptions {
15 #[serde(skip_serializing_if = "Option::is_none")]
17 pub paper_width: Option<f64>,
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub paper_height: Option<f64>,
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub print_background: Option<bool>,
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub scale: Option<f64>,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub display_header_footer: Option<bool>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub margin_top: Option<f64>,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub margin_bottom: Option<f64>,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub margin_left: Option<f64>,
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub margin_right: Option<f64>,
42}
43
44impl PdfOptions {
45 pub fn letter() -> Self {
47 Self {
48 paper_width: Some(8.5),
49 paper_height: Some(11.0),
50 print_background: Some(true),
51 ..Default::default()
52 }
53 }
54}
55
56#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
61pub struct GeoLocation {
62 pub latitude: f64,
64 pub longitude: f64,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub accuracy: Option<f64>,
69}
70
71#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
73pub struct NetworkConditions {
74 pub offline: bool,
75 pub latency_ms: f64,
76 pub download_throughput_bytes: f64,
77 pub upload_throughput_bytes: f64,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub connection_type: Option<String>,
80}
81
82impl NetworkConditions {
83 pub fn preset(name: &str) -> BrowserResult<Self> {
84 match name {
85 "slow-3g" => Ok(Self {
86 offline: false,
87 latency_ms: 400.0,
88 download_throughput_bytes: 50_000.0,
89 upload_throughput_bytes: 50_000.0,
90 connection_type: Some("cellular3g".to_string()),
91 }),
92 "fast-3g" => Ok(Self {
93 offline: false,
94 latency_ms: 100.0,
95 download_throughput_bytes: 1_500_000.0,
96 upload_throughput_bytes: 750_000.0,
97 connection_type: Some("cellular3g".to_string()),
98 }),
99 "offline" => Ok(Self {
100 offline: true,
101 latency_ms: 0.0,
102 download_throughput_bytes: 0.0,
103 upload_throughput_bytes: 0.0,
104 connection_type: Some("none".to_string()),
105 }),
106 _ => Err("network preset must be slow-3g, fast-3g, or offline".into()),
107 }
108 }
109}
110
111impl BrowserSession {
112 pub async fn set_network_conditions(
114 &self,
115 conditions: Option<&NetworkConditions>,
116 ) -> BrowserResult<()> {
117 self.cdp
118 .with_current_route(async {
119 let params = if let Some(conditions) = conditions {
120 serde_json::json!({
121 "offline": conditions.offline,
122 "latency": conditions.latency_ms,
123 "downloadThroughput": conditions.download_throughput_bytes,
124 "uploadThroughput": conditions.upload_throughput_bytes,
125 "connectionType": conditions.connection_type.clone().unwrap_or_else(|| "none".to_string())
126 })
127 } else {
128 serde_json::json!({
129 "offline": false,
130 "latency": 0,
131 "downloadThroughput": -1,
132 "uploadThroughput": -1,
133 "connectionType": "none"
134 })
135 };
136 self.cdp.send("Network.emulateNetworkConditions", Some(params)).await?;
137 Ok(())
138 })
139 .await
140 }
141
142 pub async fn set_cpu_throttling(&self, rate: Option<f64>) -> BrowserResult<()> {
144 let rate = rate.unwrap_or(1.0);
145 if !rate.is_finite() || rate <= 0.0 || rate > 20.0 {
146 return Err("CPU throttling rate must be in (0, 20]".into());
147 }
148 self.cdp
149 .with_current_route(async {
150 self.cdp
151 .send(
152 "Emulation.setCPUThrottlingRate",
153 Some(serde_json::json!({"rate": rate})),
154 )
155 .await?;
156 Ok(())
157 })
158 .await
159 }
160
161 pub async fn set_user_agent(
164 &self,
165 user_agent: Option<&str>,
166 accept_language: Option<&str>,
167 platform: Option<&str>,
168 ) -> BrowserResult<()> {
169 if user_agent.is_some_and(|value| value.len() > 512)
170 || accept_language.is_some_and(|value| value.len() > 128)
171 || platform.is_some_and(|value| value.len() > 128)
172 {
173 return Err("user-agent override is too long".into());
174 }
175 let restore = user_agent.is_none();
176 let original = if restore {
177 self.user_agent_original.lock().await.clone()
178 } else {
179 let existing = self.user_agent_original.lock().await.clone();
180 if let Some(existing) = existing {
181 Some(existing)
182 } else {
183 let current = self
184 .evaluate_value("navigator.userAgent")
185 .await?
186 .as_str()
187 .unwrap_or_default()
188 .to_string();
189 *self.user_agent_original.lock().await = Some(current.clone());
190 Some(current)
191 }
192 };
193 if restore && original.is_none() {
194 return Ok(());
195 }
196 self.cdp
197 .with_current_route(async {
198 let user_agent = user_agent.or(original.as_deref()).unwrap_or_default();
199 let mut params = serde_json::json!({"userAgent": user_agent});
200 if let Some(language) = accept_language {
201 params["acceptLanguage"] = serde_json::Value::String(language.to_string());
202 }
203 if let Some(platform) = platform {
204 params["platform"] = serde_json::Value::String(platform.to_string());
205 }
206 self.cdp
207 .send("Network.setUserAgentOverride", Some(params))
208 .await?;
209 Ok::<(), crate::browser::cdp::CdpError>(())
210 })
211 .await?;
212 if restore {
213 *self.user_agent_original.lock().await = None;
214 }
215 Ok(())
216 }
217
218 pub async fn print_to_pdf(&self, options: &PdfOptions) -> BrowserResult<String> {
223 let mut params = serde_json::Map::new();
224 if let Some(v) = options.paper_width {
225 params.insert("paperWidth".into(), v.into());
226 }
227 if let Some(v) = options.paper_height {
228 params.insert("paperHeight".into(), v.into());
229 }
230 if let Some(v) = options.print_background {
231 params.insert("printBackground".into(), v.into());
232 }
233 if let Some(v) = options.scale {
234 params.insert("scale".into(), v.into());
235 }
236 if let Some(v) = options.display_header_footer {
237 params.insert("displayHeaderFooter".into(), v.into());
238 }
239 if let Some(v) = options.margin_top {
240 params.insert("marginTop".into(), v.into());
241 }
242 if let Some(v) = options.margin_bottom {
243 params.insert("marginBottom".into(), v.into());
244 }
245 if let Some(v) = options.margin_left {
246 params.insert("marginLeft".into(), v.into());
247 }
248 if let Some(v) = options.margin_right {
249 params.insert("marginRight".into(), v.into());
250 }
251 let params = serde_json::Value::Object(params);
252 self.cdp
253 .with_current_route(async {
254 let result = self.cdp.send("Page.printToPDF", Some(params)).await?;
255 Ok(result["data"]
256 .as_str()
257 .ok_or("Page.printToPDF returned no data")?
258 .to_string())
259 })
260 .await
261 }
262
263 pub async fn set_geolocation(&self, location: Option<&GeoLocation>) -> BrowserResult<()> {
268 self.cdp
269 .with_current_route(async {
270 if let Some(loc) = location {
271 self.cdp
272 .send(
273 "Emulation.setGeolocationOverride",
274 Some(serde_json::json!({
275 "latitude": loc.latitude,
276 "longitude": loc.longitude,
277 "accuracy": loc.accuracy.unwrap_or(100.0)
278 })),
279 )
280 .await?;
281 } else {
282 self.cdp
283 .send("Emulation.clearGeolocationOverride", None)
284 .await?;
285 }
286 Ok(())
287 })
288 .await
289 }
290
291 pub async fn set_timezone(&self, timezone_id: Option<&str>) -> BrowserResult<()> {
296 self.cdp
297 .with_current_route(async {
298 let tz = timezone_id.unwrap_or("");
299 self.cdp
300 .send(
301 "Emulation.setTimezoneOverride",
302 Some(serde_json::json!({"timezoneId": tz})),
303 )
304 .await?;
305 Ok(())
306 })
307 .await
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn pdf_options_letter_is_us_letter_with_background() {
317 let opts = PdfOptions::letter();
318 assert_eq!(opts.paper_width, Some(8.5));
319 assert_eq!(opts.paper_height, Some(11.0));
320 assert_eq!(opts.print_background, Some(true));
321 assert_eq!(opts.scale, None);
322 assert_eq!(opts.display_header_footer, None);
323 assert_eq!(opts.margin_top, None);
324 }
325
326 #[test]
327 fn pdf_options_default_is_all_none() {
328 let opts = PdfOptions::default();
329 assert!(opts.paper_width.is_none());
330 assert!(opts.paper_height.is_none());
331 assert!(opts.print_background.is_none());
332 assert!(opts.scale.is_none());
333 assert!(opts.margin_top.is_none());
334 assert!(opts.margin_bottom.is_none());
335 assert!(opts.margin_left.is_none());
336 assert!(opts.margin_right.is_none());
337 }
338
339 #[test]
340 fn geo_location_serializes_without_accuracy_when_none() {
341 let loc = GeoLocation {
342 latitude: 37.7749,
343 longitude: -122.4194,
344 accuracy: None,
345 };
346 let json = serde_json::to_value(&loc).unwrap();
347 assert_eq!(json["latitude"], 37.7749);
348 assert_eq!(json["longitude"], -122.4194);
349 assert!(json.get("accuracy").is_none());
350 }
351
352 #[test]
353 fn geo_location_serializes_accuracy_when_present() {
354 let loc = GeoLocation {
355 latitude: 51.5074,
356 longitude: -0.1278,
357 accuracy: Some(42.0),
358 };
359 let json = serde_json::to_value(&loc).unwrap();
360 assert_eq!(json["accuracy"], 42.0);
361 }
362
363 #[test]
364 fn geo_location_roundtrip_through_json() {
365 let original = GeoLocation {
366 latitude: -33.8688,
367 longitude: 151.2093,
368 accuracy: Some(5.0),
369 };
370 let json = serde_json::to_string(&original).unwrap();
371 let parsed: GeoLocation = serde_json::from_str(&json).unwrap();
372 assert!((parsed.latitude - original.latitude).abs() < 1e-10);
373 assert!((parsed.longitude - original.longitude).abs() < 1e-10);
374 assert!((parsed.accuracy.unwrap() - 5.0).abs() < 1e-10);
375 }
376
377 #[test]
378 fn geo_location_roundtrip_without_accuracy() {
379 let original = GeoLocation {
380 latitude: 35.6762,
381 longitude: 139.6503,
382 accuracy: None,
383 };
384 let json = serde_json::to_string(&original).unwrap();
385 let parsed: GeoLocation = serde_json::from_str(&json).unwrap();
386 assert!((parsed.latitude - 35.6762).abs() < 1e-10);
387 assert!((parsed.longitude - 139.6503).abs() < 1e-10);
388 assert!(parsed.accuracy.is_none());
389 }
390
391 #[test]
392 fn pdf_options_letter_serializes_correctly() {
393 let opts = PdfOptions::letter();
394 let json = serde_json::to_value(&opts).unwrap();
395 assert_eq!(json["paper_width"], 8.5);
396 assert_eq!(json["paper_height"], 11.0);
397 assert_eq!(json["print_background"], true);
398 assert!(json.get("scale").is_none());
400 assert!(json.get("margin_top").is_none());
401 }
402
403 #[test]
404 fn pdf_options_default_serializes_empty_object() {
405 let opts = PdfOptions::default();
406 let json = serde_json::to_value(&opts).unwrap();
407 assert!(json.as_object().unwrap().is_empty());
409 }
410}