mod aria_snapshot;
pub use aria_snapshot::SnapshotOptions;
pub mod binding;
mod ref_resolution;
pub mod clock;
mod clock_script;
pub mod console;
mod constructors;
mod content;
pub mod dialog;
pub mod download;
pub mod emulation;
mod evaluate;
pub mod events;
pub mod file_chooser;
pub mod frame;
pub mod frame_locator;
mod frame_locator_actions;
mod frame_page_methods;
mod input_devices;
pub mod keyboard;
pub mod locator;
mod locator_factory;
pub mod locator_handler;
mod mouse;
mod mouse_drag;
mod navigation;
pub mod page_error;
mod pdf;
pub mod popup;
mod routing_impl;
mod screenshot;
mod screenshot_element;
mod scripts;
mod touchscreen;
pub mod video;
mod video_io;
use std::sync::Arc;
use std::time::Duration;
use tracing::{debug, info, instrument, trace, warn};
use viewpoint_cdp::CdpConnection;
use viewpoint_cdp::protocol::page::{NavigateParams, NavigateResult};
use viewpoint_cdp::protocol::target_domain::CloseTargetParams;
use viewpoint_js::js;
use crate::error::{NavigationError, PageError};
use crate::network::{RouteHandlerRegistry, WebSocketManager};
use crate::wait::{DocumentLoadState, LoadStateWaiter};
pub use clock::{Clock, TimeValue};
pub use console::{ConsoleMessage, ConsoleMessageLocation, ConsoleMessageType, JsArg};
pub use content::{ScriptTagBuilder, ScriptType, SetContentBuilder, StyleTagBuilder};
pub use dialog::Dialog;
pub use download::{Download, DownloadState};
pub use emulation::{EmulateMediaBuilder, MediaType, VisionDeficiency};
pub use evaluate::{JsHandle, Polling, WaitForFunctionBuilder};
pub use events::PageEventManager;
pub use file_chooser::{FileChooser, FilePayload};
pub use frame::Frame;
pub(crate) use frame::ExecutionContextRegistry;
pub use frame_locator::{FrameElementLocator, FrameLocator, FrameRoleLocatorBuilder};
pub use keyboard::Keyboard;
pub use locator::{
AriaCheckedState, AriaRole, AriaSnapshot, BoundingBox, BoxModel, ElementHandle, FilterBuilder,
Locator, LocatorOptions, RoleLocatorBuilder, Selector, TapBuilder, TextOptions,
};
pub use locator_handler::{LocatorHandlerHandle, LocatorHandlerManager, LocatorHandlerOptions};
pub use mouse::Mouse;
pub use mouse_drag::DragAndDropBuilder;
pub use navigation::{GotoBuilder, NavigationResponse};
pub use page_error::{PageError as PageErrorInfo, WebError};
pub use pdf::{Margins, PaperFormat, PdfBuilder};
pub use screenshot::{Animations, ClipRegion, ScreenshotBuilder, ScreenshotFormat};
pub use touchscreen::Touchscreen;
pub use video::{Video, VideoOptions};
pub use viewpoint_cdp::protocol::DialogType;
pub use viewpoint_cdp::protocol::emulation::ViewportSize;
pub use viewpoint_cdp::protocol::input::MouseButton;
const DEFAULT_NAVIGATION_TIMEOUT: Duration = Duration::from_secs(30);
pub const DEFAULT_TEST_ID_ATTRIBUTE: &str = "data-testid";
pub struct Page {
connection: Arc<CdpConnection>,
target_id: String,
session_id: String,
frame_id: String,
closed: bool,
route_registry: Arc<RouteHandlerRegistry>,
keyboard: Keyboard,
mouse: Mouse,
touchscreen: Touchscreen,
event_manager: Arc<PageEventManager>,
locator_handler_manager: Arc<LocatorHandlerManager>,
video_controller: Option<Arc<Video>>,
opener_target_id: Option<String>,
popup_manager: Arc<popup::PopupManager>,
websocket_manager: Arc<WebSocketManager>,
binding_manager: Arc<binding::BindingManager>,
test_id_attribute: String,
context_registry: Arc<ExecutionContextRegistry>,
}
impl std::fmt::Debug for Page {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Page")
.field("target_id", &self.target_id)
.field("session_id", &self.session_id)
.field("frame_id", &self.frame_id)
.field("closed", &self.closed)
.finish_non_exhaustive()
}
}
impl Page {
pub fn goto(&self, url: impl Into<String>) -> GotoBuilder<'_> {
GotoBuilder::new(self, url.into())
}
pub async fn goto_url(&self, url: &str) -> Result<NavigationResponse, NavigationError> {
self.goto(url).goto().await
}
#[instrument(level = "info", skip(self), fields(target_id = %self.target_id, url = %url, wait_until = ?wait_until, timeout_ms = timeout.as_millis()))]
pub(crate) async fn navigate_internal(
&self,
url: &str,
wait_until: DocumentLoadState,
timeout: Duration,
referer: Option<&str>,
) -> Result<NavigationResponse, NavigationError> {
if self.closed {
warn!("Attempted navigation on closed page");
return Err(NavigationError::Cancelled);
}
info!("Starting navigation");
let event_rx = self.connection.subscribe_events();
let mut waiter =
LoadStateWaiter::new(event_rx, self.session_id.clone(), self.frame_id.clone());
trace!("Created load state waiter");
debug!("Sending Page.navigate command");
let result: NavigateResult = self
.connection
.send_command(
"Page.navigate",
Some(NavigateParams {
url: url.to_string(),
referrer: referer.map(ToString::to_string),
transition_type: None,
frame_id: None,
}),
Some(&self.session_id),
)
.await?;
debug!(frame_id = %result.frame_id, loader_id = ?result.loader_id, "Page.navigate completed");
if let Some(ref error_text) = result.error_text {
let is_http_error = error_text == "net::ERR_HTTP_RESPONSE_CODE_FAILURE"
|| error_text == "net::ERR_INVALID_AUTH_CREDENTIALS";
if !is_http_error {
warn!(error = %error_text, "Navigation failed with error");
return Err(NavigationError::NetworkError(error_text.clone()));
}
debug!(error = %error_text, "HTTP error response - continuing to capture status");
}
trace!("Setting commit received");
waiter.set_commit_received().await;
debug!(wait_until = ?wait_until, "Waiting for load state");
waiter
.wait_for_load_state_with_timeout(wait_until, timeout)
.await?;
let response_data = waiter.response_data().await;
info!(frame_id = %result.frame_id, "Navigation completed successfully");
let final_url = response_data.url.unwrap_or_else(|| url.to_string());
if let Some(status) = response_data.status {
Ok(NavigationResponse::with_response(
final_url,
result.frame_id,
status,
response_data.headers,
))
} else {
Ok(NavigationResponse::new(final_url, result.frame_id))
}
}
#[instrument(level = "info", skip(self), fields(target_id = %self.target_id))]
pub async fn close(&mut self) -> Result<(), PageError> {
if self.closed {
debug!("Page already closed");
return Ok(());
}
info!("Closing page");
self.route_registry.unroute_all().await;
debug!("Route handlers cleaned up");
self.connection
.send_command::<_, serde_json::Value>(
"Target.closeTarget",
Some(CloseTargetParams {
target_id: self.target_id.clone(),
}),
None,
)
.await?;
self.closed = true;
info!("Page closed");
Ok(())
}
pub fn target_id(&self) -> &str {
&self.target_id
}
pub fn session_id(&self) -> &str {
&self.session_id
}
pub fn frame_id(&self) -> &str {
&self.frame_id
}
pub fn is_closed(&self) -> bool {
self.closed
}
pub fn connection(&self) -> &Arc<CdpConnection> {
&self.connection
}
pub fn screenshot(&self) -> screenshot::ScreenshotBuilder<'_> {
screenshot::ScreenshotBuilder::new(self)
}
pub fn pdf(&self) -> pdf::PdfBuilder<'_> {
pdf::PdfBuilder::new(self)
}
pub async fn url(&self) -> Result<String, PageError> {
if self.closed {
return Err(PageError::Closed);
}
let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
.connection
.send_command(
"Runtime.evaluate",
Some(viewpoint_cdp::protocol::runtime::EvaluateParams {
expression: js! { window.location.href }.to_string(),
object_group: None,
include_command_line_api: None,
silent: Some(true),
context_id: None,
return_by_value: Some(true),
await_promise: Some(false),
}),
Some(&self.session_id),
)
.await?;
result
.result
.value
.and_then(|v| v.as_str().map(std::string::ToString::to_string))
.ok_or_else(|| PageError::EvaluationFailed("Failed to get URL".to_string()))
}
pub async fn title(&self) -> Result<String, PageError> {
if self.closed {
return Err(PageError::Closed);
}
let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
.connection
.send_command(
"Runtime.evaluate",
Some(viewpoint_cdp::protocol::runtime::EvaluateParams {
expression: "document.title".to_string(),
object_group: None,
include_command_line_api: None,
silent: Some(true),
context_id: None,
return_by_value: Some(true),
await_promise: Some(false),
}),
Some(&self.session_id),
)
.await?;
result
.result
.value
.and_then(|v| v.as_str().map(std::string::ToString::to_string))
.ok_or_else(|| PageError::EvaluationFailed("Failed to get title".to_string()))
}
}