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