millipede_browser/
page.rs1use std::{
4 fmt,
5 sync::{
6 Arc,
7 atomic::{AtomicU64, Ordering},
8 },
9 time::Duration,
10};
11
12use millipede_core::{cookies::Cookie, session::Session};
13
14use crate::BrowserError;
15
16#[allow(dead_code)]
17static NEXT_PAGE_ID: AtomicU64 = AtomicU64::new(1);
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct PageId(u64);
22
23impl PageId {
24 #[allow(dead_code)]
25 pub(crate) fn next() -> Self {
26 Self(NEXT_PAGE_ID.fetch_add(1, Ordering::Relaxed))
27 }
28}
29
30impl fmt::Display for PageId {
31 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32 self.0.fmt(formatter)
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum WaitUntil {
43 DomContentLoaded,
45 Load,
47}
48
49#[derive(Debug, Clone)]
51#[non_exhaustive]
52#[must_use = "navigation options do nothing unless passed to goto"]
53pub struct GotoOptions {
54 pub timeout: Duration,
56 pub wait_until: WaitUntil,
58}
59
60impl GotoOptions {
61 pub fn with_timeout(mut self, timeout: Duration) -> Self {
63 self.timeout = timeout;
64 self
65 }
66
67 pub fn with_wait_until(mut self, wait_until: WaitUntil) -> Self {
69 self.wait_until = wait_until;
70 self
71 }
72}
73
74impl Default for GotoOptions {
75 fn default() -> Self {
76 Self {
77 timeout: Duration::from_secs(30),
78 wait_until: WaitUntil::Load,
79 }
80 }
81}
82
83#[derive(Debug, Clone, Default)]
88#[non_exhaustive]
89pub struct BrowserResponse {
90 pub status: Option<http::StatusCode>,
92 pub headers: http::HeaderMap,
94 pub url: Option<url::Url>,
96}
97
98#[derive(Debug, Clone, Default)]
100#[non_exhaustive]
101#[must_use = "screenshot options do nothing unless passed to BrowserPage::screenshot"]
102pub struct ScreenshotOptions {
103 pub full_page: bool,
105}
106
107#[derive(Clone, Default)]
109#[non_exhaustive]
110#[must_use = "page options do nothing unless passed to BrowserPool::new_page"]
111pub struct PageOptions {
112 pub session: Option<Arc<Session>>,
114 pub extra_headers: http::HeaderMap,
116}
117
118impl PageOptions {
119 pub fn new() -> Self {
121 Self::default()
122 }
123
124 pub fn with_session(mut self, session: Arc<Session>) -> Self {
126 self.session = Some(session);
127 self
128 }
129
130 pub fn with_extra_headers(mut self, extra_headers: http::HeaderMap) -> Self {
132 self.extra_headers = extra_headers;
133 self
134 }
135}
136
137impl fmt::Debug for PageOptions {
138 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
139 formatter
140 .debug_struct("PageOptions")
141 .field(
142 "session_id",
143 &self.session.as_ref().map(|session| session.id()),
144 )
145 .field("extra_headers", &self.extra_headers)
146 .finish()
147 }
148}
149
150#[async_trait::async_trait]
156pub trait BrowserPage: Send + Sync + 'static {
157 async fn goto(
159 &self,
160 url: &url::Url,
161 opts: GotoOptions,
162 ) -> Result<Option<BrowserResponse>, BrowserError>;
163
164 async fn content(&self) -> Result<String, BrowserError>;
166
167 async fn evaluate_js(&self, script: &str) -> Result<serde_json::Value, BrowserError>;
169
170 async fn evaluate_anchors(&self, selector: Option<&str>)
175 -> Result<Vec<url::Url>, BrowserError>;
176
177 async fn cookies(&self) -> Result<Vec<Cookie>, BrowserError>;
179
180 async fn set_cookies(&self, cookies: &[Cookie]) -> Result<(), BrowserError>;
182
183 async fn set_extra_headers(&self, headers: &http::HeaderMap) -> Result<(), BrowserError>;
185
186 async fn wait_for_selector(
188 &self,
189 selector: &str,
190 timeout: Duration,
191 ) -> Result<(), BrowserError>;
192
193 async fn click(&self, selector: &str) -> Result<(), BrowserError>;
195
196 async fn screenshot(&self, opts: ScreenshotOptions) -> Result<bytes::Bytes, BrowserError>;
198}