chromiumoxide/handler/
frame.rs

1use std::collections::VecDeque;
2use std::collections::{HashMap, HashSet};
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5
6use serde_json::map::Entry;
7
8use chromiumoxide_cdp::cdp::browser_protocol::network::LoaderId;
9use chromiumoxide_cdp::cdp::browser_protocol::page::{
10    AddScriptToEvaluateOnNewDocumentParams, CreateIsolatedWorldParams, EventFrameDetached,
11    EventFrameStartedLoading, EventFrameStoppedLoading, EventLifecycleEvent,
12    EventNavigatedWithinDocument, Frame as CdpFrame, FrameTree,
13};
14use chromiumoxide_cdp::cdp::browser_protocol::target::EventAttachedToTarget;
15use chromiumoxide_cdp::cdp::js_protocol::runtime::*;
16use chromiumoxide_cdp::cdp::{
17    browser_protocol::page::{self, FrameId},
18    // js_protocol::runtime,
19};
20use chromiumoxide_types::{Method, MethodId, Request};
21
22use crate::error::DeadlineExceeded;
23use crate::handler::domworld::DOMWorld;
24use crate::handler::http::HttpRequest;
25use crate::handler::REQUEST_TIMEOUT;
26use crate::{cmd::CommandChain, ArcHttpRequest};
27
28const EVALUATION_SCRIPT_URL: &str = "____chromiumoxide_utility_world___evaluation_script__";
29
30// lazy_static::lazy_static! {
31//     /// Spoof the runtime.
32//     static ref CHROME_SPOOF_RUNTIME: bool = {
33//         std::env::var("CHROME_SPOOF_RUNTIME").unwrap_or_else(|_| "false".to_string()) == "true"
34//     };
35// }
36
37/// Generate a collision-resistant world name using `id` + randomness.
38pub fn random_world_name(id: &str) -> String {
39    use rand::Rng;
40    let mut rng = rand::thread_rng();
41    let rand_len = rng.gen_range(6..=12);
42
43    // Convert first few chars of id into base36-compatible chars
44    let id_part: String = id
45        .chars()
46        .filter(|c| c.is_ascii_alphanumeric())
47        .take(5)
48        .map(|c| {
49            let c = c.to_ascii_lowercase();
50            if c.is_ascii_alphabetic() {
51                c
52            } else {
53                // convert 0-9 into a base36 letter offset to obscure it a bit
54                (b'a' + (c as u8 - b'0') % 26) as char
55            }
56        })
57        .collect();
58
59    // Generate random base36 tail
60    let rand_part: String = (0..rand_len)
61        .filter_map(|_| std::char::from_digit(rng.gen_range(0..36), 36))
62        .collect();
63
64    // Ensure first char is always a letter (10–35 => a–z)
65    let first = std::char::from_digit(rng.gen_range(10..36), 36).unwrap_or('a');
66
67    format!("{first}{id_part}{rand_part}")
68}
69
70/// Represents a frame on the page
71#[derive(Debug)]
72pub struct Frame {
73    parent_frame: Option<FrameId>,
74    /// Cdp identifier of this frame
75    id: FrameId,
76    main_world: DOMWorld,
77    secondary_world: DOMWorld,
78    loader_id: Option<LoaderId>,
79    /// Current url of this frame
80    url: Option<String>,
81    /// The http request that loaded this with this frame
82    http_request: ArcHttpRequest,
83    /// The frames contained in this frame
84    child_frames: HashSet<FrameId>,
85    name: Option<String>,
86    /// The received lifecycle events
87    lifecycle_events: HashSet<MethodId>,
88    isolated_world_name: String,
89}
90
91impl Frame {
92    pub fn new(id: FrameId) -> Self {
93        let isolated_world_name = random_world_name(id.inner());
94
95        Self {
96            parent_frame: None,
97            id,
98            main_world: Default::default(),
99            secondary_world: Default::default(),
100            loader_id: None,
101            url: None,
102            http_request: None,
103            child_frames: Default::default(),
104            name: None,
105            lifecycle_events: Default::default(),
106            isolated_world_name,
107        }
108    }
109
110    pub fn with_parent(id: FrameId, parent: &mut Frame) -> Self {
111        parent.child_frames.insert(id.clone());
112        Self {
113            parent_frame: Some(parent.id.clone()),
114            id,
115            main_world: Default::default(),
116            secondary_world: Default::default(),
117            loader_id: None,
118            url: None,
119            http_request: None,
120            child_frames: Default::default(),
121            name: None,
122            lifecycle_events: Default::default(),
123            isolated_world_name: parent.isolated_world_name.clone(),
124        }
125    }
126
127    pub fn get_isolated_world_name(&self) -> &String {
128        &self.isolated_world_name
129    }
130
131    pub fn parent_id(&self) -> Option<&FrameId> {
132        self.parent_frame.as_ref()
133    }
134
135    pub fn id(&self) -> &FrameId {
136        &self.id
137    }
138
139    pub fn url(&self) -> Option<&str> {
140        self.url.as_deref()
141    }
142
143    pub fn name(&self) -> Option<&str> {
144        self.name.as_deref()
145    }
146
147    pub fn main_world(&self) -> &DOMWorld {
148        &self.main_world
149    }
150
151    pub fn secondary_world(&self) -> &DOMWorld {
152        &self.secondary_world
153    }
154
155    pub fn lifecycle_events(&self) -> &HashSet<MethodId> {
156        &self.lifecycle_events
157    }
158
159    pub fn http_request(&self) -> Option<&Arc<HttpRequest>> {
160        self.http_request.as_ref()
161    }
162
163    fn navigated(&mut self, frame: &CdpFrame) {
164        self.name.clone_from(&frame.name);
165        let url = if let Some(ref fragment) = frame.url_fragment {
166            format!("{}{fragment}", frame.url)
167        } else {
168            frame.url.clone()
169        };
170        self.url = Some(url);
171    }
172
173    fn navigated_within_url(&mut self, url: String) {
174        self.url = Some(url)
175    }
176
177    fn on_loading_stopped(&mut self) {
178        self.lifecycle_events.insert("DOMContentLoaded".into());
179        self.lifecycle_events.insert("load".into());
180    }
181
182    fn on_loading_started(&mut self) {
183        self.lifecycle_events.clear();
184        self.http_request.take();
185    }
186
187    pub fn is_loaded(&self) -> bool {
188        self.lifecycle_events.contains("load")
189    }
190
191    pub fn clear_contexts(&mut self) {
192        self.main_world.take_context();
193        self.secondary_world.take_context();
194    }
195
196    pub fn destroy_context(&mut self, ctx_unique_id: &str) {
197        if self.main_world.execution_context_unique_id() == Some(ctx_unique_id) {
198            self.main_world.take_context();
199        } else if self.secondary_world.execution_context_unique_id() == Some(ctx_unique_id) {
200            self.secondary_world.take_context();
201        }
202    }
203
204    pub fn execution_context(&self) -> Option<ExecutionContextId> {
205        self.main_world.execution_context()
206    }
207
208    pub fn set_request(&mut self, request: HttpRequest) {
209        self.http_request = Some(Arc::new(request))
210    }
211}
212
213/// Maintains the state of the pages frame and listens to events produced by
214/// chromium targeting the `Target`. Also listens for events that indicate that
215/// a navigation was completed
216#[derive(Debug)]
217pub struct FrameManager {
218    main_frame: Option<FrameId>,
219    frames: HashMap<FrameId, Frame>,
220    /// The contexts mapped with their frames
221    context_ids: HashMap<String, FrameId>,
222    isolated_worlds: HashSet<String>,
223    /// Timeout after which an anticipated event (related to navigation) doesn't
224    /// arrive results in an error
225    request_timeout: Duration,
226    /// Track currently in progress navigation
227    pending_navigations: VecDeque<(FrameRequestedNavigation, NavigationWatcher)>,
228    /// The currently ongoing navigation
229    navigation: Option<(NavigationWatcher, Instant)>,
230}
231
232impl FrameManager {
233    pub fn new(request_timeout: Duration) -> Self {
234        FrameManager {
235            main_frame: None,
236            frames: Default::default(),
237            context_ids: Default::default(),
238            isolated_worlds: Default::default(),
239            request_timeout,
240            pending_navigations: Default::default(),
241            navigation: None,
242        }
243    }
244
245    /// The commands to execute in order to initialize this frame manager
246    pub fn init_commands(timeout: Duration) -> CommandChain {
247        let enable = page::EnableParams::default();
248        let get_tree = page::GetFrameTreeParams::default();
249        let set_lifecycle = page::SetLifecycleEventsEnabledParams::new(true);
250        // let enable_runtime = runtime::EnableParams::default();
251        // let disable_runtime = runtime::DisableParams::default();
252
253        // let mut commands = Vec::with_capacity(if *CHROME_SPOOF_RUNTIME { 5 } else { 4 });
254        let mut commands = Vec::with_capacity(3);
255
256        let enable_id = enable.identifier();
257        let get_tree_id = get_tree.identifier();
258        let set_lifecycle_id = set_lifecycle.identifier();
259        // let enable_runtime_id = enable_runtime.identifier();
260        // let disable_runtime_id = disable_runtime.identifier();
261
262        if let Ok(value) = serde_json::to_value(enable) {
263            commands.push((enable_id, value));
264        }
265
266        if let Ok(value) = serde_json::to_value(get_tree) {
267            commands.push((get_tree_id, value));
268        }
269
270        if let Ok(value) = serde_json::to_value(set_lifecycle) {
271            commands.push((set_lifecycle_id, value));
272        }
273
274        // if let Ok(value) = serde_json::to_value(enable_runtime) {
275        //     commands.push((enable_runtime_id, value));
276        // }
277
278        // if *CHROME_SPOOF_RUNTIME {
279        //     if let Ok(value) = serde_json::to_value(disable_runtime) {
280        //         commands.push((disable_runtime_id, value));
281        //     }
282        // }
283
284        CommandChain::new(commands, timeout)
285    }
286
287    pub fn main_frame(&self) -> Option<&Frame> {
288        self.main_frame.as_ref().and_then(|id| self.frames.get(id))
289    }
290
291    pub fn main_frame_mut(&mut self) -> Option<&mut Frame> {
292        if let Some(id) = self.main_frame.as_ref() {
293            self.frames.get_mut(id)
294        } else {
295            None
296        }
297    }
298
299    /// Get the main isolated world name.
300    pub fn get_isolated_world_name(&self) -> Option<&String> {
301        self.main_frame
302            .as_ref()
303            .and_then(|id| match self.frames.get(id) {
304                Some(fid) => Some(fid.get_isolated_world_name()),
305                _ => None,
306            })
307    }
308
309    pub fn frames(&self) -> impl Iterator<Item = &Frame> + '_ {
310        self.frames.values()
311    }
312
313    pub fn frame(&self, id: &FrameId) -> Option<&Frame> {
314        self.frames.get(id)
315    }
316
317    fn check_lifecycle(&self, watcher: &NavigationWatcher, frame: &Frame) -> bool {
318        watcher.expected_lifecycle.iter().all(|ev| {
319            frame.lifecycle_events.contains(ev)
320                || (frame.url.is_none() && frame.lifecycle_events.contains("DOMContentLoaded"))
321        }) && frame
322            .child_frames
323            .iter()
324            .filter_map(|f| self.frames.get(f))
325            .all(|f| self.check_lifecycle(watcher, f))
326    }
327
328    fn check_lifecycle_complete(
329        &self,
330        watcher: &NavigationWatcher,
331        frame: &Frame,
332    ) -> Option<NavigationOk> {
333        if !self.check_lifecycle(watcher, frame) {
334            return None;
335        }
336        if frame.loader_id == watcher.loader_id && !watcher.same_document_navigation {
337            return None;
338        }
339        if watcher.same_document_navigation {
340            return Some(NavigationOk::SameDocumentNavigation(watcher.id));
341        }
342        if frame.loader_id != watcher.loader_id {
343            return Some(NavigationOk::NewDocumentNavigation(watcher.id));
344        }
345        None
346    }
347
348    /// Track the request in the frame
349    pub fn on_http_request_finished(&mut self, request: HttpRequest) {
350        if let Some(id) = request.frame.as_ref() {
351            if let Some(frame) = self.frames.get_mut(id) {
352                frame.set_request(request);
353            }
354        }
355    }
356
357    pub fn poll(&mut self, now: Instant) -> Option<FrameEvent> {
358        // check if the navigation completed
359        if let Some((watcher, deadline)) = self.navigation.take() {
360            if now > deadline {
361                // navigation request timed out
362                return Some(FrameEvent::NavigationResult(Err(
363                    NavigationError::Timeout {
364                        err: DeadlineExceeded::new(now, deadline),
365                        id: watcher.id,
366                    },
367                )));
368            }
369
370            if let Some(frame) = self.frames.get(&watcher.frame_id) {
371                if let Some(nav) = self.check_lifecycle_complete(&watcher, frame) {
372                    // request is complete if the frame's lifecycle is complete = frame received all
373                    // required events
374                    return Some(FrameEvent::NavigationResult(Ok(nav)));
375                } else {
376                    // not finished yet
377                    self.navigation = Some((watcher, deadline));
378                }
379            } else {
380                return Some(FrameEvent::NavigationResult(Err(
381                    NavigationError::FrameNotFound {
382                        frame: watcher.frame_id,
383                        id: watcher.id,
384                    },
385                )));
386            }
387        } else if let Some((req, watcher)) = self.pending_navigations.pop_front() {
388            // queue in the next navigation that is must be fulfilled until `deadline`
389            let deadline = Instant::now() + req.timeout;
390            self.navigation = Some((watcher, deadline));
391            return Some(FrameEvent::NavigationRequest(req.id, req.req));
392        }
393        None
394    }
395
396    /// Entrypoint for page navigation
397    pub fn goto(&mut self, req: FrameRequestedNavigation) {
398        if let Some(frame_id) = &self.main_frame {
399            self.navigate_frame(frame_id.clone(), req);
400        }
401    }
402
403    /// Navigate a specific frame
404    pub fn navigate_frame(&mut self, frame_id: FrameId, mut req: FrameRequestedNavigation) {
405        let loader_id = self.frames.get(&frame_id).and_then(|f| f.loader_id.clone());
406        let watcher = NavigationWatcher::until_page_load(req.id, frame_id.clone(), loader_id);
407
408        // insert the frame_id in the request if not present
409        req.set_frame_id(frame_id);
410
411        self.pending_navigations.push_back((req, watcher))
412    }
413
414    /// Fired when a frame moved to another session
415    pub fn on_attached_to_target(&mut self, _event: &EventAttachedToTarget) {
416        // _onFrameMoved
417    }
418
419    pub fn on_frame_tree(&mut self, frame_tree: FrameTree) {
420        self.on_frame_attached(
421            frame_tree.frame.id.clone(),
422            frame_tree.frame.parent_id.clone().map(Into::into),
423        );
424        self.on_frame_navigated(&frame_tree.frame);
425        if let Some(children) = frame_tree.child_frames {
426            for child_tree in children {
427                self.on_frame_tree(child_tree);
428            }
429        }
430    }
431
432    pub fn on_frame_attached(&mut self, frame_id: FrameId, parent_frame_id: Option<FrameId>) {
433        if self.frames.contains_key(&frame_id) {
434            return;
435        }
436        if let Some(parent_frame_id) = parent_frame_id {
437            if let Some(parent_frame) = self.frames.get_mut(&parent_frame_id) {
438                let frame = Frame::with_parent(frame_id.clone(), parent_frame);
439                self.frames.insert(frame_id, frame);
440            }
441        }
442    }
443
444    pub fn on_frame_detached(&mut self, event: &EventFrameDetached) {
445        self.remove_frames_recursively(&event.frame_id);
446    }
447
448    pub fn on_frame_navigated(&mut self, frame: &CdpFrame) {
449        if frame.parent_id.is_some() {
450            if let Some((id, mut f)) = self.frames.remove_entry(&frame.id) {
451                for child in f.child_frames.drain() {
452                    self.remove_frames_recursively(&child);
453                }
454                f.navigated(frame);
455                self.frames.insert(id, f);
456            }
457        } else {
458            let mut f = if let Some(main) = self.main_frame.take() {
459                // update main frame
460                if let Some(mut main_frame) = self.frames.remove(&main) {
461                    for child in &main_frame.child_frames {
462                        self.remove_frames_recursively(child);
463                    }
464                    // this is necessary since we can't borrow mut and then remove recursively
465                    main_frame.child_frames.clear();
466                    main_frame.id = frame.id.clone();
467                    main_frame
468                } else {
469                    Frame::new(frame.id.clone())
470                }
471            } else {
472                // initial main frame navigation
473                Frame::new(frame.id.clone())
474            };
475            f.navigated(frame);
476            self.main_frame = Some(f.id.clone());
477            self.frames.insert(f.id.clone(), f);
478        }
479    }
480
481    pub fn on_frame_navigated_within_document(&mut self, event: &EventNavigatedWithinDocument) {
482        if let Some(frame) = self.frames.get_mut(&event.frame_id) {
483            frame.navigated_within_url(event.url.clone());
484        }
485        if let Some((watcher, _)) = self.navigation.as_mut() {
486            watcher.on_frame_navigated_within_document(event);
487        }
488    }
489
490    pub fn on_frame_stopped_loading(&mut self, event: &EventFrameStoppedLoading) {
491        if let Some(frame) = self.frames.get_mut(&event.frame_id) {
492            frame.on_loading_stopped();
493        }
494    }
495
496    /// Fired when frame has started loading.
497    pub fn on_frame_started_loading(&mut self, event: &EventFrameStartedLoading) {
498        if let Some(frame) = self.frames.get_mut(&event.frame_id) {
499            frame.on_loading_started();
500        }
501    }
502
503    /// Notification is issued every time when binding is called
504    pub fn on_runtime_binding_called(&mut self, _ev: &EventBindingCalled) {}
505
506    /// Issued when new execution context is created
507    pub fn on_frame_execution_context_created(&mut self, event: &EventExecutionContextCreated) {
508        if let Some(frame_id) = event
509            .context
510            .aux_data
511            .as_ref()
512            .and_then(|v| v["frameId"].as_str())
513        {
514            if let Some(frame) = self.frames.get_mut(frame_id) {
515                if event
516                    .context
517                    .aux_data
518                    .as_ref()
519                    .and_then(|v| v["isDefault"].as_bool())
520                    .unwrap_or_default()
521                {
522                    frame
523                        .main_world
524                        .set_context(event.context.id, event.context.unique_id.clone());
525                } else if event.context.name == frame.isolated_world_name
526                    && frame.secondary_world.execution_context().is_none()
527                {
528                    frame
529                        .secondary_world
530                        .set_context(event.context.id, event.context.unique_id.clone());
531                }
532                self.context_ids
533                    .insert(event.context.unique_id.clone(), frame.id.clone());
534            }
535        }
536        if event
537            .context
538            .aux_data
539            .as_ref()
540            .filter(|v| v["type"].as_str() == Some("isolated"))
541            .is_some()
542        {
543            self.isolated_worlds.insert(event.context.name.clone());
544        }
545    }
546
547    /// Issued when execution context is destroyed
548    pub fn on_frame_execution_context_destroyed(&mut self, event: &EventExecutionContextDestroyed) {
549        if let Some(id) = self.context_ids.remove(&event.execution_context_unique_id) {
550            if let Some(frame) = self.frames.get_mut(&id) {
551                frame.destroy_context(&event.execution_context_unique_id);
552            }
553        }
554    }
555
556    /// Issued when all executionContexts were cleared
557    pub fn on_execution_contexts_cleared(&mut self) {
558        for id in self.context_ids.values() {
559            if let Some(frame) = self.frames.get_mut(id) {
560                frame.clear_contexts();
561            }
562        }
563        self.context_ids.clear()
564    }
565
566    /// Fired for top level page lifecycle events (nav, load, paint, etc.)
567    pub fn on_page_lifecycle_event(&mut self, event: &EventLifecycleEvent) {
568        if let Some(frame) = self.frames.get_mut(&event.frame_id) {
569            if event.name == "init" {
570                frame.loader_id = Some(event.loader_id.clone());
571                frame.lifecycle_events.clear();
572            }
573            frame.lifecycle_events.insert(event.name.clone().into());
574        }
575    }
576
577    /// Detach all child frames
578    fn remove_frames_recursively(&mut self, id: &FrameId) -> Option<Frame> {
579        if let Some(mut frame) = self.frames.remove(id) {
580            for child in &frame.child_frames {
581                self.remove_frames_recursively(child);
582            }
583            if let Some(parent_id) = frame.parent_frame.take() {
584                if let Some(parent) = self.frames.get_mut(&parent_id) {
585                    parent.child_frames.remove(&frame.id);
586                }
587            }
588            Some(frame)
589        } else {
590            None
591        }
592    }
593
594    pub fn ensure_isolated_world(&mut self, world_name: &str) -> Option<CommandChain> {
595        if self.isolated_worlds.contains(world_name) {
596            return None;
597        }
598
599        self.isolated_worlds.insert(world_name.to_string());
600
601        let cmd = AddScriptToEvaluateOnNewDocumentParams::builder()
602            .source(format!("//# sourceURL={EVALUATION_SCRIPT_URL}"))
603            .world_name(world_name)
604            .build()
605            .unwrap();
606
607        let mut cmds = Vec::with_capacity(self.frames.len() + 1);
608
609        cmds.push((cmd.identifier(), serde_json::to_value(cmd).unwrap()));
610
611        let cm = self.frames.keys().filter_map(|id| {
612            if let Ok(cmd) = CreateIsolatedWorldParams::builder()
613                .frame_id(id.clone())
614                .grant_univeral_access(true)
615                .world_name(world_name)
616                .build()
617            {
618                let cm = (
619                    cmd.identifier(),
620                    serde_json::to_value(cmd).unwrap_or_default(),
621                );
622
623                Some(cm)
624            } else {
625                None
626            }
627        });
628
629        cmds.extend(cm);
630
631        Some(CommandChain::new(cmds, self.request_timeout))
632    }
633}
634
635#[derive(Debug)]
636pub enum FrameEvent {
637    /// A previously submitted navigation has finished
638    NavigationResult(Result<NavigationOk, NavigationError>),
639    /// A new navigation request needs to be submitted
640    NavigationRequest(NavigationId, Request),
641    /* /// The initial page of the target has been loaded
642     * InitialPageLoadFinished */
643}
644
645#[derive(Debug)]
646pub enum NavigationError {
647    Timeout {
648        id: NavigationId,
649        err: DeadlineExceeded,
650    },
651    FrameNotFound {
652        id: NavigationId,
653        frame: FrameId,
654    },
655}
656
657impl NavigationError {
658    pub fn navigation_id(&self) -> &NavigationId {
659        match self {
660            NavigationError::Timeout { id, .. } => id,
661            NavigationError::FrameNotFound { id, .. } => id,
662        }
663    }
664}
665
666#[derive(Debug, Clone, Eq, PartialEq)]
667pub enum NavigationOk {
668    SameDocumentNavigation(NavigationId),
669    NewDocumentNavigation(NavigationId),
670}
671
672impl NavigationOk {
673    pub fn navigation_id(&self) -> &NavigationId {
674        match self {
675            NavigationOk::SameDocumentNavigation(id) => id,
676            NavigationOk::NewDocumentNavigation(id) => id,
677        }
678    }
679}
680
681/// Tracks the progress of an issued `Page.navigate` request until completion.
682#[derive(Debug)]
683pub struct NavigationWatcher {
684    id: NavigationId,
685    expected_lifecycle: HashSet<MethodId>,
686    frame_id: FrameId,
687    loader_id: Option<LoaderId>,
688    /// Once we receive the response to the issued `Page.navigate` request we
689    /// can detect whether we were navigating withing the same document or were
690    /// navigating to a new document by checking if a loader was included in the
691    /// response.
692    same_document_navigation: bool,
693}
694
695impl NavigationWatcher {
696    pub fn until_page_load(id: NavigationId, frame: FrameId, loader_id: Option<LoaderId>) -> Self {
697        Self {
698            id,
699            expected_lifecycle: std::iter::once("load".into()).collect(),
700            loader_id,
701            frame_id: frame,
702            same_document_navigation: false,
703        }
704    }
705
706    /// Checks whether the navigation was completed
707    pub fn is_lifecycle_complete(&self) -> bool {
708        self.expected_lifecycle.is_empty()
709    }
710
711    fn on_frame_navigated_within_document(&mut self, ev: &EventNavigatedWithinDocument) {
712        if self.frame_id == ev.frame_id {
713            self.same_document_navigation = true;
714        }
715    }
716}
717
718/// An identifier for an ongoing navigation
719#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
720pub struct NavigationId(pub usize);
721
722/// Represents a the request for a navigation
723#[derive(Debug)]
724pub struct FrameRequestedNavigation {
725    /// The internal identifier
726    pub id: NavigationId,
727    /// the cdp request that will trigger the navigation
728    pub req: Request,
729    /// The timeout after which the request will be considered timed out
730    pub timeout: Duration,
731}
732
733impl FrameRequestedNavigation {
734    pub fn new(id: NavigationId, req: Request) -> Self {
735        Self {
736            id,
737            req,
738            timeout: Duration::from_millis(REQUEST_TIMEOUT),
739        }
740    }
741
742    /// This will set the id of the frame into the `params` `frameId` field.
743    pub fn set_frame_id(&mut self, frame_id: FrameId) {
744        if let Some(params) = self.req.params.as_object_mut() {
745            if let Entry::Vacant(entry) = params.entry("frameId") {
746                entry.insert(serde_json::Value::String(frame_id.into()));
747            }
748        }
749    }
750}
751
752#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
753pub enum LifecycleEvent {
754    #[default]
755    Load,
756    DomcontentLoaded,
757    NetworkIdle,
758    NetworkAlmostIdle,
759}
760
761impl AsRef<str> for LifecycleEvent {
762    fn as_ref(&self) -> &str {
763        match self {
764            LifecycleEvent::Load => "load",
765            LifecycleEvent::DomcontentLoaded => "DOMContentLoaded",
766            LifecycleEvent::NetworkIdle => "networkIdle",
767            LifecycleEvent::NetworkAlmostIdle => "networkAlmostIdle",
768        }
769    }
770}