1pub mod binding;
4pub mod clock;
5mod clock_script;
6pub mod console;
7mod constructors;
8mod content;
9pub mod dialog;
10pub mod download;
11pub mod emulation;
12mod evaluate;
13pub mod events;
14pub mod file_chooser;
15pub mod frame;
16pub mod frame_locator;
17mod frame_locator_actions;
18mod frame_page_methods;
19mod input_devices;
20pub mod keyboard;
21pub mod locator;
22mod locator_factory;
23pub mod locator_handler;
24mod mouse;
25mod mouse_drag;
26mod navigation;
27pub mod page_error;
28mod pdf;
29mod routing_impl;
30pub mod popup;
31mod screenshot;
32mod screenshot_element;
33mod scripts;
34mod touchscreen;
35pub mod video;
36mod video_io;
37
38
39use std::sync::Arc;
40use std::time::Duration;
41
42use viewpoint_cdp::protocol::page::{NavigateParams, NavigateResult};
43use viewpoint_cdp::protocol::target_domain::CloseTargetParams;
44use viewpoint_cdp::CdpConnection;
45use viewpoint_js::js;
46use tracing::{debug, info, instrument, trace, warn};
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::{AriaCheckedState, AriaRole, AriaSnapshot, BoundingBox, BoxModel, ElementHandle, FilterBuilder, Locator, LocatorOptions, RoleLocatorBuilder, Selector, TapBuilder, TextOptions};
65pub use locator_handler::{LocatorHandlerHandle, LocatorHandlerManager, LocatorHandlerOptions};
66pub use mouse::Mouse;
67pub use mouse_drag::DragAndDropBuilder;
68pub use viewpoint_cdp::protocol::input::MouseButton;
69pub use navigation::{GotoBuilder, NavigationResponse};
70pub use page_error::{PageError as PageErrorInfo, WebError};
71pub use pdf::{Margins, PaperFormat, PdfBuilder};
72pub use screenshot::{Animations, ClipRegion, ScreenshotBuilder, ScreenshotFormat};
73pub use touchscreen::Touchscreen;
74pub use video::{Video, VideoOptions};
75pub use viewpoint_cdp::protocol::emulation::ViewportSize;
76pub use viewpoint_cdp::protocol::DialogType;
77
78const DEFAULT_NAVIGATION_TIMEOUT: Duration = Duration::from_secs(30);
80
81pub const DEFAULT_TEST_ID_ATTRIBUTE: &str = "data-testid";
83
84pub struct Page {
86 connection: Arc<CdpConnection>,
88 target_id: String,
90 session_id: String,
92 frame_id: String,
94 closed: bool,
96 route_registry: Arc<RouteHandlerRegistry>,
98 keyboard: Keyboard,
100 mouse: Mouse,
102 touchscreen: Touchscreen,
104 event_manager: Arc<PageEventManager>,
106 locator_handler_manager: Arc<LocatorHandlerManager>,
108 video_controller: Option<Arc<Video>>,
110 opener_target_id: Option<String>,
112 popup_manager: Arc<popup::PopupManager>,
114 websocket_manager: Arc<WebSocketManager>,
116 binding_manager: Arc<binding::BindingManager>,
118 test_id_attribute: String,
120}
121
122impl std::fmt::Debug for Page {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 f.debug_struct("Page")
126 .field("target_id", &self.target_id)
127 .field("session_id", &self.session_id)
128 .field("frame_id", &self.frame_id)
129 .field("closed", &self.closed)
130 .finish_non_exhaustive()
131 }
132}
133
134impl Page {
135 pub fn goto(&self, url: impl Into<String>) -> GotoBuilder<'_> {
160 GotoBuilder::new(self, url.into())
161 }
162
163 pub async fn goto_url(&self, url: &str) -> Result<NavigationResponse, NavigationError> {
174 self.goto(url).goto().await
175 }
176
177 #[instrument(level = "info", skip(self), fields(target_id = %self.target_id, url = %url, wait_until = ?wait_until, timeout_ms = timeout.as_millis()))]
179 pub(crate) async fn navigate_internal(
180 &self,
181 url: &str,
182 wait_until: DocumentLoadState,
183 timeout: Duration,
184 referer: Option<&str>,
185 ) -> Result<NavigationResponse, NavigationError> {
186 if self.closed {
187 warn!("Attempted navigation on closed page");
188 return Err(NavigationError::Cancelled);
189 }
190
191 info!("Starting navigation");
192
193 let event_rx = self.connection.subscribe_events();
195 let mut waiter = LoadStateWaiter::new(
196 event_rx,
197 self.session_id.clone(),
198 self.frame_id.clone(),
199 );
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
460