viewpoint_core/page/
mod.rs

1//! # Page Management and Interaction
2//!
3//! The `Page` type represents a browser tab and provides methods for navigation,
4//! content interaction, and capturing screenshots or PDFs.
5//!
6//! ## Features
7//!
8//! - **Navigation**: Navigate to URLs, go back/forward, reload
9//! - **Element Interaction**: Locate and interact with elements via [`Locator`]
10//! - **JavaScript Evaluation**: Execute JavaScript in the page context
11//! - **Screenshots**: Capture viewport or full page screenshots
12//! - **PDF Generation**: Generate PDFs from page content
13//! - **Input Devices**: Control keyboard, mouse, and touchscreen
14//! - **Event Handling**: Handle dialogs, downloads, console messages
15//! - **Network Interception**: Route, modify, and mock network requests
16//! - **Clock Mocking**: Control time in the page with [`Clock`]
17//! - **Frames**: Access and interact with iframes via [`Frame`] and [`FrameLocator`]
18//! - **Video Recording**: Record page interactions
19//!
20//! ## Quick Start
21//!
22//! ```no_run
23//! use viewpoint_core::{Browser, DocumentLoadState};
24//! use std::time::Duration;
25//!
26//! # async fn example() -> Result<(), viewpoint_core::CoreError> {
27//! let browser = Browser::launch().headless(true).launch().await?;
28//! let context = browser.new_context().await?;
29//! let page = context.new_page().await?;
30//!
31//! // Navigate to a URL
32//! page.goto("https://example.com")
33//!     .wait_until(DocumentLoadState::DomContentLoaded)
34//!     .goto()
35//!     .await?;
36//!
37//! // Get page title
38//! let title = page.title().await?;
39//! println!("Page title: {}", title);
40//!
41//! // Get current URL
42//! let url = page.url().await?;
43//! println!("Current URL: {}", url);
44//! # Ok(())
45//! # }
46//! ```
47//!
48//! ## Element Interaction with Locators
49//!
50//! ```no_run
51//! use viewpoint_core::{Browser, AriaRole};
52//!
53//! # async fn example() -> Result<(), viewpoint_core::CoreError> {
54//! # let browser = Browser::launch().headless(true).launch().await?;
55//! # let context = browser.new_context().await?;
56//! # let page = context.new_page().await?;
57//! // Click a button
58//! page.locator("button#submit").click().await?;
59//!
60//! // Fill an input
61//! page.locator("input[name='email']").fill("user@example.com").await?;
62//!
63//! // Get text content
64//! let text = page.locator("h1").text_content().await?;
65//!
66//! // Use semantic locators
67//! page.get_by_role(AriaRole::Button)
68//!     .with_name("Submit")
69//!     .build()
70//!     .click()
71//!     .await?;
72//!
73//! page.get_by_label("Username").fill("john").await?;
74//! page.get_by_placeholder("Search...").fill("query").await?;
75//! page.get_by_test_id("submit-btn").click().await?;
76//! # Ok(())
77//! # }
78//! ```
79//!
80//! ## Screenshots and PDF
81//!
82//! ```no_run
83//! use viewpoint_core::Browser;
84//! use viewpoint_core::page::PaperFormat;
85//!
86//! # async fn example() -> Result<(), viewpoint_core::CoreError> {
87//! # let browser = Browser::launch().headless(true).launch().await?;
88//! # let context = browser.new_context().await?;
89//! # let page = context.new_page().await?;
90//! // Viewport screenshot
91//! page.screenshot()
92//!     .path("screenshot.png")
93//!     .capture()
94//!     .await?;
95//!
96//! // Full page screenshot
97//! page.screenshot()
98//!     .full_page(true)
99//!     .path("full-page.png")
100//!     .capture()
101//!     .await?;
102//!
103//! // Generate PDF
104//! page.pdf()
105//!     .format(PaperFormat::A4)
106//!     .path("document.pdf")
107//!     .generate()
108//!     .await?;
109//! # Ok(())
110//! # }
111//! ```
112//!
113//! ## Input Devices
114//!
115//! ```ignore
116//! use viewpoint_core::Browser;
117//!
118//! # async fn example() -> Result<(), viewpoint_core::CoreError> {
119//! # let browser = Browser::launch().headless(true).launch().await?;
120//! # let context = browser.new_context().await?;
121//! # let page = context.new_page().await?;
122//! // Keyboard
123//! page.keyboard().press("Tab").await?;
124//! page.keyboard().type_text("Hello World").await?;
125//! page.keyboard().press("Control+a").await?;
126//!
127//! // Mouse
128//! page.mouse().click(100.0, 200.0).await?;
129//! page.mouse().move_to(300.0, 400.0).await?;
130//!
131//! // Touchscreen
132//! page.touchscreen().tap(100.0, 200.0).await?;
133//! # Ok(())
134//! # }
135//! ```
136//!
137//! ## Event Handling
138//!
139//! ```ignore
140//! use viewpoint_core::Browser;
141//!
142//! # async fn example() -> Result<(), viewpoint_core::CoreError> {
143//! # let browser = Browser::launch().headless(true).launch().await?;
144//! # let context = browser.new_context().await?;
145//! # let page = context.new_page().await?;
146//! // Handle dialogs
147//! page.on_dialog(|dialog| async move {
148//!     println!("Dialog: {}", dialog.message());
149//!     dialog.accept(None).await
150//! }).await;
151//!
152//! // Handle downloads
153//! page.on_download(|download| async move {
154//!     download.save_as("file.zip").await
155//! }).await;
156//!
157//! // Handle console messages
158//! page.on_console(|msg| async move {
159//!     println!("[{}] {}", msg.message_type(), msg.text());
160//!     Ok(())
161//! }).await;
162//! # Ok(())
163//! # }
164//! ```
165//!
166//! ## Frames
167//!
168//! ```ignore
169//! use viewpoint_core::Browser;
170//!
171//! # async fn example() -> Result<(), viewpoint_core::CoreError> {
172//! # let browser = Browser::launch().headless(true).launch().await?;
173//! # let context = browser.new_context().await?;
174//! # let page = context.new_page().await?;
175//! // Access iframe by selector
176//! let frame = page.frame_locator("iframe#content");
177//! frame.locator("button").click().await?;
178//!
179//! // Access iframe by name
180//! let frame = page.frame("content-frame").await;
181//! if let Some(f) = frame {
182//!     f.locator("input").fill("text").await?;
183//! }
184//! # Ok(())
185//! # }
186//! ```
187//!
188//! ## Clock Mocking
189//!
190//! ```ignore
191//! use viewpoint_core::Browser;
192//!
193//! # async fn example() -> Result<(), viewpoint_core::CoreError> {
194//! # let browser = Browser::launch().headless(true).launch().await?;
195//! # let context = browser.new_context().await?;
196//! # let page = context.new_page().await?;
197//! // Install clock mocking
198//! page.clock().install().await?;
199//!
200//! // Set to specific time
201//! page.clock().set_fixed_time("2024-01-01T12:00:00Z").await?;
202//!
203//! // Advance time
204//! page.clock().run_for(60000).await?; // 60 seconds
205//! # Ok(())
206//! # }
207//! ```
208
209mod accessors;
210mod aria_snapshot;
211pub use aria_snapshot::SnapshotOptions;
212pub mod binding;
213pub mod clock;
214mod clock_script;
215pub mod console;
216mod constructors;
217mod content;
218pub mod dialog;
219pub mod download;
220pub mod emulation;
221mod evaluate;
222pub mod events;
223pub mod file_chooser;
224pub mod frame;
225pub mod frame_locator;
226mod frame_locator_actions;
227mod frame_page_methods;
228mod input_devices;
229pub mod keyboard;
230mod lifecycle;
231pub mod locator;
232mod locator_factory;
233pub mod locator_handler;
234mod mouse;
235mod mouse_drag;
236mod navigation;
237pub mod page_error;
238mod page_info;
239mod pdf;
240pub mod popup;
241mod ref_resolution;
242mod routing_impl;
243mod screenshot;
244mod screenshot_element;
245mod scripts;
246mod touchscreen;
247pub mod video;
248mod video_io;
249
250use std::sync::Arc;
251use std::time::Duration;
252
253use tokio::sync::RwLock;
254use viewpoint_cdp::CdpConnection;
255
256
257
258use crate::error::NavigationError;
259use crate::network::{RouteHandlerRegistry, WebSocketManager};
260
261pub use clock::{Clock, TimeValue};
262pub use console::{ConsoleMessage, ConsoleMessageLocation, ConsoleMessageType, JsArg};
263pub use content::{ScriptTagBuilder, ScriptType, SetContentBuilder, StyleTagBuilder};
264pub use dialog::Dialog;
265pub use download::{Download, DownloadState};
266pub use emulation::{EmulateMediaBuilder, MediaType, VisionDeficiency};
267pub use evaluate::{JsHandle, Polling, WaitForFunctionBuilder};
268pub use events::PageEventManager;
269pub use file_chooser::{FileChooser, FilePayload};
270pub(crate) use frame::ExecutionContextRegistry;
271pub use frame::Frame;
272pub use frame_locator::{FrameElementLocator, FrameLocator, FrameRoleLocatorBuilder};
273pub use keyboard::Keyboard;
274pub use locator::{
275    AriaCheckedState, AriaRole, AriaSnapshot, BoundingBox, BoxModel, ElementHandle, FilterBuilder,
276    Locator, LocatorOptions, RoleLocatorBuilder, Selector, TapBuilder, TextOptions,
277};
278pub use locator_handler::{LocatorHandlerHandle, LocatorHandlerManager, LocatorHandlerOptions};
279pub use mouse::Mouse;
280pub use mouse_drag::DragAndDropBuilder;
281pub use navigation::{GotoBuilder, NavigationResponse};
282pub use page_error::{PageError as PageErrorInfo, WebError};
283pub use pdf::{Margins, PaperFormat, PdfBuilder};
284pub use screenshot::{Animations, ClipRegion, ScreenshotBuilder, ScreenshotFormat};
285pub use touchscreen::Touchscreen;
286pub use video::{Video, VideoOptions};
287pub use viewpoint_cdp::protocol::DialogType;
288pub use viewpoint_cdp::protocol::emulation::ViewportSize;
289pub use viewpoint_cdp::protocol::input::MouseButton;
290
291/// Default navigation timeout.
292const DEFAULT_NAVIGATION_TIMEOUT: Duration = Duration::from_secs(30);
293
294/// Default test ID attribute name.
295pub const DEFAULT_TEST_ID_ATTRIBUTE: &str = "data-testid";
296
297/// A browser page (tab).
298pub struct Page {
299    /// CDP connection.
300    connection: Arc<CdpConnection>,
301    /// Target ID.
302    target_id: String,
303    /// Session ID for this page.
304    session_id: String,
305    /// Main frame ID.
306    frame_id: String,
307    /// Context index for element ref generation.
308    /// Used to generate scoped element refs in the format `c{contextIndex}p{pageIndex}e{counter}`.
309    context_index: usize,
310    /// Page index within the context for element ref generation.
311    /// Used to generate scoped element refs in the format `c{contextIndex}p{pageIndex}e{counter}`.
312    page_index: usize,
313    /// Whether the page has been closed.
314    closed: bool,
315    /// Route handler registry.
316    route_registry: Arc<RouteHandlerRegistry>,
317    /// Keyboard controller.
318    keyboard: Keyboard,
319    /// Mouse controller.
320    mouse: Mouse,
321    /// Touchscreen controller.
322    touchscreen: Touchscreen,
323    /// Event manager for dialogs, downloads, and file choosers.
324    event_manager: Arc<PageEventManager>,
325    /// Locator handler manager.
326    locator_handler_manager: Arc<LocatorHandlerManager>,
327    /// Video recording controller (if recording is enabled).
328    video_controller: Option<Arc<Video>>,
329    /// Opener target ID (for popup pages).
330    opener_target_id: Option<String>,
331    /// Popup event manager.
332    popup_manager: Arc<popup::PopupManager>,
333    /// WebSocket event manager.
334    websocket_manager: Arc<WebSocketManager>,
335    /// Exposed function binding manager.
336    binding_manager: Arc<binding::BindingManager>,
337    /// Custom test ID attribute (defaults to "data-testid").
338    test_id_attribute: String,
339    /// Execution context registry for tracking frame contexts.
340    context_registry: Arc<ExecutionContextRegistry>,
341    /// Ref map for element ref resolution.
342    /// Maps ref strings (e.g., `c0p0e1`) to their backendNodeIds.
343    /// Updated on each `aria_snapshot()` call.
344    ref_map: std::sync::Arc<
345        parking_lot::RwLock<
346            std::collections::HashMap<String, viewpoint_cdp::protocol::dom::BackendNodeId>,
347        >,
348    >,
349    /// Reference to context's pages list for removal on close.
350    /// This is used to remove the page from the context's tracking list when closed,
351    /// preventing stale sessions from accumulating.
352    /// Stores a Vec<Page> to enable returning functional Page objects from context.pages().
353    context_pages: Option<Arc<RwLock<Vec<Page>>>>,
354}
355
356// Manual Debug implementation since some fields don't implement Debug
357impl std::fmt::Debug for Page {
358    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359        f.debug_struct("Page")
360            .field("target_id", &self.target_id)
361            .field("session_id", &self.session_id)
362            .field("frame_id", &self.frame_id)
363            .field("context_index", &self.context_index)
364            .field("page_index", &self.page_index)
365            .field("closed", &self.closed)
366            .finish_non_exhaustive()
367    }
368}
369
370impl Page {
371    /// Navigate to a URL.
372    ///
373    /// Returns a builder for configuring navigation options.
374    ///
375    /// # Example
376    ///
377    /// ```no_run
378    /// use viewpoint_core::Page;
379    /// use viewpoint_core::DocumentLoadState;
380    /// use std::time::Duration;
381    ///
382    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
383    /// // Simple navigation
384    /// page.goto("https://example.com").goto().await?;
385    ///
386    /// // Navigation with options
387    /// page.goto("https://example.com")
388    ///     .wait_until(DocumentLoadState::DomContentLoaded)
389    ///     .timeout(Duration::from_secs(10))
390    ///     .goto()
391    ///     .await?;
392    /// # Ok(())
393    /// # }
394    /// ```
395    pub fn goto(&self, url: impl Into<String>) -> GotoBuilder<'_> {
396        GotoBuilder::new(self, url.into())
397    }
398
399    /// Navigate to a URL and wait for the specified load state.
400    ///
401    /// This is a convenience method that calls `goto(url).goto().await`.
402    ///
403    /// # Errors
404    ///
405    /// Returns an error if:
406    /// - The page is closed
407    /// - Navigation fails
408    /// - The wait times out
409    pub async fn goto_url(&self, url: &str) -> Result<NavigationResponse, NavigationError> {
410        self.goto(url).goto().await
411    }
412
413    // =========================================================================
414    // Screenshot & PDF Methods
415    // =========================================================================
416
417    /// Create a screenshot builder for capturing page screenshots.
418    ///
419    /// # Example
420    ///
421    /// ```no_run
422    /// # async fn example(page: viewpoint_core::Page) -> Result<(), viewpoint_core::CoreError> {
423    /// // Capture viewport screenshot
424    /// let bytes = page.screenshot().capture().await?;
425    ///
426    /// // Capture full page screenshot
427    /// page.screenshot()
428    ///     .full_page(true)
429    ///     .path("screenshot.png")
430    ///     .capture()
431    ///     .await?;
432    ///
433    /// // Capture JPEG with quality
434    /// page.screenshot()
435    ///     .jpeg(Some(80))
436    ///     .path("screenshot.jpg")
437    ///     .capture()
438    ///     .await?;
439    /// # Ok(())
440    /// # }
441    /// ```
442    pub fn screenshot(&self) -> screenshot::ScreenshotBuilder<'_> {
443        screenshot::ScreenshotBuilder::new(self)
444    }
445
446    /// Create a PDF builder for generating PDFs from the page.
447    ///
448    /// # Example
449    ///
450    /// ```no_run
451    /// use viewpoint_core::page::PaperFormat;
452    ///
453    /// # async fn example(page: viewpoint_core::Page) -> Result<(), viewpoint_core::CoreError> {
454    /// // Generate PDF with default settings
455    /// let bytes = page.pdf().generate().await?;
456    ///
457    /// // Generate A4 landscape PDF
458    /// page.pdf()
459    ///     .format(PaperFormat::A4)
460    ///     .landscape(true)
461    ///     .path("document.pdf")
462    ///     .generate()
463    ///     .await?;
464    ///
465    /// // Generate PDF with custom margins
466    /// page.pdf()
467    ///     .margin(1.0) // 1 inch margins
468    ///     .print_background(true)
469    ///     .generate()
470    ///     .await?;
471    /// # Ok(())
472    /// # }
473    /// ```
474    pub fn pdf(&self) -> pdf::PdfBuilder<'_> {
475        pdf::PdfBuilder::new(self)
476    }
477}