use std::collections::HashMap;
use std::time::Duration;
use tracing::{debug, info, instrument};
use crate::error::NavigationError;
use crate::wait::DocumentLoadState;
use super::{DEFAULT_NAVIGATION_TIMEOUT, Page};
#[derive(Debug, Clone)]
pub struct NavigationResponse {
url: String,
frame_id: String,
status: Option<u16>,
headers: Option<HashMap<String, String>>,
}
impl NavigationResponse {
pub(crate) fn new(url: String, frame_id: String) -> Self {
Self {
url,
frame_id,
status: None,
headers: None,
}
}
pub(crate) fn with_response(
url: String,
frame_id: String,
status: u16,
headers: HashMap<String, String>,
) -> Self {
Self {
url,
frame_id,
status: Some(status),
headers: Some(headers),
}
}
pub fn url(&self) -> &str {
&self.url
}
pub fn frame_id(&self) -> &str {
&self.frame_id
}
pub fn status(&self) -> Option<u16> {
self.status
}
pub fn headers(&self) -> Option<&HashMap<String, String>> {
self.headers.as_ref()
}
pub fn ok(&self) -> bool {
self.status.is_none_or(|s| (200..300).contains(&s))
}
}
#[derive(Debug)]
pub struct GotoBuilder<'a> {
page: &'a Page,
url: String,
wait_until: DocumentLoadState,
timeout: Duration,
referer: Option<String>,
}
impl<'a> GotoBuilder<'a> {
pub(crate) fn new(page: &'a Page, url: String) -> Self {
Self {
page,
url,
wait_until: DocumentLoadState::default(),
timeout: DEFAULT_NAVIGATION_TIMEOUT,
referer: None,
}
}
#[must_use]
pub fn wait_until(mut self, state: DocumentLoadState) -> Self {
self.wait_until = state;
self
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn referer(mut self, referer: impl Into<String>) -> Self {
self.referer = Some(referer.into());
self
}
#[instrument(level = "debug", skip(self), fields(url = %self.url, wait_until = ?self.wait_until, timeout_ms = self.timeout.as_millis(), has_referer = self.referer.is_some()))]
pub async fn goto(self) -> Result<NavigationResponse, NavigationError> {
debug!("Executing navigation via GotoBuilder");
self.page
.navigate_internal(
&self.url,
self.wait_until,
self.timeout,
self.referer.as_deref(),
)
.await
}
}
impl Page {
#[instrument(level = "info", skip(self))]
pub async fn go_back(&self) -> Result<Option<NavigationResponse>, NavigationError> {
if self.closed {
return Err(NavigationError::Cancelled);
}
let history: viewpoint_cdp::protocol::page::GetNavigationHistoryResult = self
.connection
.send_command(
"Page.getNavigationHistory",
None::<()>,
Some(&self.session_id),
)
.await?;
if history.current_index <= 0 {
debug!("No previous page in history");
return Ok(None);
}
let previous_entry = &history.entries[history.current_index as usize - 1];
self.connection
.send_command::<_, serde_json::Value>(
"Page.navigateToHistoryEntry",
Some(
viewpoint_cdp::protocol::page::NavigateToHistoryEntryParams {
entry_id: previous_entry.id,
},
),
Some(&self.session_id),
)
.await?;
info!("Navigated back to {}", previous_entry.url);
Ok(Some(NavigationResponse::new(
previous_entry.url.clone(),
self.frame_id.clone(),
)))
}
#[instrument(level = "info", skip(self))]
pub async fn go_forward(&self) -> Result<Option<NavigationResponse>, NavigationError> {
if self.closed {
return Err(NavigationError::Cancelled);
}
let history: viewpoint_cdp::protocol::page::GetNavigationHistoryResult = self
.connection
.send_command(
"Page.getNavigationHistory",
None::<()>,
Some(&self.session_id),
)
.await?;
let next_index = history.current_index as usize + 1;
if next_index >= history.entries.len() {
debug!("No next page in history");
return Ok(None);
}
let next_entry = &history.entries[next_index];
self.connection
.send_command::<_, serde_json::Value>(
"Page.navigateToHistoryEntry",
Some(
viewpoint_cdp::protocol::page::NavigateToHistoryEntryParams {
entry_id: next_entry.id,
},
),
Some(&self.session_id),
)
.await?;
info!("Navigated forward to {}", next_entry.url);
Ok(Some(NavigationResponse::new(
next_entry.url.clone(),
self.frame_id.clone(),
)))
}
#[instrument(level = "info", skip(self))]
pub async fn reload(&self) -> Result<NavigationResponse, NavigationError> {
if self.closed {
return Err(NavigationError::Cancelled);
}
info!("Reloading page");
self.connection
.send_command::<_, serde_json::Value>(
"Page.reload",
Some(viewpoint_cdp::protocol::page::ReloadParams::default()),
Some(&self.session_id),
)
.await?;
let url = self.url().await.unwrap_or_else(|_| String::new());
info!("Page reloaded");
Ok(NavigationResponse::new(url, self.frame_id.clone()))
}
}