chromiumoxide/page.rs
1use std::path::Path;
2use std::sync::Arc;
3
4use chromiumoxide_cdp::cdp::browser_protocol::dom::*;
5use chromiumoxide_cdp::cdp::browser_protocol::emulation::{
6 MediaFeature, SetEmulatedMediaParams, SetGeolocationOverrideParams, SetLocaleOverrideParams,
7 SetTimezoneOverrideParams,
8};
9use chromiumoxide_cdp::cdp::browser_protocol::network::{
10 Cookie, CookieParam, DeleteCookiesParams, GetCookiesParams, SetCookiesParams,
11 SetUserAgentOverrideParams,
12};
13use chromiumoxide_cdp::cdp::browser_protocol::page::*;
14use chromiumoxide_cdp::cdp::browser_protocol::performance::{GetMetricsParams, Metric};
15use chromiumoxide_cdp::cdp::browser_protocol::target::{SessionId, TargetId};
16use chromiumoxide_cdp::cdp::js_protocol;
17use chromiumoxide_cdp::cdp::js_protocol::debugger::GetScriptSourceParams;
18use chromiumoxide_cdp::cdp::js_protocol::runtime::{
19 AddBindingParams, CallArgument, CallFunctionOnParams, EvaluateParams, ExecutionContextId,
20 RemoteObjectType, ScriptId,
21};
22use chromiumoxide_cdp::cdp::{browser_protocol, IntoEventKind};
23use chromiumoxide_types::*;
24use futures::channel::mpsc::unbounded;
25use futures::channel::oneshot::channel as oneshot_channel;
26use futures::{stream, SinkExt, StreamExt};
27
28use crate::auth::Credentials;
29use crate::element::Element;
30use crate::error::{CdpError, Result};
31use crate::handler::commandfuture::CommandFuture;
32use crate::handler::domworld::DOMWorldKind;
33use crate::handler::httpfuture::HttpFuture;
34use crate::handler::target::{GetName, GetParent, GetUrl, TargetMessage};
35use crate::handler::PageInner;
36use crate::javascript::{
37 extract::{FULL_XML_SERIALIZER_JS, OUTER_HTML},
38 spoofs::{
39 DISABLE_DIALOGS, GPU_REQUEST_ADAPTER, GPU_REQUEST_ADAPTER_MAC, GPU_SPOOF_SCRIPT,
40 GPU_SPOOF_SCRIPT_MAC, HIDE_CHROME, HIDE_PERMISSIONS, HIDE_WEBDRIVER, HIDE_WEBGL,
41 HIDE_WEBGL_MAC, NAVIGATOR_SCRIPT, PLUGIN_AND_MIMETYPE_SPOOF,
42 },
43};
44use crate::js::{Evaluation, EvaluationResult};
45use crate::layout::Point;
46use crate::listeners::{EventListenerRequest, EventStream};
47use crate::{utils, ArcHttpRequest};
48
49#[derive(Debug, Clone)]
50pub struct Page {
51 inner: Arc<PageInner>,
52}
53
54/// Tier of stealth to use.
55#[derive(PartialEq, Debug, Default, Copy, Clone, serde::Serialize, serde::Deserialize)]
56pub enum Tier {
57 #[default]
58 /// Basic spoofing.
59 Basic,
60 /// Basic spoofing without webgl.
61 BasicNoWebgl,
62 /// Mid spoofing.
63 Mid,
64 /// Full spoofing.
65 Full,
66 /// No spoofing
67 None,
68}
69
70impl Tier {
71 /// Stealth mode enabled.
72 pub fn stealth(&self) -> bool {
73 match &self {
74 Tier::None => false,
75 _ => true,
76 }
77 }
78}
79
80/// The user agent types of profiles we support for stealth.
81#[derive(PartialEq, Clone, Copy, Default, Debug)]
82pub enum AgentOs {
83 #[default]
84 /// Linux.
85 Linux,
86 /// Mac.
87 Mac,
88 /// Windows.
89 Windows,
90 /// Android.
91 Android,
92}
93
94/// Generate the initial stealth script to send in one command.
95fn build_stealth_script(tier: Tier, os: AgentOs) -> String {
96 let mac_spoof = os == AgentOs::Mac;
97
98 let spoof_gpu = if mac_spoof {
99 GPU_SPOOF_SCRIPT_MAC
100 } else {
101 GPU_SPOOF_SCRIPT
102 };
103
104 let spoof_webgl = if mac_spoof {
105 HIDE_WEBGL_MAC
106 } else {
107 HIDE_WEBGL
108 };
109
110 let spoof_gpu_adapter = if mac_spoof {
111 GPU_REQUEST_ADAPTER_MAC
112 } else {
113 GPU_REQUEST_ADAPTER
114 };
115
116 if tier == Tier::Basic {
117 format!(
118 r#"{HIDE_CHROME};{spoof_webgl};{spoof_gpu_adapter};{HIDE_PERMISSIONS};{NAVIGATOR_SCRIPT};{PLUGIN_AND_MIMETYPE_SPOOF};"#
119 )
120 } else if tier == Tier::BasicNoWebgl {
121 format!(
122 r#"{HIDE_CHROME};{HIDE_PERMISSIONS};{NAVIGATOR_SCRIPT};{PLUGIN_AND_MIMETYPE_SPOOF};"#
123 )
124 } else if tier == Tier::Mid {
125 format!(
126 r#"{HIDE_CHROME};{spoof_webgl};{spoof_gpu_adapter};{HIDE_PERMISSIONS};{HIDE_WEBDRIVER};{NAVIGATOR_SCRIPT};{PLUGIN_AND_MIMETYPE_SPOOF};"#
127 )
128 } else if tier == Tier::Full {
129 format!("{HIDE_CHROME};{spoof_webgl};{spoof_gpu_adapter};{HIDE_PERMISSIONS};{HIDE_WEBDRIVER};{NAVIGATOR_SCRIPT};{PLUGIN_AND_MIMETYPE_SPOOF};{spoof_gpu};")
130 } else {
131 Default::default()
132 }
133}
134
135/// Generate the hide plugins script.
136fn generate_hide_plugins() -> String {
137 format!("{}{}", NAVIGATOR_SCRIPT, PLUGIN_AND_MIMETYPE_SPOOF)
138}
139
140/// Simple function to wrap the eval script safely.
141pub fn wrap_eval_script(source: &str) -> String {
142 format!(r#"(()=>{{{}}})();"#, source)
143}
144
145impl Page {
146 /// Add a custom script to eval on new document.
147 pub async fn add_script_to_evaluate_on_new_document(
148 &self,
149 source: Option<String>,
150 ) -> Result<()> {
151 if source.is_some() {
152 let source = source.unwrap_or_default();
153
154 if !source.is_empty() {
155 self.execute(AddScriptToEvaluateOnNewDocumentParams {
156 source,
157 world_name: None,
158 include_command_line_api: None,
159 run_immediately: None,
160 })
161 .await?;
162 }
163 }
164 Ok(())
165 }
166
167 /// Removes the `navigator.webdriver` property
168 /// changes permissions, pluggins rendering contexts and the `window.chrome`
169 /// property to make it harder to detect the scraper as a bot
170 pub async fn _enable_stealth_mode(
171 &self,
172 custom_script: Option<&str>,
173 os: Option<AgentOs>,
174 tier: Option<Tier>,
175 ) -> Result<()> {
176 let os = os.unwrap_or_default();
177 let tier = match tier {
178 Some(tier) => tier,
179 _ => Tier::Basic,
180 };
181
182 let source = if let Some(cs) = custom_script {
183 format!("{};{cs}", build_stealth_script(tier, os))
184 } else {
185 build_stealth_script(tier, os)
186 };
187 let source = wrap_eval_script(&source);
188
189 self.add_script_to_evaluate_on_new_document(Some(source))
190 .await?;
191
192 Ok(())
193 }
194
195 /// Changes your user_agent, removes the `navigator.webdriver` property
196 /// changes permissions, pluggins rendering contexts and the `window.chrome`
197 /// property to make it harder to detect the scraper as a bot
198 pub async fn enable_stealth_mode(&self) -> Result<()> {
199 let _ = self._enable_stealth_mode(None, None, None).await;
200
201 Ok(())
202 }
203
204 /// Changes your user_agent, removes the `navigator.webdriver` property
205 /// changes permissions, pluggins rendering contexts and the `window.chrome`
206 /// property to make it harder to detect the scraper as a bot
207 pub async fn enable_stealth_mode_os(
208 &self,
209 os: Option<AgentOs>,
210 tier: Option<Tier>,
211 ) -> Result<()> {
212 let _ = self._enable_stealth_mode(None, os, tier).await;
213
214 Ok(())
215 }
216
217 /// Changes your user_agent with a custom agent, removes the `navigator.webdriver` property
218 /// changes permissions, pluggins rendering contexts and the `window.chrome`
219 /// property to make it harder to detect the scraper as a bot
220 pub async fn enable_stealth_mode_with_agent(&self, ua: &str) -> Result<()> {
221 let _ = tokio::join!(
222 self._enable_stealth_mode(None, None, None),
223 self.set_user_agent(ua)
224 );
225 Ok(())
226 }
227
228 /// Changes your user_agent with a custom agent, removes the `navigator.webdriver` property
229 /// changes permissions, pluggins rendering contexts and the `window.chrome`
230 /// property to make it harder to detect the scraper as a bot. Also add dialog polyfill to prevent blocking the page.
231 pub async fn enable_stealth_mode_with_dimiss_dialogs(&self, ua: &str) -> Result<()> {
232 let _ = tokio::join!(
233 self._enable_stealth_mode(Some(DISABLE_DIALOGS), None, None),
234 self.set_user_agent(ua)
235 );
236 Ok(())
237 }
238
239 /// Changes your user_agent with a custom agent, removes the `navigator.webdriver` property
240 /// changes permissions, pluggins rendering contexts and the `window.chrome`
241 /// property to make it harder to detect the scraper as a bot. Also add dialog polyfill to prevent blocking the page.
242 pub async fn enable_stealth_mode_with_agent_and_dimiss_dialogs(&self, ua: &str) -> Result<()> {
243 let _ = tokio::join!(
244 self._enable_stealth_mode(Some(DISABLE_DIALOGS), None, None),
245 self.set_user_agent(ua)
246 );
247 Ok(())
248 }
249
250 /// Sets `window.chrome` on frame creation and console.log methods.
251 pub async fn hide_chrome(&self) -> Result<(), CdpError> {
252 self.execute(AddScriptToEvaluateOnNewDocumentParams {
253 source: HIDE_CHROME.to_string(),
254 world_name: None,
255 include_command_line_api: None,
256 run_immediately: None,
257 })
258 .await?;
259 Ok(())
260 }
261
262 /// Obfuscates WebGL vendor on frame creation
263 pub async fn hide_webgl_vendor(&self) -> Result<(), CdpError> {
264 self.execute(AddScriptToEvaluateOnNewDocumentParams {
265 source: HIDE_WEBGL.to_string(),
266 world_name: None,
267 include_command_line_api: None,
268 run_immediately: None,
269 })
270 .await?;
271 Ok(())
272 }
273
274 /// Obfuscates browser plugins and hides the navigator object on frame creation
275 pub async fn hide_plugins(&self) -> Result<(), CdpError> {
276 self.execute(AddScriptToEvaluateOnNewDocumentParams {
277 source: generate_hide_plugins(),
278 world_name: None,
279 include_command_line_api: None,
280 run_immediately: None,
281 })
282 .await?;
283
284 Ok(())
285 }
286
287 /// Obfuscates browser permissions on frame creation
288 pub async fn hide_permissions(&self) -> Result<(), CdpError> {
289 self.execute(AddScriptToEvaluateOnNewDocumentParams {
290 source: HIDE_PERMISSIONS.to_string(),
291 world_name: None,
292 include_command_line_api: None,
293 run_immediately: None,
294 })
295 .await?;
296 Ok(())
297 }
298
299 /// Removes the `navigator.webdriver` property on frame creation
300 pub async fn hide_webdriver(&self) -> Result<(), CdpError> {
301 self.execute(AddScriptToEvaluateOnNewDocumentParams {
302 source: HIDE_WEBDRIVER.to_string(),
303 world_name: None,
304 include_command_line_api: None,
305 run_immediately: None,
306 })
307 .await?;
308 Ok(())
309 }
310
311 /// Execute a command and return the `Command::Response`
312 pub async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
313 self.command_future(cmd)?.await
314 }
315
316 /// Execute a command and return the `Command::Response`
317 pub fn command_future<T: Command>(&self, cmd: T) -> Result<CommandFuture<T>> {
318 self.inner.command_future(cmd)
319 }
320
321 /// Execute a command and return the `Command::Response`
322 pub fn http_future<T: Command>(&self, cmd: T) -> Result<HttpFuture<T>> {
323 self.inner.http_future(cmd)
324 }
325
326 /// Adds an event listener to the `Target` and returns the receiver part as
327 /// `EventStream`
328 ///
329 /// An `EventStream` receives every `Event` the `Target` receives.
330 /// All event listener get notified with the same event, so registering
331 /// multiple listeners for the same event is possible.
332 ///
333 /// Custom events rely on being deserializable from the received json params
334 /// in the `EventMessage`. Custom Events are caught by the `CdpEvent::Other`
335 /// variant. If there are mulitple custom event listener is registered
336 /// for the same event, identified by the `MethodType::method_id` function,
337 /// the `Target` tries to deserialize the json using the type of the event
338 /// listener. Upon success the `Target` then notifies all listeners with the
339 /// deserialized event. This means, while it is possible to register
340 /// different types for the same custom event, only the type of first
341 /// registered event listener will be used. The subsequent listeners, that
342 /// registered for the same event but with another type won't be able to
343 /// receive anything and therefor will come up empty until all their
344 /// preceding event listeners are dropped and they become the first (or
345 /// longest) registered event listener for an event.
346 ///
347 /// # Example Listen for canceled animations
348 /// ```no_run
349 /// # use chromiumoxide::page::Page;
350 /// # use chromiumoxide::error::Result;
351 /// # use chromiumoxide_cdp::cdp::browser_protocol::animation::EventAnimationCanceled;
352 /// # use futures::StreamExt;
353 /// # async fn demo(page: Page) -> Result<()> {
354 /// let mut events = page.event_listener::<EventAnimationCanceled>().await?;
355 /// while let Some(event) = events.next().await {
356 /// //..
357 /// }
358 /// # Ok(())
359 /// # }
360 /// ```
361 ///
362 /// # Example Liste for a custom event
363 ///
364 /// ```no_run
365 /// # use chromiumoxide::page::Page;
366 /// # use chromiumoxide::error::Result;
367 /// # use futures::StreamExt;
368 /// # use serde::Deserialize;
369 /// # use chromiumoxide::types::{MethodId, MethodType};
370 /// # use chromiumoxide::cdp::CustomEvent;
371 /// # async fn demo(page: Page) -> Result<()> {
372 /// #[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
373 /// struct MyCustomEvent {
374 /// name: String,
375 /// }
376 /// impl MethodType for MyCustomEvent {
377 /// fn method_id() -> MethodId {
378 /// "Custom.Event".into()
379 /// }
380 /// }
381 /// impl CustomEvent for MyCustomEvent {}
382 /// let mut events = page.event_listener::<MyCustomEvent>().await?;
383 /// while let Some(event) = events.next().await {
384 /// //..
385 /// }
386 ///
387 /// # Ok(())
388 /// # }
389 /// ```
390 pub async fn event_listener<T: IntoEventKind>(&self) -> Result<EventStream<T>> {
391 let (tx, rx) = unbounded();
392
393 self.inner
394 .sender()
395 .clone()
396 .send(TargetMessage::AddEventListener(
397 EventListenerRequest::new::<T>(tx),
398 ))
399 .await?;
400
401 Ok(EventStream::new(rx))
402 }
403
404 pub async fn expose_function(
405 &self,
406 name: impl Into<String>,
407 function: impl AsRef<str>,
408 ) -> Result<()> {
409 let name = name.into();
410 let expression = utils::evaluation_string(function, &["exposedFun", name.as_str()]);
411
412 self.execute(AddBindingParams::new(name)).await?;
413 self.execute(AddScriptToEvaluateOnNewDocumentParams::new(
414 expression.clone(),
415 ))
416 .await?;
417
418 // TODO add execution context tracking for frames
419 //let frames = self.frames().await?;
420
421 Ok(())
422 }
423
424 /// This resolves once the navigation finished and the page is loaded.
425 ///
426 /// This is necessary after an interaction with the page that may trigger a
427 /// navigation (`click`, `press_key`) in order to wait until the new browser
428 /// page is loaded
429 pub async fn wait_for_navigation_response(&self) -> Result<ArcHttpRequest> {
430 self.inner.wait_for_navigation().await
431 }
432
433 /// Same as `wait_for_navigation_response` but returns `Self` instead
434 pub async fn wait_for_navigation(&self) -> Result<&Self> {
435 self.inner.wait_for_navigation().await?;
436 Ok(self)
437 }
438
439 /// Navigate directly to the given URL.
440 ///
441 /// This resolves directly after the requested URL is fully loaded.
442 pub async fn goto(&self, params: impl Into<NavigateParams>) -> Result<&Self> {
443 let res = self.execute(params.into()).await?;
444
445 if let Some(err) = res.result.error_text {
446 return Err(CdpError::ChromeMessage(err));
447 }
448
449 Ok(self)
450 }
451
452 /// The identifier of the `Target` this page belongs to
453 pub fn target_id(&self) -> &TargetId {
454 self.inner.target_id()
455 }
456
457 /// The identifier of the `Session` target of this page is attached to
458 pub fn session_id(&self) -> &SessionId {
459 self.inner.session_id()
460 }
461
462 /// The identifier of the `Session` target of this page is attached to
463 pub fn opener_id(&self) -> &Option<TargetId> {
464 self.inner.opener_id()
465 }
466
467 /// Returns the name of the frame
468 pub async fn frame_name(&self, frame_id: FrameId) -> Result<Option<String>> {
469 let (tx, rx) = oneshot_channel();
470 self.inner
471 .sender()
472 .clone()
473 .send(TargetMessage::Name(GetName {
474 frame_id: Some(frame_id),
475 tx,
476 }))
477 .await?;
478 Ok(rx.await?)
479 }
480
481 pub async fn authenticate(&self, credentials: Credentials) -> Result<()> {
482 self.inner
483 .sender()
484 .clone()
485 .send(TargetMessage::Authenticate(credentials))
486 .await?;
487
488 Ok(())
489 }
490
491 /// Returns the current url of the page
492 pub async fn url(&self) -> Result<Option<String>> {
493 let (tx, rx) = oneshot_channel();
494 self.inner
495 .sender()
496 .clone()
497 .send(TargetMessage::Url(GetUrl::new(tx)))
498 .await?;
499 Ok(rx.await?)
500 }
501
502 /// Returns the current url of the frame
503 pub async fn frame_url(&self, frame_id: FrameId) -> Result<Option<String>> {
504 let (tx, rx) = oneshot_channel();
505 self.inner
506 .sender()
507 .clone()
508 .send(TargetMessage::Url(GetUrl {
509 frame_id: Some(frame_id),
510 tx,
511 }))
512 .await?;
513 Ok(rx.await?)
514 }
515
516 /// Returns the parent id of the frame
517 pub async fn frame_parent(&self, frame_id: FrameId) -> Result<Option<FrameId>> {
518 let (tx, rx) = oneshot_channel();
519 self.inner
520 .sender()
521 .clone()
522 .send(TargetMessage::Parent(GetParent { frame_id, tx }))
523 .await?;
524 Ok(rx.await?)
525 }
526
527 /// Return the main frame of the page
528 pub async fn mainframe(&self) -> Result<Option<FrameId>> {
529 let (tx, rx) = oneshot_channel();
530 self.inner
531 .sender()
532 .clone()
533 .send(TargetMessage::MainFrame(tx))
534 .await?;
535 Ok(rx.await?)
536 }
537
538 /// Return the frames of the page
539 pub async fn frames(&self) -> Result<Vec<FrameId>> {
540 let (tx, rx) = oneshot_channel();
541 self.inner
542 .sender()
543 .clone()
544 .send(TargetMessage::AllFrames(tx))
545 .await?;
546 Ok(rx.await?)
547 }
548
549 /// Allows overriding user agent with the given string.
550 pub async fn set_user_agent(
551 &self,
552 params: impl Into<SetUserAgentOverrideParams>,
553 ) -> Result<&Self> {
554 self.execute(params.into()).await?;
555 Ok(self)
556 }
557
558 /// Returns the user agent of the browser
559 pub async fn user_agent(&self) -> Result<String> {
560 Ok(self.inner.version().await?.user_agent)
561 }
562
563 /// Returns the root DOM node (and optionally the subtree) of the page.
564 ///
565 /// # Note: This does not return the actual HTML document of the page. To
566 /// retrieve the HTML content of the page see `Page::content`.
567 pub async fn get_document(&self) -> Result<Node> {
568 let mut cmd = GetDocumentParams::default();
569 cmd.depth = Some(-1);
570 cmd.pierce = Some(true);
571
572 let resp = self.execute(cmd).await?;
573
574 Ok(resp.result.root)
575 }
576
577 /// Returns the first element in the document which matches the given CSS
578 /// selector.
579 ///
580 /// Execute a query selector on the document's node.
581 pub async fn find_element(&self, selector: impl Into<String>) -> Result<Element> {
582 let root = self.get_document().await?.node_id;
583 let node_id = self.inner.find_element(selector, root).await?;
584 Element::new(Arc::clone(&self.inner), node_id).await
585 }
586
587 /// Returns the outer HTML of the page.
588 pub async fn outer_html(&self) -> Result<String> {
589 let root = self.get_document().await?;
590 let element = Element::new(Arc::clone(&self.inner), root.node_id).await?;
591 self.inner
592 .outer_html(
593 element.remote_object_id,
594 element.node_id,
595 element.backend_node_id,
596 )
597 .await
598 }
599
600 /// Return all `Element`s in the document that match the given selector
601 pub async fn find_elements(&self, selector: impl Into<String>) -> Result<Vec<Element>> {
602 let root = self.get_document().await?.node_id;
603 let node_ids = self.inner.find_elements(selector, root).await?;
604 Element::from_nodes(&self.inner, &node_ids).await
605 }
606
607 /// Returns the first element in the document which matches the given xpath
608 /// selector.
609 ///
610 /// Execute a xpath selector on the document's node.
611 pub async fn find_xpath(&self, selector: impl Into<String>) -> Result<Element> {
612 self.get_document().await?;
613 let node_id = self.inner.find_xpaths(selector).await?[0];
614 Element::new(Arc::clone(&self.inner), node_id).await
615 }
616
617 /// Return all `Element`s in the document that match the given xpath selector
618 pub async fn find_xpaths(&self, selector: impl Into<String>) -> Result<Vec<Element>> {
619 self.get_document().await?;
620 let node_ids = self.inner.find_xpaths(selector).await?;
621 Element::from_nodes(&self.inner, &node_ids).await
622 }
623
624 /// Describes node given its id
625 pub async fn describe_node(&self, node_id: NodeId) -> Result<Node> {
626 let resp = self
627 .execute(DescribeNodeParams::builder().node_id(node_id).build())
628 .await?;
629 Ok(resp.result.node)
630 }
631
632 /// Tries to close page, running its beforeunload hooks, if any.
633 /// Calls Page.close with [`CloseParams`]
634 pub async fn close(self) -> Result<()> {
635 self.execute(CloseParams::default()).await?;
636 Ok(())
637 }
638
639 /// Performs a single mouse click event at the point's location.
640 ///
641 /// This scrolls the point into view first, then executes a
642 /// `DispatchMouseEventParams` command of type `MouseLeft` with
643 /// `MousePressed` as single click and then releases the mouse with an
644 /// additional `DispatchMouseEventParams` of type `MouseLeft` with
645 /// `MouseReleased`
646 ///
647 /// Bear in mind that if `click()` triggers a navigation the new page is not
648 /// immediately loaded when `click()` resolves. To wait until navigation is
649 /// finished an additional `wait_for_navigation()` is required:
650 ///
651 /// # Example
652 ///
653 /// Trigger a navigation and wait until the triggered navigation is finished
654 ///
655 /// ```no_run
656 /// # use chromiumoxide::page::Page;
657 /// # use chromiumoxide::error::Result;
658 /// # use chromiumoxide::layout::Point;
659 /// # async fn demo(page: Page, point: Point) -> Result<()> {
660 /// let html = page.click(point).await?.wait_for_navigation().await?.content();
661 /// # Ok(())
662 /// # }
663 /// ```
664 ///
665 /// # Example
666 ///
667 /// Perform custom click
668 ///
669 /// ```no_run
670 /// # use chromiumoxide::page::Page;
671 /// # use chromiumoxide::error::Result;
672 /// # use chromiumoxide::layout::Point;
673 /// # use chromiumoxide_cdp::cdp::browser_protocol::input::{DispatchMouseEventParams, MouseButton, DispatchMouseEventType};
674 /// # async fn demo(page: Page, point: Point) -> Result<()> {
675 /// // double click
676 /// let cmd = DispatchMouseEventParams::builder()
677 /// .x(point.x)
678 /// .y(point.y)
679 /// .button(MouseButton::Left)
680 /// .click_count(2);
681 ///
682 /// page.move_mouse(point).await?.execute(
683 /// cmd.clone()
684 /// .r#type(DispatchMouseEventType::MousePressed)
685 /// .build()
686 /// .unwrap(),
687 /// )
688 /// .await?;
689 ///
690 /// page.execute(
691 /// cmd.r#type(DispatchMouseEventType::MouseReleased)
692 /// .build()
693 /// .unwrap(),
694 /// )
695 /// .await?;
696 ///
697 /// # Ok(())
698 /// # }
699 /// ```
700 pub async fn click(&self, point: Point) -> Result<&Self> {
701 self.inner.click(point).await?;
702 Ok(self)
703 }
704
705 /// Dispatches a `mousemove` event and moves the mouse to the position of
706 /// the `point` where `Point.x` is the horizontal position of the mouse and
707 /// `Point.y` the vertical position of the mouse.
708 pub async fn move_mouse(&self, point: Point) -> Result<&Self> {
709 self.inner.move_mouse(point).await?;
710 Ok(self)
711 }
712
713 /// Take a screenshot of the current page
714 pub async fn screenshot(&self, params: impl Into<ScreenshotParams>) -> Result<Vec<u8>> {
715 self.inner.screenshot(params).await
716 }
717
718 /// Take a screenshot of the current page
719 pub async fn print_to_pdf(&self, params: impl Into<PrintToPdfParams>) -> Result<Vec<u8>> {
720 self.inner.print_to_pdf(params).await
721 }
722
723 /// Save a screenshot of the page
724 ///
725 /// # Example save a png file of a website
726 ///
727 /// ```no_run
728 /// # use chromiumoxide::page::{Page, ScreenshotParams};
729 /// # use chromiumoxide::error::Result;
730 /// # use chromiumoxide_cdp::cdp::browser_protocol::page::CaptureScreenshotFormat;
731 /// # async fn demo(page: Page) -> Result<()> {
732 /// page.goto("http://example.com")
733 /// .await?
734 /// .save_screenshot(
735 /// ScreenshotParams::builder()
736 /// .format(CaptureScreenshotFormat::Png)
737 /// .full_page(true)
738 /// .omit_background(true)
739 /// .build(),
740 /// "example.png",
741 /// )
742 /// .await?;
743 /// # Ok(())
744 /// # }
745 /// ```
746 pub async fn save_screenshot(
747 &self,
748 params: impl Into<ScreenshotParams>,
749 output: impl AsRef<Path>,
750 ) -> Result<Vec<u8>> {
751 let img = self.screenshot(params).await?;
752 utils::write(output.as_ref(), &img).await?;
753 Ok(img)
754 }
755
756 /// Print the current page as pdf.
757 ///
758 /// See [`PrintToPdfParams`]
759 ///
760 /// # Note Generating a pdf is currently only supported in Chrome headless.
761 pub async fn pdf(&self, params: PrintToPdfParams) -> Result<Vec<u8>> {
762 let res = self.execute(params).await?;
763 Ok(utils::base64::decode(&res.data)?)
764 }
765
766 /// Save the current page as pdf as file to the `output` path and return the
767 /// pdf contents.
768 ///
769 /// # Note Generating a pdf is currently only supported in Chrome headless.
770 pub async fn save_pdf(
771 &self,
772 opts: PrintToPdfParams,
773 output: impl AsRef<Path>,
774 ) -> Result<Vec<u8>> {
775 let pdf = self.pdf(opts).await?;
776 utils::write(output.as_ref(), &pdf).await?;
777 Ok(pdf)
778 }
779
780 /// Brings page to front (activates tab)
781 pub async fn bring_to_front(&self) -> Result<&Self> {
782 self.execute(BringToFrontParams::default()).await?;
783 Ok(self)
784 }
785
786 /// Emulates the given media type or media feature for CSS media queries
787 pub async fn emulate_media_features(&self, features: Vec<MediaFeature>) -> Result<&Self> {
788 self.execute(SetEmulatedMediaParams::builder().features(features).build())
789 .await?;
790 Ok(self)
791 }
792
793 /// Changes the CSS media type of the page
794 // Based on https://pptr.dev/api/puppeteer.page.emulatemediatype
795 pub async fn emulate_media_type(
796 &self,
797 media_type: impl Into<MediaTypeParams>,
798 ) -> Result<&Self> {
799 self.execute(
800 SetEmulatedMediaParams::builder()
801 .media(media_type.into())
802 .build(),
803 )
804 .await?;
805 Ok(self)
806 }
807
808 /// Overrides default host system timezone
809 pub async fn emulate_timezone(
810 &self,
811 timezoune_id: impl Into<SetTimezoneOverrideParams>,
812 ) -> Result<&Self> {
813 self.execute(timezoune_id.into()).await?;
814 Ok(self)
815 }
816
817 /// Overrides default host system locale with the specified one
818 pub async fn emulate_locale(
819 &self,
820 locale: impl Into<SetLocaleOverrideParams>,
821 ) -> Result<&Self> {
822 self.execute(locale.into()).await?;
823 Ok(self)
824 }
825
826 /// Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position unavailable.
827 pub async fn emulate_geolocation(
828 &self,
829 geolocation: impl Into<SetGeolocationOverrideParams>,
830 ) -> Result<&Self> {
831 self.execute(geolocation.into()).await?;
832 Ok(self)
833 }
834
835 /// Reloads given page
836 ///
837 /// To reload ignoring cache run:
838 /// ```no_run
839 /// # use chromiumoxide::page::Page;
840 /// # use chromiumoxide::error::Result;
841 /// # use chromiumoxide_cdp::cdp::browser_protocol::page::ReloadParams;
842 /// # async fn demo(page: Page) -> Result<()> {
843 /// page.execute(ReloadParams::builder().ignore_cache(true).build()).await?;
844 /// page.wait_for_navigation().await?;
845 /// # Ok(())
846 /// # }
847 /// ```
848 pub async fn reload(&self) -> Result<&Self> {
849 self.execute(ReloadParams::default()).await?;
850 self.wait_for_navigation().await
851 }
852
853 /// Enables log domain. Enabled by default.
854 ///
855 /// Sends the entries collected so far to the client by means of the
856 /// entryAdded notification.
857 ///
858 /// See https://chromedevtools.github.io/devtools-protocol/tot/Log#method-enable
859 pub async fn enable_log(&self) -> Result<&Self> {
860 self.execute(browser_protocol::log::EnableParams::default())
861 .await?;
862 Ok(self)
863 }
864
865 /// Disables log domain
866 ///
867 /// Prevents further log entries from being reported to the client
868 ///
869 /// See https://chromedevtools.github.io/devtools-protocol/tot/Log#method-disable
870 pub async fn disable_log(&self) -> Result<&Self> {
871 self.execute(browser_protocol::log::DisableParams::default())
872 .await?;
873 Ok(self)
874 }
875
876 /// Enables runtime domain. Activated by default.
877 pub async fn enable_runtime(&self) -> Result<&Self> {
878 self.execute(js_protocol::runtime::EnableParams::default())
879 .await?;
880 Ok(self)
881 }
882
883 /// Disables runtime domain.
884 pub async fn disable_runtime(&self) -> Result<&Self> {
885 self.execute(js_protocol::runtime::DisableParams::default())
886 .await?;
887 Ok(self)
888 }
889
890 /// Enables Debugger. Enabled by default.
891 pub async fn enable_debugger(&self) -> Result<&Self> {
892 self.execute(js_protocol::debugger::EnableParams::default())
893 .await?;
894 Ok(self)
895 }
896
897 /// Disables Debugger.
898 pub async fn disable_debugger(&self) -> Result<&Self> {
899 self.execute(js_protocol::debugger::DisableParams::default())
900 .await?;
901 Ok(self)
902 }
903
904 // Enables DOM agent
905 pub async fn enable_dom(&self) -> Result<&Self> {
906 self.execute(browser_protocol::dom::EnableParams::default())
907 .await?;
908 Ok(self)
909 }
910
911 // Disables DOM agent
912 pub async fn disable_dom(&self) -> Result<&Self> {
913 self.execute(browser_protocol::dom::DisableParams::default())
914 .await?;
915 Ok(self)
916 }
917
918 // Enables the CSS agent
919 pub async fn enable_css(&self) -> Result<&Self> {
920 self.execute(browser_protocol::css::EnableParams::default())
921 .await?;
922 Ok(self)
923 }
924
925 // Disables the CSS agent
926 pub async fn disable_css(&self) -> Result<&Self> {
927 self.execute(browser_protocol::css::DisableParams::default())
928 .await?;
929 Ok(self)
930 }
931
932 /// Activates (focuses) the target.
933 pub async fn activate(&self) -> Result<&Self> {
934 self.inner.activate().await?;
935 Ok(self)
936 }
937
938 /// Returns all cookies that match the tab's current URL.
939 pub async fn get_cookies(&self) -> Result<Vec<Cookie>> {
940 Ok(self
941 .execute(GetCookiesParams::default())
942 .await?
943 .result
944 .cookies)
945 }
946
947 /// Set a single cookie
948 ///
949 /// This fails if the cookie's url or if not provided, the page's url is
950 /// `about:blank` or a `data:` url.
951 ///
952 /// # Example
953 /// ```no_run
954 /// # use chromiumoxide::page::Page;
955 /// # use chromiumoxide::error::Result;
956 /// # use chromiumoxide_cdp::cdp::browser_protocol::network::CookieParam;
957 /// # async fn demo(page: Page) -> Result<()> {
958 /// page.set_cookie(CookieParam::new("Cookie-name", "Cookie-value")).await?;
959 /// # Ok(())
960 /// # }
961 /// ```
962 pub async fn set_cookie(&self, cookie: impl Into<CookieParam>) -> Result<&Self> {
963 let mut cookie = cookie.into();
964 if let Some(url) = cookie.url.as_ref() {
965 validate_cookie_url(url)?;
966 } else {
967 let url = self
968 .url()
969 .await?
970 .ok_or_else(|| CdpError::msg("Page url not found"))?;
971 validate_cookie_url(&url)?;
972 if url.starts_with("http") {
973 cookie.url = Some(url);
974 }
975 }
976 self.execute(DeleteCookiesParams::from_cookie(&cookie))
977 .await?;
978 self.execute(SetCookiesParams::new(vec![cookie])).await?;
979 Ok(self)
980 }
981
982 /// Set all the cookies
983 pub async fn set_cookies(&self, mut cookies: Vec<CookieParam>) -> Result<&Self> {
984 let url = self
985 .url()
986 .await?
987 .ok_or_else(|| CdpError::msg("Page url not found"))?;
988 let is_http = url.starts_with("http");
989 if !is_http {
990 validate_cookie_url(&url)?;
991 }
992
993 for cookie in &mut cookies {
994 if let Some(url) = cookie.url.as_ref() {
995 validate_cookie_url(url)?;
996 } else if is_http {
997 cookie.url = Some(url.clone());
998 }
999 }
1000 self.delete_cookies_unchecked(cookies.iter().map(DeleteCookiesParams::from_cookie))
1001 .await?;
1002
1003 self.execute(SetCookiesParams::new(cookies)).await?;
1004 Ok(self)
1005 }
1006
1007 /// Delete a single cookie
1008 pub async fn delete_cookie(&self, cookie: impl Into<DeleteCookiesParams>) -> Result<&Self> {
1009 let mut cookie = cookie.into();
1010 if cookie.url.is_none() {
1011 let url = self
1012 .url()
1013 .await?
1014 .ok_or_else(|| CdpError::msg("Page url not found"))?;
1015 if url.starts_with("http") {
1016 cookie.url = Some(url);
1017 }
1018 }
1019 self.execute(cookie).await?;
1020 Ok(self)
1021 }
1022
1023 /// Delete all the cookies
1024 pub async fn delete_cookies(&self, mut cookies: Vec<DeleteCookiesParams>) -> Result<&Self> {
1025 let mut url: Option<(String, bool)> = None;
1026 for cookie in &mut cookies {
1027 if cookie.url.is_none() {
1028 if let Some((url, is_http)) = url.as_ref() {
1029 if *is_http {
1030 cookie.url = Some(url.clone())
1031 }
1032 } else {
1033 let page_url = self
1034 .url()
1035 .await?
1036 .ok_or_else(|| CdpError::msg("Page url not found"))?;
1037 let is_http = page_url.starts_with("http");
1038 if is_http {
1039 cookie.url = Some(page_url.clone())
1040 }
1041 url = Some((page_url, is_http));
1042 }
1043 }
1044 }
1045 self.delete_cookies_unchecked(cookies.into_iter()).await?;
1046 Ok(self)
1047 }
1048
1049 /// Convenience method that prevents another channel roundtrip to get the
1050 /// url and validate it
1051 async fn delete_cookies_unchecked(
1052 &self,
1053 cookies: impl Iterator<Item = DeleteCookiesParams>,
1054 ) -> Result<&Self> {
1055 // NOTE: the buffer size is arbitrary
1056 let mut cmds = stream::iter(cookies.into_iter().map(|cookie| self.execute(cookie)))
1057 .buffer_unordered(5);
1058 while let Some(resp) = cmds.next().await {
1059 resp?;
1060 }
1061 Ok(self)
1062 }
1063
1064 /// Returns the title of the document.
1065 pub async fn get_title(&self) -> Result<Option<String>> {
1066 let result = self.evaluate("document.title").await?;
1067
1068 let title: String = result.into_value()?;
1069
1070 if title.is_empty() {
1071 Ok(None)
1072 } else {
1073 Ok(Some(title))
1074 }
1075 }
1076
1077 /// Retrieve current values of run-time metrics.
1078 pub async fn metrics(&self) -> Result<Vec<Metric>> {
1079 Ok(self
1080 .execute(GetMetricsParams::default())
1081 .await?
1082 .result
1083 .metrics)
1084 }
1085
1086 /// Returns metrics relating to the layout of the page
1087 pub async fn layout_metrics(&self) -> Result<GetLayoutMetricsReturns> {
1088 self.inner.layout_metrics().await
1089 }
1090
1091 /// This evaluates strictly as expression.
1092 ///
1093 /// Same as `Page::evaluate` but no fallback or any attempts to detect
1094 /// whether the expression is actually a function. However you can
1095 /// submit a function evaluation string:
1096 ///
1097 /// # Example Evaluate function call as expression
1098 ///
1099 /// This will take the arguments `(1,2)` and will call the function
1100 ///
1101 /// ```no_run
1102 /// # use chromiumoxide::page::Page;
1103 /// # use chromiumoxide::error::Result;
1104 /// # async fn demo(page: Page) -> Result<()> {
1105 /// let sum: usize = page
1106 /// .evaluate_expression("((a,b) => {return a + b;})(1,2)")
1107 /// .await?
1108 /// .into_value()?;
1109 /// assert_eq!(sum, 3);
1110 /// # Ok(())
1111 /// # }
1112 /// ```
1113 pub async fn evaluate_expression(
1114 &self,
1115 evaluate: impl Into<EvaluateParams>,
1116 ) -> Result<EvaluationResult> {
1117 self.inner.evaluate_expression(evaluate).await
1118 }
1119
1120 /// Evaluates an expression or function in the page's context and returns
1121 /// the result.
1122 ///
1123 /// In contrast to `Page::evaluate_expression` this is capable of handling
1124 /// function calls and expressions alike. This takes anything that is
1125 /// `Into<Evaluation>`. When passing a `String` or `str`, this will try to
1126 /// detect whether it is a function or an expression. JS function detection
1127 /// is not very sophisticated but works for general cases (`(async)
1128 /// functions` and arrow functions). If you want a string statement
1129 /// specifically evaluated as expression or function either use the
1130 /// designated functions `Page::evaluate_function` or
1131 /// `Page::evaluate_expression` or use the proper parameter type for
1132 /// `Page::execute`: `EvaluateParams` for strict expression evaluation or
1133 /// `CallFunctionOnParams` for strict function evaluation.
1134 ///
1135 /// If you don't trust the js function detection and are not sure whether
1136 /// the statement is an expression or of type function (arrow functions: `()
1137 /// => {..}`), you should pass it as `EvaluateParams` and set the
1138 /// `EvaluateParams::eval_as_function_fallback` option. This will first
1139 /// try to evaluate it as expression and if the result comes back
1140 /// evaluated as `RemoteObjectType::Function` it will submit the
1141 /// statement again but as function:
1142 ///
1143 /// # Example Evaluate function statement as expression with fallback
1144 /// option
1145 ///
1146 /// ```no_run
1147 /// # use chromiumoxide::page::Page;
1148 /// # use chromiumoxide::error::Result;
1149 /// # use chromiumoxide_cdp::cdp::js_protocol::runtime::{EvaluateParams, RemoteObjectType};
1150 /// # async fn demo(page: Page) -> Result<()> {
1151 /// let eval = EvaluateParams::builder().expression("() => {return 42;}");
1152 /// // this will fail because the `EvaluationResult` returned by the browser will be
1153 /// // of type `Function`
1154 /// let result = page
1155 /// .evaluate(eval.clone().build().unwrap())
1156 /// .await?;
1157 /// assert_eq!(result.object().r#type, RemoteObjectType::Function);
1158 /// assert!(result.into_value::<usize>().is_err());
1159 ///
1160 /// // This will also fail on the first try but it detects that the browser evaluated the
1161 /// // statement as function and then evaluate it again but as function
1162 /// let sum: usize = page
1163 /// .evaluate(eval.eval_as_function_fallback(true).build().unwrap())
1164 /// .await?
1165 /// .into_value()?;
1166 /// # Ok(())
1167 /// # }
1168 /// ```
1169 ///
1170 /// # Example Evaluate basic expression
1171 /// ```no_run
1172 /// # use chromiumoxide::page::Page;
1173 /// # use chromiumoxide::error::Result;
1174 /// # async fn demo(page: Page) -> Result<()> {
1175 /// let sum:usize = page.evaluate("1 + 2").await?.into_value()?;
1176 /// assert_eq!(sum, 3);
1177 /// # Ok(())
1178 /// # }
1179 /// ```
1180 pub async fn evaluate(&self, evaluate: impl Into<Evaluation>) -> Result<EvaluationResult> {
1181 match evaluate.into() {
1182 Evaluation::Expression(mut expr) => {
1183 if expr.context_id.is_none() {
1184 expr.context_id = self.execution_context().await?;
1185 }
1186 let fallback = expr.eval_as_function_fallback.and_then(|p| {
1187 if p {
1188 Some(expr.clone())
1189 } else {
1190 None
1191 }
1192 });
1193 let res = self.evaluate_expression(expr).await?;
1194
1195 if res.object().r#type == RemoteObjectType::Function {
1196 // expression was actually a function
1197 if let Some(fallback) = fallback {
1198 return self.evaluate_function(fallback).await;
1199 }
1200 }
1201 Ok(res)
1202 }
1203 Evaluation::Function(fun) => Ok(self.evaluate_function(fun).await?),
1204 }
1205 }
1206
1207 /// Eexecutes a function withinthe page's context and returns the result.
1208 ///
1209 /// # Example Evaluate a promise
1210 /// This will wait until the promise resolves and then returns the result.
1211 /// ```no_run
1212 /// # use chromiumoxide::page::Page;
1213 /// # use chromiumoxide::error::Result;
1214 /// # async fn demo(page: Page) -> Result<()> {
1215 /// let sum:usize = page.evaluate_function("() => Promise.resolve(1 + 2)").await?.into_value()?;
1216 /// assert_eq!(sum, 3);
1217 /// # Ok(())
1218 /// # }
1219 /// ```
1220 ///
1221 /// # Example Evaluate an async function
1222 /// ```no_run
1223 /// # use chromiumoxide::page::Page;
1224 /// # use chromiumoxide::error::Result;
1225 /// # async fn demo(page: Page) -> Result<()> {
1226 /// let val:usize = page.evaluate_function("async function() {return 42;}").await?.into_value()?;
1227 /// assert_eq!(val, 42);
1228 /// # Ok(())
1229 /// # }
1230 /// ```
1231 /// # Example Construct a function call
1232 ///
1233 /// ```no_run
1234 /// # use chromiumoxide::page::Page;
1235 /// # use chromiumoxide::error::Result;
1236 /// # use chromiumoxide_cdp::cdp::js_protocol::runtime::{CallFunctionOnParams, CallArgument};
1237 /// # async fn demo(page: Page) -> Result<()> {
1238 /// let call = CallFunctionOnParams::builder()
1239 /// .function_declaration(
1240 /// "(a,b) => { return a + b;}"
1241 /// )
1242 /// .argument(
1243 /// CallArgument::builder()
1244 /// .value(serde_json::json!(1))
1245 /// .build(),
1246 /// )
1247 /// .argument(
1248 /// CallArgument::builder()
1249 /// .value(serde_json::json!(2))
1250 /// .build(),
1251 /// )
1252 /// .build()
1253 /// .unwrap();
1254 /// let sum:usize = page.evaluate_function(call).await?.into_value()?;
1255 /// assert_eq!(sum, 3);
1256 /// # Ok(())
1257 /// # }
1258 /// ```
1259 pub async fn evaluate_function(
1260 &self,
1261 evaluate: impl Into<CallFunctionOnParams>,
1262 ) -> Result<EvaluationResult> {
1263 self.inner.evaluate_function(evaluate).await
1264 }
1265
1266 /// Returns the default execution context identifier of this page that
1267 /// represents the context for JavaScript execution.
1268 pub async fn execution_context(&self) -> Result<Option<ExecutionContextId>> {
1269 self.inner.execution_context().await
1270 }
1271
1272 /// Returns the secondary execution context identifier of this page that
1273 /// represents the context for JavaScript execution for manipulating the
1274 /// DOM.
1275 ///
1276 /// See `Page::set_contents`
1277 pub async fn secondary_execution_context(&self) -> Result<Option<ExecutionContextId>> {
1278 self.inner.secondary_execution_context().await
1279 }
1280
1281 pub async fn frame_execution_context(
1282 &self,
1283 frame_id: FrameId,
1284 ) -> Result<Option<ExecutionContextId>> {
1285 self.inner.frame_execution_context(frame_id).await
1286 }
1287
1288 pub async fn frame_secondary_execution_context(
1289 &self,
1290 frame_id: FrameId,
1291 ) -> Result<Option<ExecutionContextId>> {
1292 self.inner.frame_secondary_execution_context(frame_id).await
1293 }
1294
1295 /// Evaluates given script in every frame upon creation (before loading
1296 /// frame's scripts)
1297 pub async fn evaluate_on_new_document(
1298 &self,
1299 script: impl Into<AddScriptToEvaluateOnNewDocumentParams>,
1300 ) -> Result<ScriptIdentifier> {
1301 Ok(self.execute(script.into()).await?.result.identifier)
1302 }
1303
1304 /// Set the content of the frame.
1305 ///
1306 /// # Example
1307 /// ```no_run
1308 /// # use chromiumoxide::page::Page;
1309 /// # use chromiumoxide::error::Result;
1310 /// # async fn demo(page: Page) -> Result<()> {
1311 /// page.set_content("<body>
1312 /// <h1>This was set via chromiumoxide</h1>
1313 /// </body>").await?;
1314 /// # Ok(())
1315 /// # }
1316 /// ```
1317 pub async fn set_content(&self, html: impl AsRef<str>) -> Result<&Self> {
1318 let mut call = CallFunctionOnParams::builder()
1319 .function_declaration(
1320 "(html) => {
1321 document.open();
1322 document.write(html);
1323 document.close();
1324 }",
1325 )
1326 .argument(
1327 CallArgument::builder()
1328 .value(serde_json::json!(html.as_ref()))
1329 .build(),
1330 )
1331 .build()
1332 .unwrap();
1333
1334 call.execution_context_id = self
1335 .inner
1336 .execution_context_for_world(None, DOMWorldKind::Secondary)
1337 .await?;
1338
1339 self.evaluate_function(call).await?;
1340 // relying that document.open() will reset frame lifecycle with "init"
1341 // lifecycle event. @see https://crrev.com/608658
1342 self.wait_for_navigation().await
1343 }
1344
1345 /// Returns the HTML content of the page.
1346 pub async fn content(&self) -> Result<String> {
1347 Ok(self.evaluate(OUTER_HTML).await?.into_value()?)
1348 }
1349
1350 #[cfg(feature = "bytes")]
1351 /// Returns the HTML content of the page
1352 pub async fn content_bytes(&self) -> Result<Vec<u8>> {
1353 Ok(self.evaluate(OUTER_HTML).await?.into_bytes()?)
1354 }
1355
1356 #[cfg(feature = "bytes")]
1357 /// Returns the full serialized content of the page (HTML or XML)
1358 pub async fn content_bytes_xml(&self) -> Result<Vec<u8>> {
1359 Ok(self.evaluate(FULL_XML_SERIALIZER_JS).await?.into_bytes()?)
1360 }
1361
1362 #[cfg(feature = "bytes")]
1363 /// Returns the HTML outer html of the page
1364 pub async fn outer_html_bytes(&self) -> Result<Vec<u8>> {
1365 Ok(self.outer_html().await?.into())
1366 }
1367
1368 /// Returns source for the script with given id.
1369 ///
1370 /// Debugger must be enabled.
1371 pub async fn get_script_source(&self, script_id: impl Into<String>) -> Result<String> {
1372 Ok(self
1373 .execute(GetScriptSourceParams::new(ScriptId::from(script_id.into())))
1374 .await?
1375 .result
1376 .script_source)
1377 }
1378}
1379
1380impl From<Arc<PageInner>> for Page {
1381 fn from(inner: Arc<PageInner>) -> Self {
1382 Self { inner }
1383 }
1384}
1385
1386pub(crate) fn validate_cookie_url(url: &str) -> Result<()> {
1387 if url.starts_with("data:") {
1388 Err(CdpError::msg("Data URL page can not have cookie"))
1389 } else if url == "about:blank" {
1390 Err(CdpError::msg("Blank page can not have cookie"))
1391 } else {
1392 Ok(())
1393 }
1394}
1395
1396/// Page screenshot parameters with extra options.
1397#[derive(Debug, Default)]
1398pub struct ScreenshotParams {
1399 /// Chrome DevTools Protocol screenshot options.
1400 pub cdp_params: CaptureScreenshotParams,
1401 /// Take full page screenshot.
1402 pub full_page: Option<bool>,
1403 /// Make the background transparent (png only).
1404 pub omit_background: Option<bool>,
1405}
1406
1407impl ScreenshotParams {
1408 pub fn builder() -> ScreenshotParamsBuilder {
1409 Default::default()
1410 }
1411
1412 pub(crate) fn full_page(&self) -> bool {
1413 self.full_page.unwrap_or(false)
1414 }
1415
1416 pub(crate) fn omit_background(&self) -> bool {
1417 self.omit_background.unwrap_or(false)
1418 && self
1419 .cdp_params
1420 .format
1421 .as_ref()
1422 .map_or(true, |f| f == &CaptureScreenshotFormat::Png)
1423 }
1424}
1425
1426/// Page screenshot parameters builder with extra options.
1427#[derive(Debug, Default)]
1428pub struct ScreenshotParamsBuilder {
1429 cdp_params: CaptureScreenshotParams,
1430 full_page: Option<bool>,
1431 omit_background: Option<bool>,
1432}
1433
1434impl ScreenshotParamsBuilder {
1435 /// Image compression format (defaults to png).
1436 pub fn format(mut self, format: impl Into<CaptureScreenshotFormat>) -> Self {
1437 self.cdp_params.format = Some(format.into());
1438 self
1439 }
1440
1441 /// Compression quality from range [0..100] (jpeg only).
1442 pub fn quality(mut self, quality: impl Into<i64>) -> Self {
1443 self.cdp_params.quality = Some(quality.into());
1444 self
1445 }
1446
1447 /// Capture the screenshot of a given region only.
1448 pub fn clip(mut self, clip: impl Into<Viewport>) -> Self {
1449 self.cdp_params.clip = Some(clip.into());
1450 self
1451 }
1452
1453 /// Capture the screenshot from the surface, rather than the view (defaults to true).
1454 pub fn from_surface(mut self, from_surface: impl Into<bool>) -> Self {
1455 self.cdp_params.from_surface = Some(from_surface.into());
1456 self
1457 }
1458
1459 /// Capture the screenshot beyond the viewport (defaults to false).
1460 pub fn capture_beyond_viewport(mut self, capture_beyond_viewport: impl Into<bool>) -> Self {
1461 self.cdp_params.capture_beyond_viewport = Some(capture_beyond_viewport.into());
1462 self
1463 }
1464
1465 /// Full page screen capture.
1466 pub fn full_page(mut self, full_page: impl Into<bool>) -> Self {
1467 self.full_page = Some(full_page.into());
1468 self
1469 }
1470
1471 /// Make the background transparent (png only)
1472 pub fn omit_background(mut self, omit_background: impl Into<bool>) -> Self {
1473 self.omit_background = Some(omit_background.into());
1474 self
1475 }
1476
1477 pub fn build(self) -> ScreenshotParams {
1478 ScreenshotParams {
1479 cdp_params: self.cdp_params,
1480 full_page: self.full_page,
1481 omit_background: self.omit_background,
1482 }
1483 }
1484}
1485
1486impl From<CaptureScreenshotParams> for ScreenshotParams {
1487 fn from(cdp_params: CaptureScreenshotParams) -> Self {
1488 Self {
1489 cdp_params,
1490 ..Default::default()
1491 }
1492 }
1493}
1494
1495#[derive(Debug, Clone, Copy, Default)]
1496pub enum MediaTypeParams {
1497 /// Default CSS media type behavior for page and print
1498 #[default]
1499 Null,
1500 /// Force screen CSS media type for page and print
1501 Screen,
1502 /// Force print CSS media type for page and print
1503 Print,
1504}
1505impl From<MediaTypeParams> for String {
1506 fn from(media_type: MediaTypeParams) -> Self {
1507 match media_type {
1508 MediaTypeParams::Null => "null".to_string(),
1509 MediaTypeParams::Screen => "screen".to_string(),
1510 MediaTypeParams::Print => "print".to_string(),
1511 }
1512 }
1513}