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