1mod aria_snapshot;
4pub mod binding;
5pub mod clock;
6mod clock_script;
7pub mod console;
8mod constructors;
9mod content;
10pub mod dialog;
11pub mod download;
12pub mod emulation;
13mod evaluate;
14pub mod events;
15pub mod file_chooser;
16pub mod frame;
17pub mod frame_locator;
18mod frame_locator_actions;
19mod frame_page_methods;
20mod input_devices;
21pub mod keyboard;
22pub mod locator;
23mod locator_factory;
24pub mod locator_handler;
25mod mouse;
26mod mouse_drag;
27mod navigation;
28pub mod page_error;
29mod pdf;
30pub mod popup;
31mod routing_impl;
32mod screenshot;
33mod screenshot_element;
34mod scripts;
35mod touchscreen;
36pub mod video;
37mod video_io;
38
39use std::sync::Arc;
40use std::time::Duration;
41
42use tracing::{debug, info, instrument, trace, warn};
43use viewpoint_cdp::CdpConnection;
44use viewpoint_cdp::protocol::page::{NavigateParams, NavigateResult};
45use viewpoint_cdp::protocol::target_domain::CloseTargetParams;
46use viewpoint_js::js;
47
48use crate::error::{NavigationError, PageError};
49use crate::network::{RouteHandlerRegistry, WebSocketManager};
50use crate::wait::{DocumentLoadState, LoadStateWaiter};
51
52pub use clock::{Clock, TimeValue};
53pub use console::{ConsoleMessage, ConsoleMessageLocation, ConsoleMessageType, JsArg};
54pub use content::{ScriptTagBuilder, ScriptType, SetContentBuilder, StyleTagBuilder};
55pub use dialog::Dialog;
56pub use download::{Download, DownloadState};
57pub use emulation::{EmulateMediaBuilder, MediaType, VisionDeficiency};
58pub use evaluate::{JsHandle, Polling, WaitForFunctionBuilder};
59pub use events::PageEventManager;
60pub use file_chooser::{FileChooser, FilePayload};
61pub use frame::Frame;
62pub use frame_locator::{FrameElementLocator, FrameLocator, FrameRoleLocatorBuilder};
63pub use keyboard::Keyboard;
64pub use locator::{
65 AriaCheckedState, AriaRole, AriaSnapshot, BoundingBox, BoxModel, ElementHandle, FilterBuilder,
66 Locator, LocatorOptions, RoleLocatorBuilder, Selector, TapBuilder, TextOptions,
67};
68pub use locator_handler::{LocatorHandlerHandle, LocatorHandlerManager, LocatorHandlerOptions};
69pub use mouse::Mouse;
70pub use mouse_drag::DragAndDropBuilder;
71pub use navigation::{GotoBuilder, NavigationResponse};
72pub use page_error::{PageError as PageErrorInfo, WebError};
73pub use pdf::{Margins, PaperFormat, PdfBuilder};
74pub use screenshot::{Animations, ClipRegion, ScreenshotBuilder, ScreenshotFormat};
75pub use touchscreen::Touchscreen;
76pub use video::{Video, VideoOptions};
77pub use viewpoint_cdp::protocol::DialogType;
78pub use viewpoint_cdp::protocol::emulation::ViewportSize;
79pub use viewpoint_cdp::protocol::input::MouseButton;
80
81const DEFAULT_NAVIGATION_TIMEOUT: Duration = Duration::from_secs(30);
83
84pub const DEFAULT_TEST_ID_ATTRIBUTE: &str = "data-testid";
86
87pub struct Page {
89 connection: Arc<CdpConnection>,
91 target_id: String,
93 session_id: String,
95 frame_id: String,
97 closed: bool,
99 route_registry: Arc<RouteHandlerRegistry>,
101 keyboard: Keyboard,
103 mouse: Mouse,
105 touchscreen: Touchscreen,
107 event_manager: Arc<PageEventManager>,
109 locator_handler_manager: Arc<LocatorHandlerManager>,
111 video_controller: Option<Arc<Video>>,
113 opener_target_id: Option<String>,
115 popup_manager: Arc<popup::PopupManager>,
117 websocket_manager: Arc<WebSocketManager>,
119 binding_manager: Arc<binding::BindingManager>,
121 test_id_attribute: String,
123}
124
125impl std::fmt::Debug for Page {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 f.debug_struct("Page")
129 .field("target_id", &self.target_id)
130 .field("session_id", &self.session_id)
131 .field("frame_id", &self.frame_id)
132 .field("closed", &self.closed)
133 .finish_non_exhaustive()
134 }
135}
136
137impl Page {
138 pub fn goto(&self, url: impl Into<String>) -> GotoBuilder<'_> {
163 GotoBuilder::new(self, url.into())
164 }
165
166 pub async fn goto_url(&self, url: &str) -> Result<NavigationResponse, NavigationError> {
177 self.goto(url).goto().await
178 }
179
180 #[instrument(level = "info", skip(self), fields(target_id = %self.target_id, url = %url, wait_until = ?wait_until, timeout_ms = timeout.as_millis()))]
182 pub(crate) async fn navigate_internal(
183 &self,
184 url: &str,
185 wait_until: DocumentLoadState,
186 timeout: Duration,
187 referer: Option<&str>,
188 ) -> Result<NavigationResponse, NavigationError> {
189 if self.closed {
190 warn!("Attempted navigation on closed page");
191 return Err(NavigationError::Cancelled);
192 }
193
194 info!("Starting navigation");
195
196 let event_rx = self.connection.subscribe_events();
198 let mut waiter =
199 LoadStateWaiter::new(event_rx, self.session_id.clone(), self.frame_id.clone());
200 trace!("Created load state waiter");
201
202 debug!("Sending Page.navigate command");
204 let result: NavigateResult = self
205 .connection
206 .send_command(
207 "Page.navigate",
208 Some(NavigateParams {
209 url: url.to_string(),
210 referrer: referer.map(ToString::to_string),
211 transition_type: None,
212 frame_id: None,
213 }),
214 Some(&self.session_id),
215 )
216 .await?;
217
218 debug!(frame_id = %result.frame_id, loader_id = ?result.loader_id, "Page.navigate completed");
219
220 if let Some(ref error_text) = result.error_text {
226 let is_http_error = error_text == "net::ERR_HTTP_RESPONSE_CODE_FAILURE"
227 || error_text == "net::ERR_INVALID_AUTH_CREDENTIALS";
228
229 if !is_http_error {
230 warn!(error = %error_text, "Navigation failed with error");
231 return Err(NavigationError::NetworkError(error_text.clone()));
232 }
233 debug!(error = %error_text, "HTTP error response - continuing to capture status");
234 }
235
236 trace!("Setting commit received");
238 waiter.set_commit_received().await;
239
240 debug!(wait_until = ?wait_until, "Waiting for load state");
242 waiter
243 .wait_for_load_state_with_timeout(wait_until, timeout)
244 .await?;
245
246 let response_data = waiter.response_data().await;
248
249 info!(frame_id = %result.frame_id, "Navigation completed successfully");
250
251 let final_url = response_data.url.unwrap_or_else(|| url.to_string());
253
254 if let Some(status) = response_data.status {
256 Ok(NavigationResponse::with_response(
257 final_url,
258 result.frame_id,
259 status,
260 response_data.headers,
261 ))
262 } else {
263 Ok(NavigationResponse::new(final_url, result.frame_id))
264 }
265 }
266
267 #[instrument(level = "info", skip(self), fields(target_id = %self.target_id))]
273 pub async fn close(&mut self) -> Result<(), PageError> {
274 if self.closed {
275 debug!("Page already closed");
276 return Ok(());
277 }
278
279 info!("Closing page");
280
281 self.route_registry.unroute_all().await;
283 debug!("Route handlers cleaned up");
284
285 self.connection
286 .send_command::<_, serde_json::Value>(
287 "Target.closeTarget",
288 Some(CloseTargetParams {
289 target_id: self.target_id.clone(),
290 }),
291 None,
292 )
293 .await?;
294
295 self.closed = true;
296 info!("Page closed");
297 Ok(())
298 }
299
300 pub fn target_id(&self) -> &str {
302 &self.target_id
303 }
304
305 pub fn session_id(&self) -> &str {
307 &self.session_id
308 }
309
310 pub fn frame_id(&self) -> &str {
312 &self.frame_id
313 }
314
315 pub fn is_closed(&self) -> bool {
317 self.closed
318 }
319
320 pub fn connection(&self) -> &Arc<CdpConnection> {
322 &self.connection
323 }
324
325 pub fn screenshot(&self) -> screenshot::ScreenshotBuilder<'_> {
355 screenshot::ScreenshotBuilder::new(self)
356 }
357
358 pub fn pdf(&self) -> pdf::PdfBuilder<'_> {
387 pdf::PdfBuilder::new(self)
388 }
389
390 pub async fn url(&self) -> Result<String, PageError> {
396 if self.closed {
397 return Err(PageError::Closed);
398 }
399
400 let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
401 .connection
402 .send_command(
403 "Runtime.evaluate",
404 Some(viewpoint_cdp::protocol::runtime::EvaluateParams {
405 expression: js! { window.location.href }.to_string(),
406 object_group: None,
407 include_command_line_api: None,
408 silent: Some(true),
409 context_id: None,
410 return_by_value: Some(true),
411 await_promise: Some(false),
412 }),
413 Some(&self.session_id),
414 )
415 .await?;
416
417 result
418 .result
419 .value
420 .and_then(|v| v.as_str().map(std::string::ToString::to_string))
421 .ok_or_else(|| PageError::EvaluationFailed("Failed to get URL".to_string()))
422 }
423
424 pub async fn title(&self) -> Result<String, PageError> {
430 if self.closed {
431 return Err(PageError::Closed);
432 }
433
434 let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
435 .connection
436 .send_command(
437 "Runtime.evaluate",
438 Some(viewpoint_cdp::protocol::runtime::EvaluateParams {
439 expression: "document.title".to_string(),
440 object_group: None,
441 include_command_line_api: None,
442 silent: Some(true),
443 context_id: None,
444 return_by_value: Some(true),
445 await_promise: Some(false),
446 }),
447 Some(&self.session_id),
448 )
449 .await?;
450
451 result
452 .result
453 .value
454 .and_then(|v| v.as_str().map(std::string::ToString::to_string))
455 .ok_or_else(|| PageError::EvaluationFailed("Failed to get title".to_string()))
456 }
457}
458
459