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