viewpoint_core/page/
mod.rs

1//! Page management and navigation.
2
3mod 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
83/// Default navigation timeout.
84const DEFAULT_NAVIGATION_TIMEOUT: Duration = Duration::from_secs(30);
85
86/// Default test ID attribute name.
87pub const DEFAULT_TEST_ID_ATTRIBUTE: &str = "data-testid";
88
89/// A browser page (tab).
90pub struct Page {
91    /// CDP connection.
92    connection: Arc<CdpConnection>,
93    /// Target ID.
94    target_id: String,
95    /// Session ID for this page.
96    session_id: String,
97    /// Main frame ID.
98    frame_id: String,
99    /// Whether the page has been closed.
100    closed: bool,
101    /// Route handler registry.
102    route_registry: Arc<RouteHandlerRegistry>,
103    /// Keyboard controller.
104    keyboard: Keyboard,
105    /// Mouse controller.
106    mouse: Mouse,
107    /// Touchscreen controller.
108    touchscreen: Touchscreen,
109    /// Event manager for dialogs, downloads, and file choosers.
110    event_manager: Arc<PageEventManager>,
111    /// Locator handler manager.
112    locator_handler_manager: Arc<LocatorHandlerManager>,
113    /// Video recording controller (if recording is enabled).
114    video_controller: Option<Arc<Video>>,
115    /// Opener target ID (for popup pages).
116    opener_target_id: Option<String>,
117    /// Popup event manager.
118    popup_manager: Arc<popup::PopupManager>,
119    /// WebSocket event manager.
120    websocket_manager: Arc<WebSocketManager>,
121    /// Exposed function binding manager.
122    binding_manager: Arc<binding::BindingManager>,
123    /// Custom test ID attribute (defaults to "data-testid").
124    test_id_attribute: String,
125    /// Execution context registry for tracking frame contexts.
126    context_registry: Arc<ExecutionContextRegistry>,
127}
128
129// Manual Debug implementation since some fields don't implement Debug
130impl 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    /// Navigate to a URL.
143    ///
144    /// Returns a builder for configuring navigation options.
145    ///
146    /// # Example
147    ///
148    /// ```no_run
149    /// use viewpoint_core::Page;
150    /// use viewpoint_core::DocumentLoadState;
151    /// use std::time::Duration;
152    ///
153    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
154    /// // Simple navigation
155    /// page.goto("https://example.com").goto().await?;
156    ///
157    /// // Navigation with options
158    /// page.goto("https://example.com")
159    ///     .wait_until(DocumentLoadState::DomContentLoaded)
160    ///     .timeout(Duration::from_secs(10))
161    ///     .goto()
162    ///     .await?;
163    /// # Ok(())
164    /// # }
165    /// ```
166    pub fn goto(&self, url: impl Into<String>) -> GotoBuilder<'_> {
167        GotoBuilder::new(self, url.into())
168    }
169
170    /// Navigate to a URL and wait for the specified load state.
171    ///
172    /// This is a convenience method that calls `goto(url).goto().await`.
173    ///
174    /// # Errors
175    ///
176    /// Returns an error if:
177    /// - The page is closed
178    /// - Navigation fails
179    /// - The wait times out
180    pub async fn goto_url(&self, url: &str) -> Result<NavigationResponse, NavigationError> {
181        self.goto(url).goto().await
182    }
183
184    /// Navigate to a URL with the given options.
185    #[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        // Create a load state waiter
201        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        // Send the navigation command
207        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        // Check for navigation errors
225        // Note: Chrome reports HTTP error status codes (4xx, 5xx) as errors with
226        // "net::ERR_HTTP_RESPONSE_CODE_FAILURE" or "net::ERR_INVALID_AUTH_CREDENTIALS".
227        // Following Playwright's behavior, we treat these as successful navigations
228        // that return a response with the appropriate status code.
229        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        // Mark commit as received
241        trace!("Setting commit received");
242        waiter.set_commit_received().await;
243
244        // Wait for the target load state
245        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        // Get response data captured during navigation
251        let response_data = waiter.response_data().await;
252
253        info!(frame_id = %result.frame_id, "Navigation completed successfully");
254
255        // Use the final URL from response data if available (handles redirects)
256        let final_url = response_data.url.unwrap_or_else(|| url.to_string());
257
258        // Build the response with captured data
259        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    /// Close this page.
272    ///
273    /// # Errors
274    ///
275    /// Returns an error if closing fails.
276    #[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        // Clean up route handlers
286        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    /// Get the target ID.
305    pub fn target_id(&self) -> &str {
306        &self.target_id
307    }
308
309    /// Get the session ID.
310    pub fn session_id(&self) -> &str {
311        &self.session_id
312    }
313
314    /// Get the main frame ID.
315    pub fn frame_id(&self) -> &str {
316        &self.frame_id
317    }
318
319    /// Check if this page has been closed.
320    pub fn is_closed(&self) -> bool {
321        self.closed
322    }
323
324    /// Get a reference to the CDP connection.
325    pub fn connection(&self) -> &Arc<CdpConnection> {
326        &self.connection
327    }
328
329    // =========================================================================
330    // Screenshot & PDF Methods
331    // =========================================================================
332
333    /// Create a screenshot builder for capturing page screenshots.
334    ///
335    /// # Example
336    ///
337    /// ```no_run
338    /// # async fn example(page: viewpoint_core::Page) -> Result<(), viewpoint_core::CoreError> {
339    /// // Capture viewport screenshot
340    /// let bytes = page.screenshot().capture().await?;
341    ///
342    /// // Capture full page screenshot
343    /// page.screenshot()
344    ///     .full_page(true)
345    ///     .path("screenshot.png")
346    ///     .capture()
347    ///     .await?;
348    ///
349    /// // Capture JPEG with quality
350    /// page.screenshot()
351    ///     .jpeg(Some(80))
352    ///     .path("screenshot.jpg")
353    ///     .capture()
354    ///     .await?;
355    /// # Ok(())
356    /// # }
357    /// ```
358    pub fn screenshot(&self) -> screenshot::ScreenshotBuilder<'_> {
359        screenshot::ScreenshotBuilder::new(self)
360    }
361
362    /// Create a PDF builder for generating PDFs from the page.
363    ///
364    /// # Example
365    ///
366    /// ```no_run
367    /// use viewpoint_core::page::PaperFormat;
368    ///
369    /// # async fn example(page: viewpoint_core::Page) -> Result<(), viewpoint_core::CoreError> {
370    /// // Generate PDF with default settings
371    /// let bytes = page.pdf().generate().await?;
372    ///
373    /// // Generate A4 landscape PDF
374    /// page.pdf()
375    ///     .format(PaperFormat::A4)
376    ///     .landscape(true)
377    ///     .path("document.pdf")
378    ///     .generate()
379    ///     .await?;
380    ///
381    /// // Generate PDF with custom margins
382    /// page.pdf()
383    ///     .margin(1.0) // 1 inch margins
384    ///     .print_background(true)
385    ///     .generate()
386    ///     .await?;
387    /// # Ok(())
388    /// # }
389    /// ```
390    pub fn pdf(&self) -> pdf::PdfBuilder<'_> {
391        pdf::PdfBuilder::new(self)
392    }
393
394    /// Get the current page URL.
395    ///
396    /// # Errors
397    ///
398    /// Returns an error if the page is closed or the evaluation fails.
399    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    /// Get the current page title.
429    ///
430    /// # Errors
431    ///
432    /// Returns an error if the page is closed or the evaluation fails.
433    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// Additional Page methods are defined in:
464// - scripts.rs: add_init_script, add_init_script_path
465// - locator_handler.rs: add_locator_handler, add_locator_handler_with_options, remove_locator_handler
466// - video.rs: video, start_video_recording, stop_video_recording
467// - binding.rs: expose_function, remove_exposed_function
468// - input_devices.rs: keyboard, mouse, touchscreen, clock, drag_and_drop
469// - locator_factory.rs: locator, get_by_*, set_test_id_attribute
470// - DragAndDropBuilder is defined in mouse.rs
471// - RoleLocatorBuilder is defined in locator/mod.rs