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(crate) use frame::ExecutionContextRegistry;
64pub use frame_locator::{FrameElementLocator, FrameLocator, FrameRoleLocatorBuilder};
65pub use keyboard::Keyboard;
66pub use locator::{
67 AriaCheckedState, AriaRole, AriaSnapshot, BoundingBox, BoxModel, ElementHandle, FilterBuilder,
68 Locator, LocatorOptions, RoleLocatorBuilder, Selector, TapBuilder, TextOptions,
69};
70pub use locator_handler::{LocatorHandlerHandle, LocatorHandlerManager, LocatorHandlerOptions};
71pub use mouse::Mouse;
72pub use mouse_drag::DragAndDropBuilder;
73pub use navigation::{GotoBuilder, NavigationResponse};
74pub use page_error::{PageError as PageErrorInfo, WebError};
75pub use pdf::{Margins, PaperFormat, PdfBuilder};
76pub use screenshot::{Animations, ClipRegion, ScreenshotBuilder, ScreenshotFormat};
77pub use touchscreen::Touchscreen;
78pub use video::{Video, VideoOptions};
79pub use viewpoint_cdp::protocol::DialogType;
80pub use viewpoint_cdp::protocol::emulation::ViewportSize;
81pub use viewpoint_cdp::protocol::input::MouseButton;
82
83const DEFAULT_NAVIGATION_TIMEOUT: Duration = Duration::from_secs(30);
85
86pub const DEFAULT_TEST_ID_ATTRIBUTE: &str = "data-testid";
88
89pub struct Page {
91 connection: Arc<CdpConnection>,
93 target_id: String,
95 session_id: String,
97 frame_id: String,
99 closed: bool,
101 route_registry: Arc<RouteHandlerRegistry>,
103 keyboard: Keyboard,
105 mouse: Mouse,
107 touchscreen: Touchscreen,
109 event_manager: Arc<PageEventManager>,
111 locator_handler_manager: Arc<LocatorHandlerManager>,
113 video_controller: Option<Arc<Video>>,
115 opener_target_id: Option<String>,
117 popup_manager: Arc<popup::PopupManager>,
119 websocket_manager: Arc<WebSocketManager>,
121 binding_manager: Arc<binding::BindingManager>,
123 test_id_attribute: String,
125 context_registry: Arc<ExecutionContextRegistry>,
127}
128
129impl std::fmt::Debug for Page {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 f.debug_struct("Page")
133 .field("target_id", &self.target_id)
134 .field("session_id", &self.session_id)
135 .field("frame_id", &self.frame_id)
136 .field("closed", &self.closed)
137 .finish_non_exhaustive()
138 }
139}
140
141impl Page {
142 pub fn goto(&self, url: impl Into<String>) -> GotoBuilder<'_> {
167 GotoBuilder::new(self, url.into())
168 }
169
170 pub async fn goto_url(&self, url: &str) -> Result<NavigationResponse, NavigationError> {
181 self.goto(url).goto().await
182 }
183
184 #[instrument(level = "info", skip(self), fields(target_id = %self.target_id, url = %url, wait_until = ?wait_until, timeout_ms = timeout.as_millis()))]
186 pub(crate) async fn navigate_internal(
187 &self,
188 url: &str,
189 wait_until: DocumentLoadState,
190 timeout: Duration,
191 referer: Option<&str>,
192 ) -> Result<NavigationResponse, NavigationError> {
193 if self.closed {
194 warn!("Attempted navigation on closed page");
195 return Err(NavigationError::Cancelled);
196 }
197
198 info!("Starting navigation");
199
200 let event_rx = self.connection.subscribe_events();
202 let mut waiter =
203 LoadStateWaiter::new(event_rx, self.session_id.clone(), self.frame_id.clone());
204 trace!("Created load state waiter");
205
206 debug!("Sending Page.navigate command");
208 let result: NavigateResult = self
209 .connection
210 .send_command(
211 "Page.navigate",
212 Some(NavigateParams {
213 url: url.to_string(),
214 referrer: referer.map(ToString::to_string),
215 transition_type: None,
216 frame_id: None,
217 }),
218 Some(&self.session_id),
219 )
220 .await?;
221
222 debug!(frame_id = %result.frame_id, loader_id = ?result.loader_id, "Page.navigate completed");
223
224 if let Some(ref error_text) = result.error_text {
230 let is_http_error = error_text == "net::ERR_HTTP_RESPONSE_CODE_FAILURE"
231 || error_text == "net::ERR_INVALID_AUTH_CREDENTIALS";
232
233 if !is_http_error {
234 warn!(error = %error_text, "Navigation failed with error");
235 return Err(NavigationError::NetworkError(error_text.clone()));
236 }
237 debug!(error = %error_text, "HTTP error response - continuing to capture status");
238 }
239
240 trace!("Setting commit received");
242 waiter.set_commit_received().await;
243
244 debug!(wait_until = ?wait_until, "Waiting for load state");
246 waiter
247 .wait_for_load_state_with_timeout(wait_until, timeout)
248 .await?;
249
250 let response_data = waiter.response_data().await;
252
253 info!(frame_id = %result.frame_id, "Navigation completed successfully");
254
255 let final_url = response_data.url.unwrap_or_else(|| url.to_string());
257
258 if let Some(status) = response_data.status {
260 Ok(NavigationResponse::with_response(
261 final_url,
262 result.frame_id,
263 status,
264 response_data.headers,
265 ))
266 } else {
267 Ok(NavigationResponse::new(final_url, result.frame_id))
268 }
269 }
270
271 #[instrument(level = "info", skip(self), fields(target_id = %self.target_id))]
277 pub async fn close(&mut self) -> Result<(), PageError> {
278 if self.closed {
279 debug!("Page already closed");
280 return Ok(());
281 }
282
283 info!("Closing page");
284
285 self.route_registry.unroute_all().await;
287 debug!("Route handlers cleaned up");
288
289 self.connection
290 .send_command::<_, serde_json::Value>(
291 "Target.closeTarget",
292 Some(CloseTargetParams {
293 target_id: self.target_id.clone(),
294 }),
295 None,
296 )
297 .await?;
298
299 self.closed = true;
300 info!("Page closed");
301 Ok(())
302 }
303
304 pub fn target_id(&self) -> &str {
306 &self.target_id
307 }
308
309 pub fn session_id(&self) -> &str {
311 &self.session_id
312 }
313
314 pub fn frame_id(&self) -> &str {
316 &self.frame_id
317 }
318
319 pub fn is_closed(&self) -> bool {
321 self.closed
322 }
323
324 pub fn connection(&self) -> &Arc<CdpConnection> {
326 &self.connection
327 }
328
329 pub fn screenshot(&self) -> screenshot::ScreenshotBuilder<'_> {
359 screenshot::ScreenshotBuilder::new(self)
360 }
361
362 pub fn pdf(&self) -> pdf::PdfBuilder<'_> {
391 pdf::PdfBuilder::new(self)
392 }
393
394 pub async fn url(&self) -> Result<String, PageError> {
400 if self.closed {
401 return Err(PageError::Closed);
402 }
403
404 let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
405 .connection
406 .send_command(
407 "Runtime.evaluate",
408 Some(viewpoint_cdp::protocol::runtime::EvaluateParams {
409 expression: js! { window.location.href }.to_string(),
410 object_group: None,
411 include_command_line_api: None,
412 silent: Some(true),
413 context_id: None,
414 return_by_value: Some(true),
415 await_promise: Some(false),
416 }),
417 Some(&self.session_id),
418 )
419 .await?;
420
421 result
422 .result
423 .value
424 .and_then(|v| v.as_str().map(std::string::ToString::to_string))
425 .ok_or_else(|| PageError::EvaluationFailed("Failed to get URL".to_string()))
426 }
427
428 pub async fn title(&self) -> Result<String, PageError> {
434 if self.closed {
435 return Err(PageError::Closed);
436 }
437
438 let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
439 .connection
440 .send_command(
441 "Runtime.evaluate",
442 Some(viewpoint_cdp::protocol::runtime::EvaluateParams {
443 expression: "document.title".to_string(),
444 object_group: None,
445 include_command_line_api: None,
446 silent: Some(true),
447 context_id: None,
448 return_by_value: Some(true),
449 await_promise: Some(false),
450 }),
451 Some(&self.session_id),
452 )
453 .await?;
454
455 result
456 .result
457 .value
458 .and_then(|v| v.as_str().map(std::string::ToString::to_string))
459 .ok_or_else(|| PageError::EvaluationFailed("Failed to get title".to_string()))
460 }
461}
462
463