use std::{
fmt,
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
time::Duration,
};
use millipede_core::{cookies::Cookie, session::Session};
use crate::BrowserError;
#[allow(dead_code)]
static NEXT_PAGE_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PageId(u64);
impl PageId {
#[allow(dead_code)]
pub(crate) fn next() -> Self {
Self(NEXT_PAGE_ID.fetch_add(1, Ordering::Relaxed))
}
}
impl fmt::Display for PageId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum WaitUntil {
DomContentLoaded,
Load,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
#[must_use = "navigation options do nothing unless passed to goto"]
pub struct GotoOptions {
pub timeout: Duration,
pub wait_until: WaitUntil,
}
impl GotoOptions {
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn with_wait_until(mut self, wait_until: WaitUntil) -> Self {
self.wait_until = wait_until;
self
}
}
impl Default for GotoOptions {
fn default() -> Self {
Self {
timeout: Duration::from_secs(30),
wait_until: WaitUntil::Load,
}
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct BrowserResponse {
pub status: Option<http::StatusCode>,
pub headers: http::HeaderMap,
pub url: Option<url::Url>,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
#[must_use = "screenshot options do nothing unless passed to BrowserPage::screenshot"]
pub struct ScreenshotOptions {
pub full_page: bool,
}
#[derive(Clone, Default)]
#[non_exhaustive]
#[must_use = "page options do nothing unless passed to BrowserPool::new_page"]
pub struct PageOptions {
pub session: Option<Arc<Session>>,
pub extra_headers: http::HeaderMap,
}
impl PageOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_session(mut self, session: Arc<Session>) -> Self {
self.session = Some(session);
self
}
pub fn with_extra_headers(mut self, extra_headers: http::HeaderMap) -> Self {
self.extra_headers = extra_headers;
self
}
}
impl fmt::Debug for PageOptions {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("PageOptions")
.field(
"session_id",
&self.session.as_ref().map(|session| session.id()),
)
.field("extra_headers", &self.extra_headers)
.finish()
}
}
#[async_trait::async_trait]
pub trait BrowserPage: Send + Sync + 'static {
async fn goto(
&self,
url: &url::Url,
opts: GotoOptions,
) -> Result<Option<BrowserResponse>, BrowserError>;
async fn content(&self) -> Result<String, BrowserError>;
async fn evaluate_js(&self, script: &str) -> Result<serde_json::Value, BrowserError>;
async fn evaluate_anchors(&self, selector: Option<&str>)
-> Result<Vec<url::Url>, BrowserError>;
async fn cookies(&self) -> Result<Vec<Cookie>, BrowserError>;
async fn set_cookies(&self, cookies: &[Cookie]) -> Result<(), BrowserError>;
async fn set_extra_headers(&self, headers: &http::HeaderMap) -> Result<(), BrowserError>;
async fn wait_for_selector(
&self,
selector: &str,
timeout: Duration,
) -> Result<(), BrowserError>;
async fn click(&self, selector: &str) -> Result<(), BrowserError>;
async fn screenshot(&self, opts: ScreenshotOptions) -> Result<bytes::Bytes, BrowserError>;
}