Skip to main content

millipede_browser/
page.rs

1//! Provider-erased browser page operations and configuration.
2
3use 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/// Stable process-local identifier for a pooled browser page.
20#[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/// Browser lifecycle event awaited after navigation.
37///
38/// Providers map these events best-effort. A provider with weaker protocol capabilities may use
39/// the nearest available event.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum WaitUntil {
43    /// Wait until the initial HTML is parsed without waiting for subresources.
44    DomContentLoaded,
45    /// Wait until the document and dependent resources report loaded.
46    Load,
47}
48
49/// Options controlling a page navigation.
50#[derive(Debug, Clone)]
51#[non_exhaustive]
52#[must_use = "navigation options do nothing unless passed to goto"]
53pub struct GotoOptions {
54    /// Maximum duration allowed for navigation.
55    pub timeout: Duration,
56    /// Lifecycle event awaited after navigation.
57    pub wait_until: WaitUntil,
58}
59
60impl GotoOptions {
61    /// Sets the navigation timeout.
62    pub fn with_timeout(mut self, timeout: Duration) -> Self {
63        self.timeout = timeout;
64        self
65    }
66
67    /// Sets the lifecycle event awaited after navigation.
68    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/// Navigation response metadata when the provider can expose it.
84///
85/// Providers are allowed to be lossy and may return no response from
86/// [`BrowserPage::goto`]. Individual fields may also be unavailable.
87#[derive(Debug, Clone, Default)]
88#[non_exhaustive]
89pub struct BrowserResponse {
90    /// Final navigation status code, when available.
91    pub status: Option<http::StatusCode>,
92    /// Final navigation response headers, when available.
93    pub headers: http::HeaderMap,
94    /// Final response URL, including redirects, when available.
95    pub url: Option<url::Url>,
96}
97
98/// Options controlling screenshot capture.
99#[derive(Debug, Clone, Default)]
100#[non_exhaustive]
101#[must_use = "screenshot options do nothing unless passed to BrowserPage::screenshot"]
102pub struct ScreenshotOptions {
103    /// Capture the complete scrollable page instead of only the viewport.
104    pub full_page: bool,
105}
106
107/// Per-page creation context consumed by browser hooks.
108#[derive(Clone, Default)]
109#[non_exhaustive]
110#[must_use = "page options do nothing unless passed to BrowserPool::new_page"]
111pub struct PageOptions {
112    /// Session whose cookies should be synchronized with the page.
113    pub session: Option<Arc<Session>>,
114    /// Headers to install on the page before navigation.
115    pub extra_headers: http::HeaderMap,
116}
117
118impl PageOptions {
119    /// Creates an empty page context.
120    pub fn new() -> Self {
121        Self::default()
122    }
123
124    /// Sets the session associated with the page.
125    pub fn with_session(mut self, session: Arc<Session>) -> Self {
126        self.session = Some(session);
127        self
128    }
129
130    /// Replaces the page's extra request headers.
131    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/// Object-safe browser page surface implemented by concrete providers.
151///
152/// This is the provider-erased page interface from INTERFACE ยง12.2. Providers may adapt weaker
153/// protocols lossily; in particular, [`Self::goto`] may return `None` when response metadata is
154/// unavailable.
155#[async_trait::async_trait]
156pub trait BrowserPage: Send + Sync + 'static {
157    /// Navigates to `url` and returns response metadata when the provider exposes it.
158    async fn goto(
159        &self,
160        url: &url::Url,
161        opts: GotoOptions,
162    ) -> Result<Option<BrowserResponse>, BrowserError>;
163
164    /// Returns the current serialized document HTML.
165    async fn content(&self) -> Result<String, BrowserError>;
166
167    /// Evaluates JavaScript in the page and returns its JSON-compatible value.
168    async fn evaluate_js(&self, script: &str) -> Result<serde_json::Value, BrowserError>;
169
170    /// Evaluates anchor destinations and returns DOM-resolved absolute URLs.
171    ///
172    /// `None` selects `a[href]`. Implementations must read the DOM `a.href` value so relative
173    /// destinations are resolved against the document URL.
174    async fn evaluate_anchors(&self, selector: Option<&str>)
175    -> Result<Vec<url::Url>, BrowserError>;
176
177    /// Returns the page's cookies as Millipede's structured cookie records.
178    async fn cookies(&self) -> Result<Vec<Cookie>, BrowserError>;
179
180    /// Replaces or merges the supplied structured cookies into the page.
181    async fn set_cookies(&self, cookies: &[Cookie]) -> Result<(), BrowserError>;
182
183    /// Installs additional request headers for subsequent page requests.
184    async fn set_extra_headers(&self, headers: &http::HeaderMap) -> Result<(), BrowserError>;
185
186    /// Waits until an element matching `selector` exists or `timeout` elapses.
187    async fn wait_for_selector(
188        &self,
189        selector: &str,
190        timeout: Duration,
191    ) -> Result<(), BrowserError>;
192
193    /// Clicks an element matching `selector`.
194    async fn click(&self, selector: &str) -> Result<(), BrowserError>;
195
196    /// Captures a screenshot and returns its encoded bytes.
197    async fn screenshot(&self, opts: ScreenshotOptions) -> Result<bytes::Bytes, BrowserError>;
198}