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