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