Skip to main content

cdp_core/page/
page_core.rs

1use crate::DomainType;
2use crate::accessibility::AccessibilityController;
3use crate::browser::{
4    discovery::{find_running_browser_port, find_running_browser_port_by_process_name},
5    launcher::BrowserType,
6    ws_endpoints::resolve_active_page_ws_url,
7};
8use crate::domain_manager::DomainManager;
9use crate::emulation::EmulationController;
10use crate::error::{CdpError, Result};
11use crate::input::{keyboard::Keyboard, mouse::Mouse};
12use crate::network::network_intercept::{
13    NetworkEventCallback, NetworkMonitor, ResponseMonitorManager,
14};
15use crate::page::{element::ElementHandle, frame::Frame};
16use crate::session::Session;
17use crate::tracing::{TracingController, TracingSessionState};
18use crate::transport::{cdp_protocol::*, websocket_connection::*};
19use cdp_protocol::page::Viewport;
20use cdp_protocol::{
21    page::{
22        self as page_cdp, FrameTree, GetFrameTree, GetFrameTreeReturnObject, Navigate,
23        NavigateReturnObject,
24    },
25    runtime::{Evaluate, EvaluateReturnObject},
26};
27use futures_util::Stream;
28use futures_util::StreamExt;
29use std::collections::HashMap;
30use std::path::PathBuf;
31use std::pin::Pin;
32use std::sync::Arc;
33use tokio::fs::File;
34use tokio::io::AsyncWriteExt;
35use tokio::sync::Mutex;
36use tokio::sync::broadcast;
37use tokio::sync::mpsc;
38use tokio_stream::wrappers::BroadcastStream;
39use tokio_tungstenite::connect_async;
40
41/// Frame lifecycle event type
42#[derive(Clone, Debug)]
43pub enum FrameLifecycleEvent {
44    /// Frame attached
45    Attached {
46        frame_id: String,
47        parent_frame_id: Option<String>,
48    },
49    /// Frame detached
50    Detached { frame_id: String },
51    /// Frame navigated
52    Navigated { frame_id: String, url: String },
53}
54
55/// Frame lifecycle callback
56pub type FrameLifecycleCallback = Arc<dyn Fn(FrameLifecycleEvent) + Send + Sync>;
57
58/// DOM mutation event type
59#[derive(Clone, Debug)]
60pub enum DomMutationEvent {
61    /// Child node inserted
62    ChildNodeInserted {
63        parent_node_id: u32,
64        previous_node_id: u32,
65        node: serde_json::Value,
66    },
67    /// Child node removed
68    ChildNodeRemoved { parent_node_id: u32, node_id: u32 },
69    /// Attribute modified
70    AttributeModified {
71        node_id: u32,
72        name: String,
73        value: String,
74    },
75    /// Attribute removed
76    AttributeRemoved { node_id: u32, name: String },
77    /// Character data modified
78    CharacterDataModified {
79        node_id: u32,
80        character_data: String,
81    },
82}
83
84/// Navigation wait options
85#[derive(Debug, Clone, Default)]
86pub struct WaitForNavigationOptions {
87    /// Timeout in milliseconds, default 30000
88    pub timeout_ms: Option<u64>,
89    /// Wait condition, default WaitUntil::Load
90    pub wait_until: Option<WaitUntil>,
91}
92
93/// Selector wait options
94#[derive(Debug, Clone, Default)]
95pub struct WaitForSelectorOptions {
96    /// Timeout in milliseconds, default 30000
97    pub timeout_ms: Option<u64>,
98    /// Whether to wait for element to be visible, default false
99    pub visible: Option<bool>,
100    /// Whether to wait for element to be hidden, default false
101    pub hidden: Option<bool>,
102}
103
104/// Page navigation wait condition
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
106pub enum WaitUntil {
107    /// Wait for load event (document.readyState === 'complete')
108    /// This is the strictest condition, waiting for all resources (images, stylesheets, etc.) to load
109    Load,
110
111    /// Wait for DOMContentLoaded event (document.readyState === 'interactive' or 'complete')
112    /// DOM tree is built, but external resources may still be loading
113    DOMContentLoaded,
114
115    /// Wait for network idle (no network connections for at least 500ms)
116    /// Suitable for SPAs waiting for async data loading
117    NetworkIdle0,
118
119    /// Wait for network almost idle (no more than 2 network connections for at least 500ms)
120    /// More relaxed than NetworkIdle0, suitable for most scenarios
121    #[default]
122    NetworkIdle2,
123}
124
125/// DOM mutation callback
126pub type DomMutationCallback = Arc<dyn Fn(DomMutationEvent) + Send + Sync>;
127
128/// Internal information for a registered execution context.
129#[derive(Clone, Debug)]
130pub struct ExecutionContextInfo {
131    /// Numeric execution context identifier.
132    pub id: u32,
133    /// System-unique execution context identifier.
134    pub unique_id: String,
135}
136
137// --- PAGE (Unified Interface) ---
138pub struct Page {
139    pub(crate) session: Arc<Session>,
140    /// Frame ID -> Execution Context Info mapping
141    pub contexts: Arc<Mutex<HashMap<String, ExecutionContextInfo>>>,
142    /// Domain manager (unified management of CDP Domain enabling/disabling)
143    pub domain_manager: Arc<DomainManager>,
144    /// Network monitor
145    pub network_monitor: Arc<NetworkMonitor>,
146    /// Response monitor manager
147    pub response_monitor_manager: Arc<ResponseMonitorManager>,
148    /// Main Frame (lazy loaded)
149    main_frame_cache: Arc<Mutex<Option<Frame>>>,
150    /// Cache of all Frames (Frame ID -> Frame)
151    frames_cache: Arc<Mutex<HashMap<String, Frame>>>,
152    /// Frame parent-child relationship mapping (Frame ID -> Parent Frame ID)
153    frame_parent_map: Arc<Mutex<HashMap<String, String>>>,
154    /// Frame children mapping (Frame ID -> Child Frame IDs)
155    frame_children_map: Arc<Mutex<HashMap<String, Vec<String>>>>,
156    /// Frame lifecycle callback list
157    lifecycle_callbacks: Arc<Mutex<Vec<FrameLifecycleCallback>>>,
158    /// DOM mutation callback list
159    dom_mutation_callbacks: Arc<Mutex<Vec<DomMutationCallback>>>,
160    /// Tracing state cache
161    tracing_state: Arc<Mutex<TracingSessionState>>,
162    /// File chooser mutex lock, ensuring upload flow serialization
163    pub(crate) file_chooser_lock: Arc<Mutex<()>>,
164}
165
166impl Page {
167    /// Mode 1 entry: Create via Browser
168    pub(crate) fn new_from_browser(session: Arc<Session>) -> Self {
169        let domain_manager = Arc::new(DomainManager::new(Arc::clone(&session)));
170        Self {
171            session,
172            contexts: Arc::new(Mutex::new(HashMap::new())),
173            domain_manager,
174            main_frame_cache: Arc::new(Mutex::new(None)),
175            frames_cache: Arc::new(Mutex::new(HashMap::new())),
176            frame_parent_map: Arc::new(Mutex::new(HashMap::new())),
177            frame_children_map: Arc::new(Mutex::new(HashMap::new())),
178            lifecycle_callbacks: Arc::new(Mutex::new(Vec::new())),
179            dom_mutation_callbacks: Arc::new(Mutex::new(Vec::new())),
180            network_monitor: Arc::new(NetworkMonitor::default()),
181            response_monitor_manager: Arc::new(ResponseMonitorManager::default()),
182            tracing_state: Arc::new(Mutex::new(TracingSessionState::default())),
183            file_chooser_lock: Arc::new(Mutex::new(())),
184        }
185    }
186
187    /// Mode 2 entry: Connect directly
188    pub async fn connect_to_active_page(
189        port: Option<u16>,
190        pattern: Option<&str>,
191    ) -> Result<Arc<Self>> {
192        let port = Self::resolve_remote_debug_port(port, None)?;
193        Self::connect_to_active_page_inner(port, pattern).await
194    }
195
196    /// Mode 2 entry: Connect directly (supports custom browser process)
197    pub async fn connect_to_active_page_with_browser(
198        port: Option<u16>,
199        pattern: Option<&str>,
200        browser_process: Option<&str>,
201    ) -> Result<Arc<Self>> {
202        let port = Self::resolve_remote_debug_port(port, browser_process)?;
203        Self::connect_to_active_page_inner(port, pattern).await
204    }
205
206    fn resolve_remote_debug_port(port: Option<u16>, browser_process: Option<&str>) -> Result<u16> {
207        if let Some(explicit) = port {
208            return Ok(explicit);
209        }
210
211        if let Some(process_name) = browser_process {
212            return find_running_browser_port_by_process_name(process_name);
213        }
214
215        for bt in [
216            BrowserType::Chrome,
217            BrowserType::Edge,
218            BrowserType::Chromium,
219        ] {
220            if let Ok(found) = find_running_browser_port(bt) {
221                return Ok(found);
222            }
223        }
224
225        Err(CdpError::tool(
226            "Found no running browser with remote debugging enabled, please specify browser type or port explicitly",
227        ))
228    }
229
230    async fn connect_to_active_page_inner(port: u16, pattern: Option<&str>) -> Result<Arc<Self>> {
231        let page_ws_url = resolve_active_page_ws_url(port, pattern).await?;
232        let (ws_stream, _) = connect_async(page_ws_url.as_str()).await?;
233        let (writer, reader) = ws_stream.split();
234        // Only one Session, so only one event channel
235        let (event_sender, event_receiver) = mpsc::channel(crate::DEFAULT_CHANNEL_CAPACITY);
236        let connection = Arc::new(ConnectionInternals::new(writer));
237
238        let session = Session::new(None, connection.clone(), event_receiver);
239        let session = Arc::new(session);
240        let domain_manager = Arc::new(DomainManager::new(Arc::clone(&session)));
241
242        let page = Arc::new(Page {
243            session,
244            contexts: Arc::new(Mutex::new(HashMap::new())),
245            domain_manager,
246            main_frame_cache: Arc::new(Mutex::new(None)),
247            frames_cache: Arc::new(Mutex::new(HashMap::new())),
248            frame_parent_map: Arc::new(Mutex::new(HashMap::new())),
249            frame_children_map: Arc::new(Mutex::new(HashMap::new())),
250            lifecycle_callbacks: Arc::new(Mutex::new(Vec::new())),
251            dom_mutation_callbacks: Arc::new(Mutex::new(Vec::new())),
252            network_monitor: Arc::new(NetworkMonitor::default()),
253            response_monitor_manager: Arc::new(ResponseMonitorManager::default()),
254            tracing_state: Arc::new(Mutex::new(TracingSessionState::default())),
255            file_chooser_lock: Arc::new(Mutex::new(())),
256        });
257
258        // Create weak reference to pass to dispatcher
259        let page_weak = Arc::downgrade(&page);
260
261        // Start the event dispatcher specific to this connection (with Page reference)
262        tokio::spawn(direct_message_dispatcher(
263            reader,
264            connection.clone(),
265            event_sender,
266            page_weak,
267        ));
268
269        // Use DomainManager to enable all required Domains
270        page.domain_manager.enable_required_domains().await?;
271
272        Ok(page)
273    }
274
275    /// Get mouse controller
276    pub fn mouse(&self) -> Mouse {
277        Mouse::new(Arc::clone(&self.session), Arc::clone(&self.domain_manager))
278    }
279
280    /// Get keyboard controller
281    pub fn keyboard(&self) -> Keyboard {
282        Keyboard::new(Arc::clone(&self.session), Arc::clone(&self.domain_manager))
283    }
284
285    pub fn accessibility(self: &Arc<Self>) -> AccessibilityController {
286        AccessibilityController::new(Arc::clone(self))
287    }
288
289    pub fn emulation(self: &Arc<Self>) -> EmulationController {
290        EmulationController::new(Arc::clone(self))
291    }
292
293    pub fn tracing(self: &Arc<Self>) -> TracingController {
294        TracingController::new(Arc::clone(self), Arc::clone(&self.tracing_state))
295    }
296
297    pub async fn navigate(&self, url: &str) -> Result<NavigateReturnObject> {
298        let navigate = Navigate {
299            url: url.to_string(),
300            referrer: None,
301            transition_type: None,
302            frame_id: None,
303            referrer_policy: None,
304        };
305        self.session
306            .send_command::<_, NavigateReturnObject>(navigate, None)
307            .await
308    }
309
310    fn events(&self) -> broadcast::Receiver<CdpEvent> {
311        self.session.event_bus.subscribe()
312    }
313
314    pub fn on<E>(&self) -> Pin<Box<dyn Stream<Item = E> + Send + 'static>>
315    where
316        E: TryFrom<CdpEvent> + Send + 'static,
317        <E as TryFrom<CdpEvent>>::Error: Send,
318    {
319        let stream = BroadcastStream::new(self.events())
320            .filter_map(|result| async { result.ok() })
321            .filter_map(|event| async { E::try_from(event).ok() });
322        Box::pin(stream)
323    }
324
325    pub(crate) async fn wait_for<E>(&self) -> Result<E>
326    where
327        E: TryFrom<CdpEvent> + Send + 'static,
328        <E as TryFrom<CdpEvent>>::Error: Send,
329    {
330        self.on::<E>().next().await.ok_or_else(|| {
331            CdpError::page("Event stream closed before event was received.".to_string())
332        })
333    }
334
335    pub async fn wait_for_loaded(&self) -> Result<page_cdp::events::LoadEventFiredEvent> {
336        self.wait_for::<page_cdp::events::LoadEventFiredEvent>()
337            .await
338    }
339
340    pub async fn get_title(&self) -> Result<String> {
341        let method = Evaluate {
342            expression: "document.title".to_string(),
343            object_group: None,
344            include_command_line_api: None,
345            silent: None,
346            context_id: None,
347            return_by_value: None,
348            generate_preview: None,
349            user_gesture: None,
350            await_promise: None,
351            throw_on_side_effect: None,
352            timeout: None,
353            disable_breaks: None,
354            repl_mode: None,
355            allow_unsafe_eval_blocked_by_csp: None,
356            unique_context_id: None,
357            serialization_options: None,
358        };
359        let eval_result = self
360            .session
361            .send_command::<_, EvaluateReturnObject>(method, None)
362            .await?;
363
364        // Extract title string from complex return structure
365        let title = eval_result
366            .result
367            .value
368            .and_then(|v| v.as_str().map(ToString::to_string))
369            .ok_or_else(|| {
370                CdpError::page("Could not extract title string from evaluation result".to_string())
371            })?;
372
373        Ok(title)
374    }
375
376    /// Get main Frame (cached)
377    pub async fn main_frame(self: &Arc<Self>) -> Result<Frame> {
378        {
379            let cache = self.main_frame_cache.lock().await;
380            if let Some(frame) = cache.as_ref() {
381                return Ok(frame.clone());
382            }
383        }
384        let method = GetFrameTree(None);
385        let obj = self
386            .session
387            .send_command::<_, GetFrameTreeReturnObject>(method, None)
388            .await?;
389        let main_frame_id = obj.frame_tree.frame.id.clone();
390        let main_frame = Frame::new(main_frame_id.clone(), Arc::clone(self));
391        {
392            let mut cache = self.main_frame_cache.lock().await;
393            *cache = Some(main_frame.clone());
394        }
395        {
396            let mut frames = self.frames_cache.lock().await;
397            frames.insert(main_frame_id, main_frame.clone());
398        }
399
400        Ok(main_frame)
401    }
402
403    /// Queries the first element matching the CSS selector.
404    ///
405    /// This method automatically searches across all frames (including iframes):
406    /// 1. First searches in the main frame
407    /// 2. If not found, searches in all child frames
408    ///
409    /// # Parameters
410    /// * `selector` - CSS selector string
411    ///
412    /// # Returns
413    /// First matching element handle, or None if no match
414    ///
415    /// # Examples
416    /// ```no_run
417    /// # use cdp_core::Page;
418    /// # use std::sync::Arc;
419    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
420    /// // Automatically search for first matching button in all frames
421    /// if let Some(button) = page.query_selector(".submit-btn").await? {
422    ///     button.click().await?;
423    /// }
424    /// # Ok(())
425    /// # }
426    /// ```
427    pub async fn query_selector(self: &Arc<Self>, selector: &str) -> Result<Option<ElementHandle>> {
428        // Search in main frame first
429        let main_frame = self.main_frame().await?;
430        match main_frame.query_selector(selector).await {
431            Ok(Some(element)) => return Ok(Some(element)),
432            Ok(None) => {} // Not found, continue searching other frames
433            Err(e) => {
434                // If execution context does not exist, continue trying other frames
435                eprintln!("Warning: Failed to query in main frame: {}", e);
436            }
437        }
438
439        // If not found in main frame or error occurred, search in all child frames
440        let frames = self.all_frames().await?;
441        for frame in frames.iter().skip(1) {
442            // Skip the first one (main frame)
443            match frame.query_selector(selector).await {
444                Ok(Some(element)) => return Ok(Some(element)),
445                Ok(None) => continue, // Not found, continue to next frame
446                Err(e) => {
447                    // Frame query failed (possibly detached or context missing), continue trying next
448                    eprintln!("Warning: Failed to query in frame {}: {}", frame.id(), e);
449                    continue;
450                }
451            }
452        }
453
454        Ok(None)
455    }
456
457    /// Queries all elements matching the CSS selector.
458    ///
459    /// This method automatically searches across all frames (including iframes):
460    /// 1. First searches in the main frame
461    /// 2. Then searches in all child frames and collects all matches
462    ///
463    /// # Parameters
464    /// * `selector` - CSS selector string
465    ///
466    /// # Returns
467    /// List of all matching element handles, or empty list if no match
468    ///
469    /// # Examples
470    /// ```no_run
471    /// # use cdp_core::Page;
472    /// # use std::sync::Arc;
473    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
474    /// // Automatically search for all matching links in all frames
475    /// let links = page.query_selector_all("a").await?;
476    /// println!("Found {} links across all frames", links.len());
477    ///
478    /// for (i, link) in links.iter().enumerate() {
479    ///     let href = link.get_attribute("href").await?;
480    ///     println!("Link {}: {:?}", i + 1, href);
481    /// }
482    /// # Ok(())
483    /// # }
484    /// ```
485    pub async fn query_selector_all(
486        self: &Arc<Self>,
487        selector: &str,
488    ) -> Result<Vec<ElementHandle>> {
489        // Get all frames
490        let frames = self.all_frames().await?;
491        let mut all_elements = Vec::new();
492
493        // Query and collect results in each frame
494        for frame in frames {
495            match frame.query_selector_all(selector).await {
496                Ok(elements) => {
497                    all_elements.extend(elements);
498                }
499                Err(e) => {
500                    // Frame query failed (possibly detached or context missing), continue trying next
501                    eprintln!("Warning: Failed to query in frame {}: {}", frame.id(), e);
502                    continue;
503                }
504            }
505        }
506
507        Ok(all_elements)
508    }
509
510    /// Returns a flat list of all frames attached to this page.
511    pub async fn all_frames(self: &Arc<Self>) -> Result<Vec<Frame>> {
512        let method = GetFrameTree(None);
513        let result = self
514            .session
515            .send_command::<_, GetFrameTreeReturnObject>(method, None)
516            .await?;
517        let mut all_frames = Vec::new();
518        let mut new_cache = HashMap::new();
519        let mut new_parent_map = HashMap::new();
520        let mut new_children_map = HashMap::new();
521
522        Self::collect_frames_recursive(
523            &result.frame_tree,
524            None, // Main frame has no parent
525            Arc::clone(self),
526            &mut all_frames,
527            &mut new_cache,
528            &mut new_parent_map,
529            &mut new_children_map,
530        );
531
532        // Update all caches
533        {
534            let mut frames_cache = self.frames_cache.lock().await;
535            *frames_cache = new_cache;
536        }
537
538        {
539            let mut parent_map = self.frame_parent_map.lock().await;
540            *parent_map = new_parent_map;
541        }
542
543        {
544            let mut children_map = self.frame_children_map.lock().await;
545            *children_map = new_children_map;
546        }
547
548        {
549            let mut main_cache = self.main_frame_cache.lock().await;
550            if main_cache.is_none() && !all_frames.is_empty() {
551                *main_cache = Some(all_frames[0].clone());
552            }
553        }
554
555        Ok(all_frames)
556    }
557
558    /// Recursive helper to collect Frames (with hierarchy)
559    fn collect_frames_recursive(
560        frame_tree: &FrameTree,
561        parent_id: Option<String>,
562        page_arc: Arc<Page>,
563        frames_list: &mut Vec<Frame>,
564        frames_map: &mut HashMap<String, Frame>,
565        parent_map: &mut HashMap<String, String>,
566        children_map: &mut HashMap<String, Vec<String>>,
567    ) {
568        let frame_id = frame_tree.frame.id.clone();
569
570        // Create Frame object for current node
571        let current_frame = Frame::new(frame_id.clone(), Arc::clone(&page_arc));
572
573        // Add to list and Map
574        frames_list.push(current_frame.clone());
575        frames_map.insert(frame_id.clone(), current_frame);
576
577        // Record parent-child relationship
578        if let Some(ref parent) = parent_id {
579            parent_map.insert(frame_id.clone(), parent.clone());
580            children_map
581                .entry(parent.clone())
582                .or_default()
583                .push(frame_id.clone());
584        }
585
586        // If there are child Frames, recurse
587        if let Some(child_frames) = &frame_tree.child_frames {
588            for child_tree in child_frames {
589                Self::collect_frames_recursive(
590                    child_tree,
591                    Some(frame_id.clone()),
592                    Arc::clone(&page_arc),
593                    frames_list,
594                    frames_map,
595                    parent_map,
596                    children_map,
597                );
598            }
599        }
600    }
601
602    /// Get Frame by ID (from cache)
603    pub async fn get_frame(&self, frame_id: &str) -> Option<Frame> {
604        self.frames_cache.lock().await.get(frame_id).cloned()
605    }
606
607    /// Clear Frame cache (called after navigation)
608    pub async fn clear_frame_cache(&self) {
609        self.main_frame_cache.lock().await.take();
610        self.frames_cache.lock().await.clear();
611    }
612
613    /// Register Execution Context (called by event handler)
614    pub async fn register_execution_context(
615        &self,
616        frame_id: String,
617        context_id: u32,
618        unique_id: String,
619    ) {
620        self.contexts.lock().await.insert(
621            frame_id,
622            ExecutionContextInfo {
623                id: context_id,
624                unique_id,
625            },
626        );
627    }
628
629    /// Remove specified Execution Context (called by event handler)
630    pub async fn remove_execution_context(&self, unique_id: String) {
631        let mut contexts = self.contexts.lock().await;
632        // Find and remove entry with this unique_id
633        contexts.retain(|_frame_id, info| info.unique_id != unique_id);
634    }
635
636    /// Clear all Execution Contexts (called by event handler)
637    pub async fn clear_execution_contexts(&self) {
638        self.contexts.lock().await.clear();
639    }
640
641    // ========== Frame Hierarchy Query Methods ==========
642
643    /// Get parent Frame of specified Frame
644    pub async fn get_parent_frame(&self, frame_id: &str) -> Option<Frame> {
645        let parent_map = self.frame_parent_map.lock().await;
646        let parent_id = parent_map.get(frame_id)?;
647        self.get_frame(parent_id).await
648    }
649
650    /// Get all child Frames of specified Frame
651    pub async fn get_child_frames(&self, frame_id: &str) -> Vec<Frame> {
652        let children_map = self.frame_children_map.lock().await;
653        let child_ids = children_map.get(frame_id);
654
655        match child_ids {
656            Some(ids) => {
657                let mut children = Vec::new();
658                for id in ids {
659                    if let Some(frame) = self.get_frame(id).await {
660                        children.push(frame);
661                    }
662                }
663                children
664            }
665            None => Vec::new(),
666        }
667    }
668
669    /// Get all ancestor Frames of specified Frame (from near to far)
670    pub async fn get_ancestor_frames(&self, frame_id: &str) -> Vec<Frame> {
671        let mut ancestors = Vec::new();
672        let mut current_id = frame_id.to_string();
673
674        loop {
675            let parent = self.get_parent_frame(&current_id).await;
676            match parent {
677                Some(frame) => {
678                    current_id = frame.id.clone();
679                    ancestors.push(frame);
680                }
681                None => break,
682            }
683        }
684
685        ancestors
686    }
687
688    /// Get all descendant Frames of specified Frame (DFS)
689    pub async fn get_descendant_frames(&self, frame_id: &str) -> Vec<Frame> {
690        let mut descendants = Vec::new();
691        self.collect_descendants_recursive(frame_id, &mut descendants)
692            .await;
693        descendants
694    }
695
696    /// Recursively collect descendant Frames
697    fn collect_descendants_recursive<'a>(
698        &'a self,
699        frame_id: &'a str,
700        result: &'a mut Vec<Frame>,
701    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + 'a>> {
702        Box::pin(async move {
703            let children = self.get_child_frames(frame_id).await;
704            for child in children {
705                result.push(child.clone());
706                self.collect_descendants_recursive(&child.id, result).await;
707            }
708        })
709    }
710
711    // ========== Frame Selector Methods ==========
712
713    /// Find Frame by selector
714    ///
715    /// # Selector Syntax
716    /// - `name:iframe-name` - Match by name
717    /// - `url:https://example.com` - Match by URL prefix
718    /// - `url~pattern` - Match by URL regex
719    /// - `depth:2` - Match by depth (0 = Main Frame, 1 = Direct Child Frame)
720    ///
721    /// # Examples
722    /// ```no_run
723    /// # use cdp_core::Page;
724    /// # use std::sync::Arc;
725    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
726    /// // Find Frame named "login-iframe"
727    /// if let Some(frame) = page.query_frame("name:login-iframe").await? {
728    ///     println!("Found iframe: {}", frame.id());
729    /// }
730    ///
731    /// // Find Frame with URL containing "checkout"
732    /// if let Some(frame) = page.query_frame("url~checkout").await? {
733    ///     println!("Found checkout frame: {}", frame.id());
734    /// }
735    /// # Ok(())
736    /// # }
737    /// ```
738    pub async fn query_frame(self: &Arc<Self>, selector: &str) -> Result<Option<Frame>> {
739        let frames = self.all_frames().await?;
740
741        for frame in frames {
742            if self.matches_selector(&frame, selector).await? {
743                return Ok(Some(frame));
744            }
745        }
746
747        Ok(None)
748    }
749
750    /// Find all Frames matching selector
751    pub async fn query_frames(self: &Arc<Self>, selector: &str) -> Result<Vec<Frame>> {
752        let frames = self.all_frames().await?;
753        let mut matched = Vec::new();
754
755        for frame in frames {
756            if self.matches_selector(&frame, selector).await? {
757                matched.push(frame);
758            }
759        }
760
761        Ok(matched)
762    }
763
764    /// Check if Frame matches selector
765    async fn matches_selector(&self, frame: &Frame, selector: &str) -> Result<bool> {
766        if let Some(name_pattern) = selector.strip_prefix("name:") {
767            let frame_name = frame.name().await?;
768            return Ok(frame_name.as_deref() == Some(name_pattern));
769        }
770
771        if let Some(url_prefix) = selector.strip_prefix("url:") {
772            let frame_url = frame.url().await?;
773            return Ok(frame_url.starts_with(url_prefix));
774        }
775
776        if let Some(url_pattern) = selector.strip_prefix("url~") {
777            let frame_url = frame.url().await?;
778            // Simple pattern matching (can be enhanced with regex crate)
779            return Ok(frame_url.contains(url_pattern));
780        }
781
782        if let Some(depth_str) = selector.strip_prefix("depth:")
783            && let Ok(target_depth) = depth_str.parse::<usize>()
784        {
785            let ancestors = self.get_ancestor_frames(&frame.id).await;
786            return Ok(ancestors.len() == target_depth);
787        }
788
789        Ok(false)
790    }
791
792    // ========== Frame Lifecycle Callbacks ==========
793
794    /// Register Frame lifecycle callback
795    ///
796    /// # Examples
797    /// ```rust
798    /// use cdp_core::{Page, FrameLifecycleEvent};
799    /// use std::sync::Arc;
800    ///
801    /// # async fn example(page: Arc<Page>) {
802    /// page.on_frame_lifecycle(Arc::new(|event| {
803    ///     match event {
804    ///         FrameLifecycleEvent::Attached { frame_id, parent_frame_id } => {
805    ///             println!("Frame attached: {}", frame_id);
806    ///         }
807    ///         FrameLifecycleEvent::Detached { frame_id } => {
808    ///             println!("Frame detached: {}", frame_id);
809    ///         }
810    ///         FrameLifecycleEvent::Navigated { frame_id, url } => {
811    ///             println!("Frame navigated: {} to {}", frame_id, url);
812    ///         }
813    ///     }
814    /// })).await;
815    /// # }
816    /// ```
817    pub async fn on_frame_lifecycle(&self, callback: FrameLifecycleCallback) {
818        self.lifecycle_callbacks.lock().await.push(callback);
819    }
820
821    /// Trigger all registered lifecycle callbacks (internal use)
822    pub(crate) async fn trigger_lifecycle_event(&self, event: FrameLifecycleEvent) {
823        let callbacks = self.lifecycle_callbacks.lock().await;
824        for callback in callbacks.iter() {
825            callback(event.clone());
826        }
827    }
828
829    /// Handle Frame attached event (internal use)
830    pub(crate) async fn handle_frame_attached(
831        &self,
832        frame_id: String,
833        parent_frame_id: Option<String>,
834    ) {
835        // Update hierarchy
836        if let Some(ref parent_id) = parent_frame_id {
837            self.frame_parent_map
838                .lock()
839                .await
840                .insert(frame_id.clone(), parent_id.clone());
841
842            self.frame_children_map
843                .lock()
844                .await
845                .entry(parent_id.clone())
846                .or_insert_with(Vec::new)
847                .push(frame_id.clone());
848        }
849
850        // Trigger callback
851        self.trigger_lifecycle_event(FrameLifecycleEvent::Attached {
852            frame_id,
853            parent_frame_id,
854        })
855        .await;
856    }
857
858    /// Handle Frame detached event (internal use)
859    pub(crate) async fn handle_frame_detached(&self, frame_id: String) {
860        // Remove from cache
861        self.frames_cache.lock().await.remove(&frame_id);
862
863        // Remove hierarchy
864        let parent_id = self.frame_parent_map.lock().await.remove(&frame_id);
865        if let Some(parent) = parent_id {
866            let mut children_map = self.frame_children_map.lock().await;
867            if let Some(children) = children_map.get_mut(&parent) {
868                children.retain(|id| id != &frame_id);
869            }
870        }
871        self.frame_children_map.lock().await.remove(&frame_id);
872
873        // Trigger callback
874        self.trigger_lifecycle_event(FrameLifecycleEvent::Detached { frame_id })
875            .await;
876    }
877
878    /// Handle Frame navigated event (internal use)
879    pub(crate) async fn handle_frame_navigated(&self, frame_id: String, url: String) {
880        // Clear cache for this Frame
881        self.frames_cache.lock().await.remove(&frame_id);
882
883        // Trigger callback
884        self.trigger_lifecycle_event(FrameLifecycleEvent::Navigated { frame_id, url })
885            .await;
886    }
887
888    // ========== DOM Mutation Monitoring ==========
889
890    /// Register DOM mutation callback
891    ///
892    /// # Examples
893    /// ```rust
894    /// use cdp_core::{Page, DomMutationEvent};
895    /// use std::sync::Arc;
896    ///
897    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
898    /// // Enable DOM monitoring
899    /// page.enable_dom_mutations().await?;
900    ///
901    /// // Register callback
902    /// page.on_dom_mutation(Arc::new(|event| {
903    ///     match event {
904    ///         DomMutationEvent::ChildNodeInserted { parent_node_id, .. } => {
905    ///             println!("Child node inserted into {}", parent_node_id);
906    ///         }
907    ///         DomMutationEvent::AttributeModified { node_id, name, value } => {
908    ///             println!("Attribute {} = {} on node {}", name, value, node_id);
909    ///         }
910    ///         _ => {}
911    ///     }
912    /// })).await;
913    /// # Ok(())
914    /// # }
915    /// ```
916    pub async fn on_dom_mutation(&self, callback: DomMutationCallback) {
917        self.dom_mutation_callbacks.lock().await.push(callback);
918    }
919
920    /// Enable DOM mutation monitoring
921    pub async fn enable_dom_mutations(&self) -> Result<()> {
922        use cdp_protocol::dom;
923
924        // Enable DOM domain
925        let enable = dom::Enable {
926            include_whitespace: None,
927        };
928        self.session
929            .send_command::<_, dom::EnableReturnObject>(enable, None)
930            .await?;
931
932        println!("DOM mutation monitoring enabled");
933        Ok(())
934    }
935
936    /// Trigger all registered DOM mutation callbacks (internal use)
937    pub(crate) async fn trigger_dom_mutation_event(&self, event: DomMutationEvent) {
938        let callbacks = self.dom_mutation_callbacks.lock().await;
939        for callback in callbacks.iter() {
940            callback(event.clone());
941        }
942    }
943
944    /// Handle DOM child node inserted event (internal use)
945    pub(crate) async fn handle_child_node_inserted(
946        &self,
947        parent_node_id: u32,
948        previous_node_id: u32,
949        node: serde_json::Value,
950    ) {
951        self.trigger_dom_mutation_event(DomMutationEvent::ChildNodeInserted {
952            parent_node_id,
953            previous_node_id,
954            node,
955        })
956        .await;
957    }
958
959    /// Handle DOM child node removed event (internal use)
960    pub(crate) async fn handle_child_node_removed(&self, parent_node_id: u32, node_id: u32) {
961        self.trigger_dom_mutation_event(DomMutationEvent::ChildNodeRemoved {
962            parent_node_id,
963            node_id,
964        })
965        .await;
966    }
967
968    /// Handle DOM attribute modified event (internal use)
969    pub(crate) async fn handle_attribute_modified(
970        &self,
971        node_id: u32,
972        name: String,
973        value: String,
974    ) {
975        self.trigger_dom_mutation_event(DomMutationEvent::AttributeModified {
976            node_id,
977            name,
978            value,
979        })
980        .await;
981    }
982
983    /// Handle DOM attribute removed event (internal use)
984    pub(crate) async fn handle_attribute_removed(&self, node_id: u32, name: String) {
985        self.trigger_dom_mutation_event(DomMutationEvent::AttributeRemoved { node_id, name })
986            .await;
987    }
988
989    /// Handle character data modified event (internal use)
990    pub(crate) async fn handle_character_data_modified(
991        &self,
992        node_id: u32,
993        character_data: String,
994    ) {
995        self.trigger_dom_mutation_event(DomMutationEvent::CharacterDataModified {
996            node_id,
997            character_data,
998        })
999        .await;
1000    }
1001}
1002
1003// ========== Frame Snapshot Functionality ==========
1004
1005/// Frame snapshot data
1006#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1007pub struct FrameSnapshot {
1008    /// Frame ID
1009    pub frame_id: String,
1010    /// Frame URL
1011    pub url: String,
1012    /// Frame Name
1013    pub name: Option<String>,
1014    /// Parent Frame ID
1015    pub parent_frame_id: Option<String>,
1016    /// Child Frame IDs
1017    pub child_frame_ids: Vec<String>,
1018    /// Snapshot timestamp
1019    pub timestamp: u64,
1020    /// HTML content (optional)
1021    pub html_content: Option<String>,
1022    /// DOM tree depth
1023    pub dom_depth: Option<usize>,
1024}
1025
1026impl Page {
1027    /// Create Frame snapshot
1028    ///
1029    /// # Arguments
1030    /// * `frame_id` - Frame ID
1031    /// * `include_html` - Whether to include HTML content
1032    ///
1033    /// # Examples
1034    /// ```rust
1035    /// # use cdp_core::Page;
1036    /// # use std::sync::Arc;
1037    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
1038    /// let main_frame = page.main_frame().await?;
1039    ///
1040    /// // Create snapshot (without HTML)
1041    /// let snapshot = page.create_frame_snapshot(&main_frame.id, false).await?;
1042    /// println!("Snapshot: {:?}", snapshot);
1043    ///
1044    /// // Create snapshot (with HTML)
1045    /// let snapshot_with_html = page.create_frame_snapshot(&main_frame.id, true).await?;
1046    /// println!("HTML length: {}", snapshot_with_html.html_content.as_ref().map(|s| s.len()).unwrap_or(0));
1047    /// # Ok(())
1048    /// # }
1049    /// ```
1050    pub async fn create_frame_snapshot(
1051        &self,
1052        frame_id: &str,
1053        include_html: bool,
1054    ) -> Result<FrameSnapshot> {
1055        let frame = self
1056            .get_frame(frame_id)
1057            .await
1058            .ok_or_else(|| CdpError::page(format!("Frame not found: {}", frame_id)))?;
1059
1060        let url = frame.url().await.unwrap_or_else(|_| "".to_string());
1061        let name = frame.name().await.unwrap_or(None);
1062        let parent_frame_id = self.frame_parent_map.lock().await.get(frame_id).cloned();
1063        let child_frame_ids = self
1064            .frame_children_map
1065            .lock()
1066            .await
1067            .get(frame_id)
1068            .cloned()
1069            .unwrap_or_default();
1070
1071        let html_content = if include_html {
1072            match frame.evaluate("document.documentElement.outerHTML").await {
1073                Ok(html_value) => html_value.as_str().map(|s| s.to_string()),
1074                Err(_) => None,
1075            }
1076        } else {
1077            None
1078        };
1079
1080        let dom_depth = if include_html {
1081            match frame
1082                .evaluate(
1083                    r#"
1084                    (function() {
1085                        function getDepth(element) {
1086                            if (!element || !element.children) return 0;
1087                            let maxDepth = 0;
1088                            for (let child of element.children) {
1089                                maxDepth = Math.max(maxDepth, getDepth(child));
1090                            }
1091                            return maxDepth + 1;
1092                        }
1093                        return getDepth(document.documentElement);
1094                    })()
1095                "#,
1096                )
1097                .await
1098            {
1099                Ok(depth_value) => depth_value.as_u64().map(|d| d as usize),
1100                Err(_) => None,
1101            }
1102        } else {
1103            None
1104        };
1105
1106        let timestamp = std::time::SystemTime::now()
1107            .duration_since(std::time::UNIX_EPOCH)
1108            .unwrap_or_default()
1109            .as_secs();
1110
1111        Ok(FrameSnapshot {
1112            frame_id: frame_id.to_string(),
1113            url,
1114            name,
1115            parent_frame_id,
1116            child_frame_ids,
1117            timestamp,
1118            html_content,
1119            dom_depth,
1120        })
1121    }
1122
1123    /// Create snapshots for all Frames
1124    ///
1125    /// # Examples
1126    /// ```rust
1127    /// # use cdp_core::Page;
1128    /// # use std::sync::Arc;
1129    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
1130    /// let snapshots = page.create_all_frames_snapshot(false).await?;
1131    /// println!("Created {} snapshots", snapshots.len());
1132    ///
1133    /// // Save as JSON
1134    /// let json = serde_json::to_string_pretty(&snapshots)?;
1135    /// std::fs::write("frame_snapshots.json", json)?;
1136    /// # Ok(())
1137    /// # }
1138    /// ```
1139    pub async fn create_all_frames_snapshot(
1140        self: &Arc<Self>,
1141        include_html: bool,
1142    ) -> Result<Vec<FrameSnapshot>> {
1143        let frames = self.all_frames().await?;
1144        let mut snapshots = Vec::new();
1145
1146        for frame in frames {
1147            match self.create_frame_snapshot(&frame.id, include_html).await {
1148                Ok(snapshot) => snapshots.push(snapshot),
1149                Err(e) => {
1150                    eprintln!("Failed to create snapshot for frame {}: {}", frame.id, e);
1151                }
1152            }
1153        }
1154
1155        Ok(snapshots)
1156    }
1157
1158    /// Compare differences between two snapshots
1159    ///
1160    /// # Examples
1161    /// ```rust
1162    /// # use cdp_core::{Page, FrameSnapshot};
1163    /// # use std::sync::Arc;
1164    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
1165    /// let snapshot1 = page.create_all_frames_snapshot(false).await?;
1166    ///
1167    /// // Wait for some changes...
1168    /// tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
1169    ///
1170    /// let snapshot2 = page.create_all_frames_snapshot(false).await?;
1171    ///
1172    /// for (snap1, snap2) in snapshot1.iter().zip(snapshot2.iter()) {
1173    ///     let diff = Page::compare_snapshots(snap1, snap2);
1174    ///     println!("Frame {}: {} changes", snap1.frame_id, diff.len());
1175    /// }
1176    /// # Ok(())
1177    /// # }
1178    /// ```
1179    pub fn compare_snapshots(snapshot1: &FrameSnapshot, snapshot2: &FrameSnapshot) -> Vec<String> {
1180        let mut differences = Vec::new();
1181
1182        if snapshot1.frame_id != snapshot2.frame_id {
1183            differences.push(format!(
1184                "Frame ID changed: {} -> {}",
1185                snapshot1.frame_id, snapshot2.frame_id
1186            ));
1187        }
1188
1189        if snapshot1.url != snapshot2.url {
1190            differences.push(format!(
1191                "URL changed: {} -> {}",
1192                snapshot1.url, snapshot2.url
1193            ));
1194        }
1195
1196        if snapshot1.name != snapshot2.name {
1197            differences.push(format!(
1198                "Name changed: {:?} -> {:?}",
1199                snapshot1.name, snapshot2.name
1200            ));
1201        }
1202
1203        if snapshot1.parent_frame_id != snapshot2.parent_frame_id {
1204            differences.push(format!(
1205                "Parent Frame ID changed: {:?} -> {:?}",
1206                snapshot1.parent_frame_id, snapshot2.parent_frame_id
1207            ));
1208        }
1209
1210        if snapshot1.child_frame_ids != snapshot2.child_frame_ids {
1211            differences.push(format!(
1212                "Child Frames changed: {} -> {}",
1213                snapshot1.child_frame_ids.len(),
1214                snapshot2.child_frame_ids.len()
1215            ));
1216        }
1217
1218        if let (Some(html1), Some(html2)) = (&snapshot1.html_content, &snapshot2.html_content)
1219            && html1 != html2
1220        {
1221            differences.push(format!(
1222                "HTML content changed: {} bytes -> {} bytes",
1223                html1.len(),
1224                html2.len()
1225            ));
1226        }
1227
1228        if snapshot1.dom_depth != snapshot2.dom_depth {
1229            differences.push(format!(
1230                "DOM depth changed: {:?} -> {:?}",
1231                snapshot1.dom_depth, snapshot2.dom_depth
1232            ));
1233        }
1234
1235        differences
1236    }
1237
1238    /// Type text into current focused element (insert all text at once)
1239    ///
1240    /// This method uses CDP's `Input.insertText` command to insert all text at once.
1241    /// Suitable for fast input, but does not simulate real user character-by-character input.
1242    ///
1243    /// # Parameters
1244    /// * `text` - Text to type
1245    ///
1246    /// # Examples
1247    /// ```no_run
1248    /// # use cdp_core::Page;
1249    /// # use std::sync::Arc;
1250    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
1251    /// // Focus on input box first
1252    /// if let Some(input) = page.query_selector("input[type='text']").await? {
1253    ///     input.click().await?;
1254    /// }
1255    ///
1256    /// // Fast type text
1257    /// page.type_text("Hello, World!").await?;
1258    /// # Ok(())
1259    /// # }
1260    /// ```
1261    pub async fn type_text(&self, text: &str) -> Result<()> {
1262        self.keyboard().insert_text(text).await
1263    }
1264
1265    /// Type text into current focused element character by character, with random delay between each character
1266    ///
1267    /// This method simulates real user input behavior, each character triggers keyDown, keyUp events.
1268    /// Delay time is randomly generated within `[min_delay_ms, max_delay_ms]`.
1269    ///
1270    /// # Parameters
1271    /// * `text` - Text to type
1272    /// * `min_delay_ms` - Minimum delay (ms)
1273    /// * `max_delay_ms` - Maximum delay (ms)
1274    ///
1275    /// # Examples
1276    /// ```no_run
1277    /// # use cdp_core::Page;
1278    /// # use std::sync::Arc;
1279    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
1280    /// // Focus on input box first
1281    /// if let Some(input) = page.query_selector("input[type='text']").await? {
1282    ///     input.click().await?;
1283    /// }
1284    ///
1285    /// // Type with random delay
1286    /// page.type_text_with_delay("Hello, World!", 50, 150).await?;
1287    /// # Ok(())
1288    /// # }
1289    /// ```
1290    pub async fn type_text_with_delay(
1291        &self,
1292        text: &str,
1293        min_delay_ms: u64,
1294        max_delay_ms: u64,
1295    ) -> Result<()> {
1296        self.keyboard()
1297            .type_text_with_delay(text, min_delay_ms, max_delay_ms)
1298            .await
1299    }
1300
1301    /// Get device pixel ratio (DPR) of current page
1302    ///
1303    /// # Returns
1304    /// Device pixel ratio, usually 1.0, 1.5, 2.0, 3.0 etc.
1305    async fn get_device_pixel_ratio(&self) -> Result<f64> {
1306        let eval_result = self
1307            .session
1308            .send_command::<_, EvaluateReturnObject>(
1309                Evaluate {
1310                    expression: "window.devicePixelRatio".to_string(),
1311                    object_group: None,
1312                    include_command_line_api: None,
1313                    silent: None,
1314                    context_id: None,
1315                    return_by_value: Some(true),
1316                    generate_preview: None,
1317                    user_gesture: None,
1318                    await_promise: None,
1319                    throw_on_side_effect: None,
1320                    timeout: None,
1321                    disable_breaks: None,
1322                    repl_mode: None,
1323                    allow_unsafe_eval_blocked_by_csp: None,
1324                    unique_context_id: None,
1325                    serialization_options: None,
1326                },
1327                None,
1328            )
1329            .await?;
1330
1331        let dpr = eval_result
1332            .result
1333            .value
1334            .and_then(|v| v.as_f64())
1335            .unwrap_or(1.0);
1336
1337        Ok(dpr)
1338    }
1339
1340    /// Takes a screenshot of the page.
1341    ///
1342    /// Takes a screenshot of the page.
1343    ///
1344    /// # Parameters
1345    /// * `full_page` - Whether to capture full page (including scroll area). If false, only capture current viewport
1346    /// * `save_path` - Optional save path (including filename). If None, save to current directory with name `screenshot_timestamp.png`
1347    ///
1348    /// # Returns
1349    /// Saved file path
1350    ///
1351    /// # Examples
1352    /// ```no_run
1353    /// # use cdp_core::Page;
1354    /// # use std::sync::Arc;
1355    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
1356    /// // Capture current viewport and save automatically
1357    /// let path = page.screenshot(false, None).await?;
1358    /// println!("Screenshot saved to: {}", path);
1359    ///
1360    /// // Capture full page and save to specified path
1361    /// let path = page.screenshot(true, Some("screenshots/fullpage.png".into())).await?;
1362    /// println!("Full page screenshot saved to: {}", path);
1363    /// # Ok(())
1364    /// # }
1365    /// ```
1366    pub async fn screenshot(&self, full_page: bool, save_path: Option<PathBuf>) -> Result<String> {
1367        self.screenshot_with_options(full_page, save_path, true)
1368            .await
1369    }
1370
1371    /// Takes a screenshot of the page with custom options.
1372    ///
1373    /// Takes a screenshot of the page with custom options.
1374    ///
1375    /// # Parameters
1376    /// * `full_page` - Whether to capture full page (including scroll area). If false, only capture current viewport
1377    /// * `save_path` - Optional save path (including filename). If None, save to current directory with name `screenshot_timestamp.png`
1378    /// * `auto_resolve_dpr` - Whether to automatically adapt to device pixel ratio. If true, automatically detect and use actual DPR to avoid screenshot being too large or distorted
1379    ///
1380    /// # Returns
1381    /// Saved file path
1382    ///
1383    /// # Examples
1384    /// ```no_run
1385    /// # use cdp_core::Page;
1386    /// # use std::sync::Arc;
1387    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
1388    /// // Capture full page, do not adapt DPR (use fixed 1.0, compatible with old behavior)
1389    /// let path = page.screenshot_with_options(true, None, false).await?;
1390    ///
1391    /// // Capture full page, adapt DPR (recommended, avoid screenshot being too large)
1392    /// let path = page.screenshot_with_options(true, None, true).await?;
1393    /// # Ok(())
1394    /// # }
1395    /// ```
1396    pub async fn screenshot_with_options(
1397        &self,
1398        full_page: bool,
1399        save_path: Option<PathBuf>,
1400        auto_resolve_dpr: bool,
1401    ) -> Result<String> {
1402        use base64::Engine;
1403        use cdp_protocol::page as page_cdp;
1404
1405        // 1. Get device pixel ratio
1406        let device_scale = if auto_resolve_dpr {
1407            let dpr = self.get_device_pixel_ratio().await?;
1408            // Ensure DPR is within reasonable range (0.5 to 3.0)
1409            dpr.clamp(0.5, 3.0)
1410        } else {
1411            1.0
1412        };
1413
1414        let mut clip: Option<Viewport> = None;
1415        let layout_metrics_params = page_cdp::GetLayoutMetrics(None);
1416        let layout_metrics: page_cdp::GetLayoutMetricsReturnObject = self
1417            .session
1418            .send_command(layout_metrics_params, None)
1419            .await?;
1420        let visual_viewport_scale = layout_metrics.css_visual_viewport.scale;
1421        let device_scale_factor = device_scale.max(visual_viewport_scale);
1422        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1423
1424        if let Some(mut c) = clip.as_ref().cloned() {
1425            let dpr = c.scale.max(1.0);
1426            let to_device = |v: f64| (v * dpr).round();
1427            let x_dev = to_device(c.x);
1428            let y_dev = to_device(c.y);
1429            let x_px = (x_dev.floor() - 1.0).max(0.0);
1430            let y_px = (y_dev.floor() - 1.0).max(0.0);
1431            let right_px = to_device(c.x + c.width).ceil();
1432            let bottom_px = to_device(c.y + c.height).ceil();
1433            let w_px = (right_px - x_px).max(1.0);
1434            let h_px = (bottom_px - y_px).max(1.0);
1435            c.x = x_px / dpr;
1436            c.y = y_px / dpr;
1437            c.width = w_px / dpr;
1438            c.height = h_px / dpr;
1439            c.scale = device_scale_factor;
1440            clip = Some(c);
1441        }
1442
1443        // 2. Capture screenshot
1444        let screenshot_params = page_cdp::CaptureScreenshot {
1445            format: Some(page_cdp::CaptureScreenshotFormatOption::Png),
1446            quality: None,
1447            clip,
1448            from_surface: Some(true),
1449            capture_beyond_viewport: Some(full_page),
1450            optimize_for_speed: None,
1451        };
1452
1453        let result: page_cdp::CaptureScreenshotReturnObject =
1454            self.session.send_command(screenshot_params, None).await?;
1455
1456        // 4. Generate save path
1457        let out_path_buf: std::path::PathBuf = match save_path {
1458            Some(pv) => {
1459                if pv.parent().is_none_or(|p| p.as_os_str().is_empty()) {
1460                    std::env::current_dir()?.join(pv)
1461                } else {
1462                    if let Some(parent) = pv.parent() {
1463                        std::fs::create_dir_all(parent)?;
1464                    }
1465                    pv.to_path_buf()
1466                }
1467            }
1468            None => {
1469                let out_dir = std::env::var("OUT_PATH").unwrap_or_else(|_| ".".to_string());
1470                let dir = std::path::PathBuf::from(out_dir);
1471                std::fs::create_dir_all(&dir)?;
1472                let nanos = std::time::SystemTime::now()
1473                    .duration_since(std::time::UNIX_EPOCH)
1474                    .unwrap_or_default()
1475                    .as_nanos();
1476                dir.join(format!("screenshot-{}.png", nanos))
1477            }
1478        };
1479
1480        // 3. Decode and save file
1481        let bytes = base64::engine::general_purpose::STANDARD
1482            .decode(&result.data)
1483            .map_err(|err| {
1484                CdpError::page(format!("Failed to decode screenshot response: {}", err))
1485            })?;
1486        let mut f = File::create(&out_path_buf).await?;
1487        f.write_all(&bytes).await?;
1488        f.flush().await?;
1489
1490        let out_path = out_path_buf.to_string_lossy();
1491        Ok(out_path.into_owned())
1492    }
1493
1494    // ========== Wait Functionality ==========
1495
1496    /// Wait for element matching selector to appear
1497    ///
1498    /// # Parameters
1499    /// * `selector` - CSS selector or XPath expression
1500    /// * `options` - Wait options (timeout, visibility, etc.)
1501    ///
1502    /// # Returns
1503    /// Matching element handle
1504    ///
1505    /// # Examples
1506    /// ```no_run
1507    /// # use cdp_core::{Page, WaitForSelectorOptions};
1508    /// # use std::sync::Arc;
1509    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
1510    /// // Wait for element to appear (default 30s timeout)
1511    /// let button = page.wait_for_selector("#submit-btn", None).await?;
1512    ///
1513    /// // Custom timeout and visibility requirements
1514    /// let element = page.wait_for_selector(
1515    ///     ".dynamic-content",
1516    ///     Some(WaitForSelectorOptions {
1517    ///         timeout_ms: Some(5000),
1518    ///         visible: Some(true),
1519    ///         hidden: Some(false),
1520    ///     })
1521    /// ).await?;
1522    /// # Ok(())
1523    /// # }
1524    /// ```
1525    pub async fn wait_for_selector(
1526        self: &Arc<Self>,
1527        selector: &str,
1528        options: Option<WaitForSelectorOptions>,
1529    ) -> Result<ElementHandle> {
1530        let opts = options.unwrap_or_default();
1531        let timeout_ms = opts.timeout_ms.unwrap_or(30000);
1532        let visible = opts.visible.unwrap_or(false);
1533        let hidden = opts.hidden.unwrap_or(false);
1534
1535        let start = std::time::Instant::now();
1536        let poll_interval = std::time::Duration::from_millis(100);
1537
1538        loop {
1539            // Check timeout
1540            if start.elapsed().as_millis() > timeout_ms as u128 {
1541                return Err(CdpError::page(format!(
1542                    "Timeout waiting for selector '{}' ({}ms)",
1543                    selector, timeout_ms
1544                )));
1545            }
1546
1547            // Try to query element
1548            if let Some(element) = self.query_selector(selector).await? {
1549                // If visibility check is needed
1550                if visible
1551                    && let Ok(is_visible) = element.is_visible().await
1552                    && !is_visible
1553                {
1554                    tokio::time::sleep(poll_interval).await;
1555                    continue;
1556                }
1557
1558                // If hidden check is needed
1559                if hidden
1560                    && let Ok(is_visible) = element.is_visible().await
1561                    && is_visible
1562                {
1563                    tokio::time::sleep(poll_interval).await;
1564                    continue;
1565                }
1566
1567                return Ok(element);
1568            }
1569
1570            tokio::time::sleep(poll_interval).await;
1571        }
1572    }
1573
1574    /// Wait for element matching selector to disappear or be hidden
1575    ///
1576    /// # Parameters
1577    /// * `selector` - CSS selector or XPath expression
1578    /// * `timeout_ms` - Timeout in milliseconds, default 30000
1579    ///
1580    /// # Examples
1581    /// ```no_run
1582    /// # use cdp_core::Page;
1583    /// # use std::sync::Arc;
1584    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
1585    /// // Wait for loading spinner to disappear
1586    /// page.wait_for_selector_hidden(".loading-spinner", None).await?;
1587    /// # Ok(())
1588    /// # }
1589    /// ```
1590    pub async fn wait_for_selector_hidden(
1591        self: &Arc<Self>,
1592        selector: &str,
1593        timeout_ms: Option<u64>,
1594    ) -> Result<()> {
1595        let timeout = timeout_ms.unwrap_or(30000);
1596        let start = std::time::Instant::now();
1597        let poll_interval = std::time::Duration::from_millis(100);
1598
1599        loop {
1600            if start.elapsed().as_millis() > timeout as u128 {
1601                return Err(CdpError::page(format!(
1602                    "Timeout waiting for selector '{}' to be hidden ({}ms)",
1603                    selector, timeout
1604                )));
1605            }
1606
1607            match self.query_selector(selector).await? {
1608                None => return Ok(()), // Element does not exist, considered hidden
1609                Some(element) => {
1610                    // Check if element is visible
1611                    if let Ok(is_visible) = element.is_visible().await
1612                        && !is_visible
1613                    {
1614                        return Ok(());
1615                    }
1616                }
1617            }
1618
1619            tokio::time::sleep(poll_interval).await;
1620        }
1621    }
1622
1623    /// Wait for custom function to return true
1624    ///
1625    /// # Parameters
1626    /// * `function` - JavaScript function string, should return boolean
1627    /// * `timeout_ms` - Timeout in milliseconds, default 30000
1628    /// * `poll_interval_ms` - Polling interval (ms), default 100
1629    ///
1630    /// # Examples
1631    /// ```no_run
1632    /// # use cdp_core::Page;
1633    /// # use std::sync::Arc;
1634    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
1635    /// // Wait for page title to change
1636    /// page.wait_for_function(
1637    ///     "() => document.title === 'Loaded'",
1638    ///     Some(5000),
1639    ///     None
1640    /// ).await?;
1641    ///
1642    /// // Wait for global variable
1643    /// page.wait_for_function(
1644    ///     "() => window.myApp && window.myApp.ready",
1645    ///     None,
1646    ///     None
1647    /// ).await?;
1648    /// # Ok(())
1649    /// # }
1650    /// ```
1651    pub async fn wait_for_function(
1652        self: &Arc<Self>,
1653        function: &str,
1654        timeout_ms: Option<u64>,
1655        poll_interval_ms: Option<u64>,
1656    ) -> Result<()> {
1657        let timeout = timeout_ms.unwrap_or(30000);
1658        let poll_interval = std::time::Duration::from_millis(poll_interval_ms.unwrap_or(100));
1659        let start = std::time::Instant::now();
1660
1661        // Construct wrapper script: supports passing function or expression
1662        let script = format!(
1663            r#"
1664                (() => {{
1665                    try {{
1666                        const candidate = ({});
1667                        const value = typeof candidate === 'function' ? candidate() : candidate;
1668                        if (value && typeof value.then === 'function') {{
1669                            return value.then(Boolean).catch(() => false);
1670                        }}
1671                        return Boolean(value);
1672                    }} catch (error) {{
1673                        return false;
1674                    }}
1675                }})()
1676            "#,
1677            function
1678        );
1679
1680        loop {
1681            if start.elapsed().as_millis() > timeout as u128 {
1682                return Err(CdpError::page(format!(
1683                    "Timeout waiting for function ({}ms)",
1684                    timeout
1685                )));
1686            }
1687
1688            // Execute function
1689            let main_frame = match self.main_frame().await {
1690                Ok(frame) => frame,
1691                Err(_) => {
1692                    tokio::time::sleep(poll_interval).await;
1693                    continue;
1694                }
1695            };
1696
1697            match main_frame.evaluate(&script).await {
1698                Ok(result) => {
1699                    if let Some(true) = result.as_bool() {
1700                        return Ok(());
1701                    }
1702                }
1703                Err(_) => {
1704                    // Execution context may not be ready, retry later
1705                }
1706            }
1707
1708            tokio::time::sleep(poll_interval).await;
1709        }
1710    }
1711
1712    /// Wait for page navigation to complete
1713    ///
1714    /// Supports multiple wait conditions:
1715    /// - **Load**: Wait for `load` event (all resources loaded)
1716    /// - **DOMContentLoaded**: Wait for DOM tree construction to complete
1717    /// - **NetworkIdle0**: Wait for network to be fully idle (no requests for 500ms)
1718    /// - **NetworkIdle2**: Wait for network to be almost idle (<= 2 requests for 500ms)
1719    ///
1720    /// # Parameters
1721    /// * `options` - Navigation wait options (timeout, wait condition, etc.)
1722    ///
1723    /// # Examples
1724    /// ```no_run
1725    /// # use cdp_core::{Page, WaitForNavigationOptions, WaitUntil};
1726    /// # use std::sync::Arc;
1727    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
1728    /// // Wait for load event (default)
1729    /// page.wait_for_navigation(None).await?;
1730    ///
1731    /// // Wait for DOMContentLoaded (faster)
1732    /// page.wait_for_navigation(Some(WaitForNavigationOptions {
1733    ///     timeout_ms: Some(5000),
1734    ///     wait_until: Some(WaitUntil::DOMContentLoaded),
1735    /// })).await?;
1736    ///
1737    /// // Wait for network idle (suitable for SPA)
1738    /// page.wait_for_navigation(Some(WaitForNavigationOptions {
1739    ///     timeout_ms: Some(10000),
1740    ///     wait_until: Some(WaitUntil::NetworkIdle2),
1741    /// })).await?;
1742    /// # Ok(())
1743    /// # }
1744    /// ```
1745    pub async fn wait_for_navigation(
1746        self: &Arc<Self>,
1747        options: Option<WaitForNavigationOptions>,
1748    ) -> Result<()> {
1749        let opts = options.unwrap_or_default();
1750        let timeout = opts.timeout_ms.unwrap_or(60000);
1751        let wait_until = opts.wait_until.unwrap_or(WaitUntil::NetworkIdle2);
1752
1753        match wait_until {
1754            WaitUntil::Load => {
1755                // Wait for load event
1756                self.wait_for_function(
1757                    "() => document.readyState === 'complete'",
1758                    Some(timeout),
1759                    None,
1760                )
1761                .await
1762            }
1763            WaitUntil::DOMContentLoaded => {
1764                // Wait for DOMContentLoaded event
1765                self.wait_for_function(
1766                    "() => document.readyState === 'interactive'",
1767                    Some(timeout),
1768                    None,
1769                )
1770                .await
1771            }
1772            WaitUntil::NetworkIdle0 => {
1773                // Wait for network fully idle
1774                self.wait_for_network_idle(timeout, 0).await
1775            }
1776            WaitUntil::NetworkIdle2 => {
1777                // Wait for network almost idle
1778                self.wait_for_network_idle(timeout, 2).await
1779            }
1780        }
1781    }
1782
1783    /// Register network event callback
1784    ///
1785    /// # Examples
1786    /// ```ignore
1787    /// # use cdp_core::{Page, NetworkEvent};
1788    /// # use std::sync::Arc;
1789    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
1790    /// page.enable_network_monitoring().await?;
1791    ///
1792    /// page.on_network(Arc::new(|event| {
1793    ///     match event {
1794    ///         NetworkEvent::RequestWillBeSent { url, method, .. } => {
1795    ///             println!("[{}] {}", method, url);
1796    ///         }
1797    ///         NetworkEvent::ResponseReceived { request_id, status, .. } => {
1798    ///             println!("[{}] Status: {}", request_id, status);
1799    ///         }
1800    ///         NetworkEvent::LoadingFailed { request_id, error_text } => {
1801    ///             eprintln!("[{}] Failed: {}", request_id, error_text);
1802    ///         }
1803    ///         _ => {}
1804    ///     }
1805    /// })).await;
1806    /// # Ok(())
1807    /// # }
1808    /// ```
1809    pub async fn on_network(&self, callback: NetworkEventCallback) {
1810        self.network_monitor.add_callback(callback).await;
1811    }
1812
1813    /// Get count of currently active network requests
1814    ///
1815    /// # Examples
1816    /// ```ignore
1817    /// # use cdp_core::Page;
1818    /// # use std::sync::Arc;
1819    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
1820    /// page.enable_network_monitoring().await?;
1821    ///
1822    /// let count = page.get_inflight_requests_count();
1823    /// println!("Active requests: {}", count);
1824    /// # Ok(())
1825    /// # }
1826    /// ```
1827    pub fn get_inflight_requests_count(&self) -> usize {
1828        self.network_monitor.get_inflight_count()
1829    }
1830
1831    /// Wait for network idle
1832    ///
1833    /// # Parameters
1834    /// * `timeout_ms` - Timeout (ms)
1835    /// * `max_inflight` - Maximum allowed concurrent requests (0 = fully idle, 2 = almost idle)
1836    ///
1837    /// # Implementation Details
1838    /// This method listens for network request events, considering network idle when concurrent requests <= max_inflight for at least 500ms
1839    async fn wait_for_network_idle(
1840        self: &Arc<Self>,
1841        timeout_ms: u64,
1842        max_inflight: usize,
1843    ) -> Result<()> {
1844        // Ensure network monitoring is enabled
1845        if !self.domain_manager.is_enabled(DomainType::Network).await {
1846            self.domain_manager.enable_network_domain().await?;
1847        }
1848
1849        // Reset count (avoid influence from previous requests)
1850        self.network_monitor.reset_inflight().await;
1851
1852        // Wait for network idle
1853        let start = std::time::Instant::now();
1854        let mut idle_start: Option<std::time::Instant> = None;
1855        let idle_duration = std::time::Duration::from_millis(500); // 500ms idle time
1856
1857        loop {
1858            if start.elapsed().as_millis() > timeout_ms as u128 {
1859                return Ok(());
1860            }
1861
1862            let current_inflight = self.network_monitor.get_inflight_count();
1863            tracing::debug!("Current active requests: {}", current_inflight);
1864
1865            if current_inflight <= max_inflight {
1866                // Network is idle, ensure timer exists
1867                let start_time = idle_start.get_or_insert_with(std::time::Instant::now);
1868
1869                // Check if idle for enough time
1870                if start_time.elapsed() >= idle_duration {
1871                    return Ok(());
1872                }
1873            } else {
1874                // Network not idle, reset timer
1875                idle_start = None;
1876            }
1877
1878            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1879        }
1880    }
1881
1882    /// Wait for specified time (ms)
1883    ///
1884    /// # Examples
1885    /// ```no_run
1886    /// # use cdp_core::Page;
1887    /// # use std::sync::Arc;
1888    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
1889    /// // Wait for 2 seconds
1890    /// page.wait_for_timeout(2000).await;
1891    /// # Ok(())
1892    /// # }
1893    /// ```
1894    pub async fn wait_for_timeout(&self, ms: u64) {
1895        tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
1896    }
1897
1898    /// Clean up Page resources (explicit async cleanup)
1899    ///
1900    /// This method will:
1901    /// 1. Clear all network monitors and interceptors
1902    /// 2. Disable all enabled CDP Domains
1903    /// 3. Clear Frame cache and callbacks
1904    ///
1905    /// # Best Practices
1906    ///
1907    /// Although Page automatically cleans up on Drop, it is recommended to call this method explicitly when Page is no longer used:
1908    ///
1909    /// ```no_run
1910    /// # use cdp_core::Browser;
1911    /// # async fn example() -> anyhow::Result<()> {
1912    /// let browser = Browser::launcher().launch().await?;
1913    /// let page = browser.new_page().await?;
1914    ///
1915    /// // Use page...
1916    ///
1917    /// // Explicitly cleanup when no longer used
1918    /// page.cleanup().await?;
1919    /// # Ok(())
1920    /// # }
1921    /// ```
1922    ///
1923    /// # Note
1924    ///
1925    /// - After calling this method, Page will be in an unusable state
1926    /// - If Page is shared via `Arc`, ensure no other places are still using it
1927    /// - Cleanup is also performed automatically on Drop, but explicit call gives better control over timing and error handling
1928    pub async fn cleanup(&self) -> Result<()> {
1929        tracing::info!("Starting Page resource cleanup...");
1930
1931        // 1. Clear network monitors
1932        self.response_monitor_manager.clear_monitors().await;
1933        tracing::debug!("Network monitors cleared");
1934
1935        // 2. Clear callbacks
1936        self.lifecycle_callbacks.lock().await.clear();
1937        self.dom_mutation_callbacks.lock().await.clear();
1938        tracing::debug!("Lifecycle and DOM mutation callbacks cleared");
1939
1940        // 3. Clear Frame cache
1941        self.main_frame_cache.lock().await.take();
1942        self.frames_cache.lock().await.clear();
1943        self.frame_parent_map.lock().await.clear();
1944        self.frame_children_map.lock().await.clear();
1945        tracing::debug!("Frame cache cleared");
1946
1947        // 4. Disable all CDP Domains
1948        self.domain_manager.disable_all_domains().await?;
1949        tracing::debug!("All CDP Domains disabled");
1950
1951        tracing::info!("Page resource cleanup completed");
1952        Ok(())
1953    }
1954}
1955
1956/// RAII automatic cleanup implementation
1957///
1958/// Automatically cleans up all resources when Page is dropped
1959impl Drop for Page {
1960    fn drop(&mut self) {
1961        tracing::debug!("Page Drop triggered, starting automatic cleanup...");
1962
1963        // Since Drop is synchronous and our cleanup is asynchronous,
1964        // we need to execute cleanup in the current tokio runtime
1965        if let Ok(handle) = tokio::runtime::Handle::try_current() {
1966            // Spawn a task in tokio runtime to execute cleanup
1967            let domain_manager = Arc::clone(&self.domain_manager);
1968            let response_monitor_manager = Arc::clone(&self.response_monitor_manager);
1969            let lifecycle_callbacks = Arc::clone(&self.lifecycle_callbacks);
1970            let dom_mutation_callbacks = Arc::clone(&self.dom_mutation_callbacks);
1971            let main_frame_cache = Arc::clone(&self.main_frame_cache);
1972            let frames_cache = Arc::clone(&self.frames_cache);
1973            let frame_parent_map = Arc::clone(&self.frame_parent_map);
1974            let frame_children_map = Arc::clone(&self.frame_children_map);
1975
1976            handle.spawn(async move {
1977                // Clear network monitors
1978                response_monitor_manager.clear_monitors().await;
1979
1980                // Clear callbacks
1981                lifecycle_callbacks.lock().await.clear();
1982                dom_mutation_callbacks.lock().await.clear();
1983
1984                // Clear Frame cache
1985                main_frame_cache.lock().await.take();
1986                frames_cache.lock().await.clear();
1987                frame_parent_map.lock().await.clear();
1988                frame_children_map.lock().await.clear();
1989
1990                // Disable all Domains
1991                if let Err(e) = domain_manager.disable_all_domains().await {
1992                    tracing::warn!("Failed to disable Domains during Page Drop: {:?}", e);
1993                }
1994
1995                tracing::debug!("Page automatic cleanup completed");
1996            });
1997        } else {
1998            // If not in tokio runtime, just print warning
1999            tracing::warn!("Cannot perform async cleanup during Page Drop (not in tokio runtime)");
2000            tracing::warn!("Recommended to call page.cleanup().await explicitly");
2001        }
2002    }
2003}