viewpoint_core/page/
mod.rs

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