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 };
20use chromiumoxide_types::{Method, MethodId, Request};
21use spider_fingerprint::BASE_CHROME_VERSION;
22
23use crate::error::DeadlineExceeded;
24use crate::handler::domworld::DOMWorld;
25use crate::handler::http::HttpRequest;
26
27use crate::{cmd::CommandChain, ArcHttpRequest};
28
29lazy_static::lazy_static! {
30 static ref EVALUATION_SCRIPT_URL: String = format!("____{}___evaluation_script__", random_world_name(&BASE_CHROME_VERSION.to_string()));
32}
33
34pub fn random_world_name(id: &str) -> String {
36 use rand::RngExt;
37 let mut rng = rand::rng();
38 let rand_len = rng.random_range(6..=12);
39
40 let id_part: String = id
42 .chars()
43 .filter(|c| c.is_ascii_alphanumeric())
44 .take(5)
45 .map(|c| {
46 let c = c.to_ascii_lowercase();
47 if c.is_ascii_alphabetic() {
48 c
49 } else {
50 (b'a' + (c as u8 - b'0') % 26) as char
52 }
53 })
54 .collect();
55
56 let rand_part: String = (0..rand_len)
58 .filter_map(|_| std::char::from_digit(rng.random_range(0..36), 36))
59 .collect();
60
61 let first = std::char::from_digit(rng.random_range(10..36), 36).unwrap_or('a');
63
64 format!("{first}{id_part}{rand_part}")
65}
66
67#[derive(Debug)]
69pub struct Frame {
70 parent_frame: Option<FrameId>,
72 id: FrameId,
74 main_world: DOMWorld,
76 secondary_world: DOMWorld,
78 loader_id: Option<LoaderId>,
79 url: Option<String>,
81 http_request: ArcHttpRequest,
83 child_frames: HashSet<FrameId>,
85 name: Option<String>,
86 lifecycle_events: HashSet<MethodId>,
88 isolated_world_name: String,
90}
91
92impl Frame {
93 pub fn new(id: FrameId) -> Self {
94 let isolated_world_name = random_world_name(id.inner());
95
96 Self {
97 parent_frame: None,
98 id,
99 main_world: Default::default(),
100 secondary_world: Default::default(),
101 loader_id: None,
102 url: None,
103 http_request: None,
104 child_frames: Default::default(),
105 name: None,
106 lifecycle_events: Default::default(),
107 isolated_world_name,
108 }
109 }
110
111 pub fn with_parent(id: FrameId, parent: &mut Frame) -> Self {
112 parent.child_frames.insert(id.clone());
113 Self {
114 parent_frame: Some(parent.id.clone()),
115 id,
116 main_world: Default::default(),
117 secondary_world: Default::default(),
118 loader_id: None,
119 url: None,
120 http_request: None,
121 child_frames: Default::default(),
122 name: None,
123 lifecycle_events: Default::default(),
124 isolated_world_name: parent.isolated_world_name.clone(),
125 }
126 }
127
128 pub fn get_isolated_world_name(&self) -> &String {
129 &self.isolated_world_name
130 }
131
132 pub fn parent_id(&self) -> Option<&FrameId> {
133 self.parent_frame.as_ref()
134 }
135
136 pub fn id(&self) -> &FrameId {
137 &self.id
138 }
139
140 pub fn url(&self) -> Option<&str> {
141 self.url.as_deref()
142 }
143
144 pub fn name(&self) -> Option<&str> {
145 self.name.as_deref()
146 }
147
148 pub fn main_world(&self) -> &DOMWorld {
149 &self.main_world
150 }
151
152 pub fn secondary_world(&self) -> &DOMWorld {
153 &self.secondary_world
154 }
155
156 pub fn lifecycle_events(&self) -> &HashSet<MethodId> {
157 &self.lifecycle_events
158 }
159
160 pub fn http_request(&self) -> Option<&Arc<HttpRequest>> {
161 self.http_request.as_ref()
162 }
163
164 fn navigated(&mut self, frame: &CdpFrame) {
165 self.name.clone_from(&frame.name);
166 let url = if let Some(ref fragment) = frame.url_fragment {
167 format!("{}{fragment}", frame.url)
168 } else {
169 frame.url.clone()
170 };
171 self.url = Some(url);
172 }
173
174 fn navigated_within_url(&mut self, url: String) {
175 self.url = Some(url)
176 }
177
178 fn on_loading_stopped(&mut self) {
179 self.lifecycle_events.insert("DOMContentLoaded".into());
180 self.lifecycle_events.insert("load".into());
181 }
182
183 fn on_loading_started(&mut self) {
184 self.lifecycle_events.clear();
185 self.http_request.take();
186 }
187
188 pub fn is_loaded(&self) -> bool {
189 self.lifecycle_events.contains("load")
190 }
191
192 pub fn is_dom_content_loaded(&self) -> bool {
196 self.lifecycle_events.contains("DOMContentLoaded")
197 }
198
199 pub fn is_network_idle(&self) -> bool {
201 self.lifecycle_events.contains("networkIdle")
202 }
203
204 pub fn is_network_almost_idle(&self) -> bool {
206 self.lifecycle_events.contains("networkAlmostIdle")
207 }
208
209 pub fn clear_contexts(&mut self) {
210 self.main_world.take_context();
211 self.secondary_world.take_context();
212 }
213
214 pub fn destroy_context(&mut self, ctx_unique_id: &str) {
215 if self.main_world.execution_context_unique_id() == Some(ctx_unique_id) {
216 self.main_world.take_context();
217 } else if self.secondary_world.execution_context_unique_id() == Some(ctx_unique_id) {
218 self.secondary_world.take_context();
219 }
220 }
221
222 pub fn execution_context(&self) -> Option<ExecutionContextId> {
223 self.main_world.execution_context()
224 }
225
226 pub fn set_request(&mut self, request: HttpRequest) {
227 self.http_request = Some(Arc::new(request))
228 }
229}
230
231#[derive(Debug)]
235pub struct FrameManager {
236 main_frame: Option<FrameId>,
237 frames: HashMap<FrameId, Frame>,
238 context_ids: HashMap<String, FrameId>,
240 isolated_worlds: HashSet<String>,
241 request_timeout: Duration,
244 pending_navigations: VecDeque<(FrameRequestedNavigation, NavigationWatcher)>,
246 navigation: Option<(NavigationWatcher, Instant)>,
248 max_main_frame_navigations: Option<u32>,
256 main_frame_nav_count: u32,
258}
259
260impl FrameManager {
261 pub fn new(request_timeout: Duration) -> Self {
262 FrameManager {
263 main_frame: None,
264 frames: Default::default(),
265 context_ids: Default::default(),
266 isolated_worlds: Default::default(),
267 request_timeout,
268 pending_navigations: Default::default(),
269 navigation: None,
270 max_main_frame_navigations: None,
271 main_frame_nav_count: 0,
272 }
273 }
274
275 pub fn set_max_main_frame_navigations(&mut self, cap: Option<u32>) {
278 self.max_main_frame_navigations = cap;
279 }
280
281 pub fn init_commands(timeout: Duration) -> CommandChain {
283 let enable = page::EnableParams::default();
284 let get_tree = page::GetFrameTreeParams::default();
285 let set_lifecycle = page::SetLifecycleEventsEnabledParams::new(true);
286 let mut commands = Vec::with_capacity(3);
290
291 let enable_id = enable.identifier();
292 let get_tree_id = get_tree.identifier();
293 let set_lifecycle_id = set_lifecycle.identifier();
294 if let Ok(value) = serde_json::to_value(enable) {
298 commands.push((enable_id, value));
299 }
300
301 if let Ok(value) = serde_json::to_value(get_tree) {
302 commands.push((get_tree_id, value));
303 }
304
305 if let Ok(value) = serde_json::to_value(set_lifecycle) {
306 commands.push((set_lifecycle_id, value));
307 }
308
309 CommandChain::new(commands, timeout)
318 }
319
320 pub fn main_frame(&self) -> Option<&Frame> {
321 self.main_frame.as_ref().and_then(|id| self.frames.get(id))
322 }
323
324 pub fn main_frame_mut(&mut self) -> Option<&mut Frame> {
325 if let Some(id) = self.main_frame.as_ref() {
326 self.frames.get_mut(id)
327 } else {
328 None
329 }
330 }
331
332 pub fn get_isolated_world_name(&self) -> Option<&String> {
334 self.main_frame
335 .as_ref()
336 .and_then(|id| self.frames.get(id).map(|fid| fid.get_isolated_world_name()))
337 }
338
339 pub fn frames(&self) -> impl Iterator<Item = &Frame> + '_ {
340 self.frames.values()
341 }
342
343 pub fn frame(&self, id: &FrameId) -> Option<&Frame> {
344 self.frames.get(id)
345 }
346
347 fn check_lifecycle(&self, watcher: &NavigationWatcher, frame: &Frame) -> bool {
348 watcher.expected_lifecycle.iter().all(|ev| {
349 frame.lifecycle_events.contains(ev)
350 || (frame.url.is_none() && frame.lifecycle_events.contains("DOMContentLoaded"))
351 })
352 }
353
354 fn check_lifecycle_complete(
355 &self,
356 watcher: &NavigationWatcher,
357 frame: &Frame,
358 ) -> Option<NavigationOk> {
359 if !self.check_lifecycle(watcher, frame) {
360 return None;
361 }
362 if frame.loader_id == watcher.loader_id && !watcher.same_document_navigation {
363 return None;
364 }
365 if watcher.same_document_navigation {
366 return Some(NavigationOk::SameDocumentNavigation(watcher.id));
367 }
368 if frame.loader_id != watcher.loader_id {
369 return Some(NavigationOk::NewDocumentNavigation(watcher.id));
370 }
371 None
372 }
373
374 pub fn on_http_request_finished(&mut self, request: HttpRequest) {
376 if let Some(id) = request.frame.as_ref() {
377 if let Some(frame) = self.frames.get_mut(id) {
378 frame.set_request(request);
379 }
380 }
381 }
382
383 pub fn poll(&mut self, now: Instant) -> Option<FrameEvent> {
384 if let Some((watcher, deadline)) = self.navigation.take() {
386 if let Some(cap) = self.max_main_frame_navigations {
389 if self.main_frame_nav_count > cap {
390 let count = self.main_frame_nav_count;
391 return Some(FrameEvent::NavigationResult(Err(
394 NavigationError::TooManyNavigations {
395 id: watcher.id,
396 count,
397 },
398 )));
399 }
400 }
401
402 if now > deadline {
403 return Some(FrameEvent::NavigationResult(Err(
405 NavigationError::Timeout {
406 err: DeadlineExceeded::new(now, deadline),
407 id: watcher.id,
408 },
409 )));
410 }
411
412 if let Some(frame) = self.frames.get(&watcher.frame_id) {
413 if let Some(nav) = self.check_lifecycle_complete(&watcher, frame) {
414 return Some(FrameEvent::NavigationResult(Ok(nav)));
417 } else {
418 self.navigation = Some((watcher, deadline));
420 }
421 } else {
422 return Some(FrameEvent::NavigationResult(Err(
423 NavigationError::FrameNotFound {
424 frame: watcher.frame_id,
425 id: watcher.id,
426 },
427 )));
428 }
429 } else if let Some((req, watcher)) = self.pending_navigations.pop_front() {
430 let deadline = Instant::now() + req.timeout;
432 self.navigation = Some((watcher, deadline));
433 return Some(FrameEvent::NavigationRequest(req.id, req.req));
434 }
435 None
436 }
437
438 pub fn goto(&mut self, req: FrameRequestedNavigation) {
440 if let Some(frame_id) = &self.main_frame {
441 self.navigate_frame(frame_id.clone(), req);
442 }
443 }
444
445 pub fn navigate_frame(&mut self, frame_id: FrameId, mut req: FrameRequestedNavigation) {
447 let loader_id = self.frames.get(&frame_id).and_then(|f| f.loader_id.clone());
448 let watcher = NavigationWatcher::until_load(req.id, frame_id.clone(), loader_id);
449
450 req.set_frame_id(frame_id);
452
453 self.main_frame_nav_count = 0;
455
456 self.pending_navigations.push_back((req, watcher))
457 }
458
459 pub fn on_attached_to_target(&mut self, _event: &EventAttachedToTarget) {
461 }
463
464 pub fn on_frame_tree(&mut self, frame_tree: FrameTree) {
465 self.on_frame_attached(
466 frame_tree.frame.id.clone(),
467 frame_tree.frame.parent_id.clone(),
468 );
469 self.on_frame_navigated(&frame_tree.frame);
470 if let Some(children) = frame_tree.child_frames {
471 for child_tree in children {
472 self.on_frame_tree(child_tree);
473 }
474 }
475 }
476
477 pub fn on_frame_attached(&mut self, frame_id: FrameId, parent_frame_id: Option<FrameId>) {
478 if self.frames.contains_key(&frame_id) {
479 return;
480 }
481 if let Some(parent_frame_id) = parent_frame_id {
482 if let Some(parent_frame) = self.frames.get_mut(&parent_frame_id) {
483 let frame = Frame::with_parent(frame_id.clone(), parent_frame);
484 self.frames.insert(frame_id, frame);
485 }
486 }
487 }
488
489 pub fn on_frame_detached(&mut self, event: &EventFrameDetached) {
490 self.remove_frames_recursively(&event.frame_id);
491 }
492
493 pub fn on_frame_navigated(&mut self, frame: &CdpFrame) {
494 if frame.parent_id.is_some() {
495 if let Some((id, mut f)) = self.frames.remove_entry(&frame.id) {
496 for child in f.child_frames.drain() {
497 self.remove_frames_recursively(&child);
498 }
499 f.navigated(frame);
500 self.frames.insert(id, f);
501 }
502 } else {
503 self.main_frame_nav_count = self.main_frame_nav_count.saturating_add(1);
507
508 let old_main = self.main_frame.take();
509 let mut f = if let Some(main) = old_main.as_ref() {
510 if let Some(mut main_frame) = self.frames.remove(main) {
512 for child in &main_frame.child_frames {
513 self.remove_frames_recursively(child);
514 }
515 main_frame.child_frames.clear();
517 main_frame.id = frame.id.clone();
518 main_frame
519 } else {
520 Frame::new(frame.id.clone())
521 }
522 } else {
523 Frame::new(frame.id.clone())
525 };
526 f.navigated(frame);
527 let new_id = f.id.clone();
528 self.main_frame = Some(new_id.clone());
529 self.frames.insert(new_id.clone(), f);
530
531 if old_main.as_ref() != Some(&new_id) {
534 if let Some((watcher, _)) = self.navigation.as_mut() {
535 if old_main.as_ref() == Some(&watcher.frame_id) {
536 watcher.frame_id = new_id;
537 }
538 }
539 }
540 }
541 }
542
543 pub fn on_frame_navigated_within_document(&mut self, event: &EventNavigatedWithinDocument) {
544 if let Some(frame) = self.frames.get_mut(&event.frame_id) {
545 frame.navigated_within_url(event.url.clone());
546 }
547 if let Some((watcher, _)) = self.navigation.as_mut() {
548 watcher.on_frame_navigated_within_document(event);
549 }
550 }
551
552 pub fn on_frame_stopped_loading(&mut self, event: &EventFrameStoppedLoading) {
553 if let Some(frame) = self.frames.get_mut(&event.frame_id) {
554 frame.on_loading_stopped();
555 }
556 }
557
558 pub fn on_frame_started_loading(&mut self, event: &EventFrameStartedLoading) {
560 if let Some(frame) = self.frames.get_mut(&event.frame_id) {
561 frame.on_loading_started();
562 }
563 }
564
565 pub fn on_runtime_binding_called(&mut self, _ev: &EventBindingCalled) {}
567
568 pub fn on_frame_execution_context_created(&mut self, event: &EventExecutionContextCreated) {
570 if let Some(frame_id) = event
571 .context
572 .aux_data
573 .as_ref()
574 .and_then(|v| v["frameId"].as_str())
575 {
576 if let Some(frame) = self.frames.get_mut(frame_id) {
577 if event
578 .context
579 .aux_data
580 .as_ref()
581 .and_then(|v| v["isDefault"].as_bool())
582 .unwrap_or_default()
583 {
584 frame
585 .main_world
586 .set_context(event.context.id, event.context.unique_id.clone());
587 } else if event.context.name == frame.isolated_world_name
588 && frame.secondary_world.execution_context().is_none()
589 {
590 frame
591 .secondary_world
592 .set_context(event.context.id, event.context.unique_id.clone());
593 }
594 self.context_ids
595 .insert(event.context.unique_id.clone(), frame.id.clone());
596 }
597 }
598 if event
599 .context
600 .aux_data
601 .as_ref()
602 .filter(|v| v["type"].as_str() == Some("isolated"))
603 .is_some()
604 {
605 self.isolated_worlds.insert(event.context.name.clone());
606 }
607 }
608
609 pub fn on_frame_execution_context_destroyed(&mut self, event: &EventExecutionContextDestroyed) {
611 if let Some(id) = self.context_ids.remove(&event.execution_context_unique_id) {
612 if let Some(frame) = self.frames.get_mut(&id) {
613 frame.destroy_context(&event.execution_context_unique_id);
614 }
615 }
616 }
617
618 pub fn on_execution_contexts_cleared(&mut self) {
620 for id in self.context_ids.values() {
621 if let Some(frame) = self.frames.get_mut(id) {
622 frame.clear_contexts();
623 }
624 }
625 self.context_ids.clear()
626 }
627
628 pub fn evict_stale_context_ids(&mut self) {
632 if !self.context_ids.is_empty() {
633 self.context_ids
634 .retain(|_, fid| self.frames.contains_key(fid));
635 }
636 }
637
638 pub fn on_page_lifecycle_event(&mut self, event: &EventLifecycleEvent) {
640 if let Some(frame) = self.frames.get_mut(&event.frame_id) {
641 if event.name == "init" {
642 frame.loader_id = Some(event.loader_id.clone());
643 frame.lifecycle_events.clear();
644 }
645 frame.lifecycle_events.insert(event.name.clone().into());
646 }
647 }
648
649 fn remove_frames_recursively(&mut self, id: &FrameId) -> Option<Frame> {
651 if let Some(mut frame) = self.frames.remove(id) {
652 for child in &frame.child_frames {
653 self.remove_frames_recursively(child);
654 }
655 if let Some(parent_id) = frame.parent_frame.take() {
656 if let Some(parent) = self.frames.get_mut(&parent_id) {
657 parent.child_frames.remove(&frame.id);
658 }
659 }
660 Some(frame)
661 } else {
662 None
663 }
664 }
665
666 pub fn ensure_isolated_world(&mut self, world_name: &str) -> Option<CommandChain> {
667 if self.isolated_worlds.contains(world_name) {
668 return None;
669 }
670
671 self.isolated_worlds.insert(world_name.to_string());
672
673 if let Ok(cmd) = AddScriptToEvaluateOnNewDocumentParams::builder()
674 .source(format!("//# sourceURL={}", *EVALUATION_SCRIPT_URL))
675 .world_name(world_name)
676 .build()
677 {
678 let mut cmds = Vec::with_capacity(self.frames.len() + 1);
679 let identifier = cmd.identifier();
680
681 if let Ok(cmd) = serde_json::to_value(cmd) {
682 cmds.push((identifier, cmd));
683 }
684
685 let cm = self.frames.keys().filter_map(|id| {
686 if let Ok(cmd) = CreateIsolatedWorldParams::builder()
687 .frame_id(id.clone())
688 .grant_univeral_access(true)
689 .world_name(world_name)
690 .build()
691 {
692 let cm = (
693 cmd.identifier(),
694 serde_json::to_value(cmd).unwrap_or_default(),
695 );
696
697 Some(cm)
698 } else {
699 None
700 }
701 });
702
703 cmds.extend(cm);
704
705 Some(CommandChain::new(cmds, self.request_timeout))
706 } else {
707 None
708 }
709 }
710}
711
712#[derive(Debug)]
713pub enum FrameEvent {
714 NavigationResult(Result<NavigationOk, NavigationError>),
716 NavigationRequest(NavigationId, Request),
718 }
721
722#[derive(Debug)]
723pub enum NavigationError {
724 Timeout {
725 id: NavigationId,
726 err: DeadlineExceeded,
727 },
728 FrameNotFound {
729 id: NavigationId,
730 frame: FrameId,
731 },
732 TooManyNavigations {
737 id: NavigationId,
738 count: u32,
740 },
741}
742
743impl NavigationError {
744 pub fn navigation_id(&self) -> &NavigationId {
745 match self {
746 NavigationError::Timeout { id, .. } => id,
747 NavigationError::FrameNotFound { id, .. } => id,
748 NavigationError::TooManyNavigations { id, .. } => id,
749 }
750 }
751}
752
753#[derive(Debug, Clone, Eq, PartialEq)]
754pub enum NavigationOk {
755 SameDocumentNavigation(NavigationId),
756 NewDocumentNavigation(NavigationId),
757}
758
759impl NavigationOk {
760 pub fn navigation_id(&self) -> &NavigationId {
761 match self {
762 NavigationOk::SameDocumentNavigation(id) => id,
763 NavigationOk::NewDocumentNavigation(id) => id,
764 }
765 }
766}
767
768#[derive(Debug)]
770pub struct NavigationWatcher {
771 id: NavigationId,
772 expected_lifecycle: HashSet<MethodId>,
773 frame_id: FrameId,
774 loader_id: Option<LoaderId>,
775 same_document_navigation: bool,
780}
781
782impl NavigationWatcher {
783 pub fn until_lifecycle(
786 id: NavigationId,
787 frame: FrameId,
788 loader_id: Option<LoaderId>,
789 events: &[LifecycleEvent],
790 ) -> Self {
791 let expected_lifecycle = events.iter().map(LifecycleEvent::to_method_id).collect();
792
793 Self {
794 id,
795 expected_lifecycle,
796 frame_id: frame,
797 loader_id,
798 same_document_navigation: false,
799 }
800 }
801
802 pub fn until_load(id: NavigationId, frame: FrameId, loader_id: Option<LoaderId>) -> Self {
804 Self::until_lifecycle(id, frame, loader_id, &[LifecycleEvent::Load])
805 }
806
807 pub fn until_domcontent_loaded(
809 id: NavigationId,
810 frame: FrameId,
811 loader_id: Option<LoaderId>,
812 ) -> Self {
813 Self::until_lifecycle(id, frame, loader_id, &[LifecycleEvent::DomcontentLoaded])
814 }
815
816 pub fn until_network_idle(
818 id: NavigationId,
819 frame: FrameId,
820 loader_id: Option<LoaderId>,
821 ) -> Self {
822 Self::until_lifecycle(id, frame, loader_id, &[LifecycleEvent::NetworkIdle])
823 }
824
825 pub fn until_network_almost_idle(
827 id: NavigationId,
828 frame: FrameId,
829 loader_id: Option<LoaderId>,
830 ) -> Self {
831 Self::until_lifecycle(id, frame, loader_id, &[LifecycleEvent::NetworkAlmostIdle])
832 }
833
834 pub fn until_domcontent_and_network_idle(
836 id: NavigationId,
837 frame: FrameId,
838 loader_id: Option<LoaderId>,
839 ) -> Self {
840 Self::until_lifecycle(
841 id,
842 frame,
843 loader_id,
844 &[
845 LifecycleEvent::DomcontentLoaded,
846 LifecycleEvent::NetworkIdle,
847 ],
848 )
849 }
850
851 pub fn is_lifecycle_complete(&self) -> bool {
853 self.expected_lifecycle.is_empty()
854 }
855
856 fn on_frame_navigated_within_document(&mut self, ev: &EventNavigatedWithinDocument) {
857 if self.frame_id == ev.frame_id {
858 self.same_document_navigation = true;
859 }
860 }
861}
862
863#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
865pub struct NavigationId(pub usize);
866
867#[derive(Debug)]
869pub struct FrameRequestedNavigation {
870 pub id: NavigationId,
872 pub req: Request,
874 pub timeout: Duration,
876}
877
878impl FrameRequestedNavigation {
879 pub fn new(id: NavigationId, req: Request, request_timeout: Duration) -> Self {
880 Self {
881 id,
882 req,
883 timeout: request_timeout,
884 }
885 }
886
887 pub fn set_frame_id(&mut self, frame_id: FrameId) {
889 if let Some(params) = self.req.params.as_object_mut() {
890 if let Entry::Vacant(entry) = params.entry("frameId") {
891 entry.insert(serde_json::Value::String(frame_id.into()));
892 }
893 }
894 }
895}
896
897#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
898pub enum LifecycleEvent {
899 #[default]
900 Load,
901 DomcontentLoaded,
902 NetworkIdle,
903 NetworkAlmostIdle,
904}
905
906impl LifecycleEvent {
907 #[inline]
908 pub fn to_method_id(&self) -> MethodId {
909 match self {
910 LifecycleEvent::Load => "load".into(),
911 LifecycleEvent::DomcontentLoaded => "DOMContentLoaded".into(),
912 LifecycleEvent::NetworkIdle => "networkIdle".into(),
913 LifecycleEvent::NetworkAlmostIdle => "networkAlmostIdle".into(),
914 }
915 }
916}
917
918impl AsRef<str> for LifecycleEvent {
919 fn as_ref(&self) -> &str {
920 match self {
921 LifecycleEvent::Load => "load",
922 LifecycleEvent::DomcontentLoaded => "DOMContentLoaded",
923 LifecycleEvent::NetworkIdle => "networkIdle",
924 LifecycleEvent::NetworkAlmostIdle => "networkAlmostIdle",
925 }
926 }
927}
928
929#[cfg(test)]
930mod tests {
931 use super::*;
932
933 #[test]
934 fn frame_lifecycle_events_cleared_on_loading_started() {
935 let mut frame = Frame::new(FrameId::new("test"));
936
937 frame.lifecycle_events.insert("load".into());
939 frame.lifecycle_events.insert("DOMContentLoaded".into());
940 assert!(frame.is_loaded());
941
942 frame.on_loading_started();
944 assert!(!frame.is_loaded());
945 }
946
947 #[test]
948 fn frame_loading_stopped_inserts_load_events() {
949 let mut frame = Frame::new(FrameId::new("test"));
950 assert!(!frame.is_loaded());
951
952 frame.on_loading_stopped();
953 assert!(frame.is_loaded());
954 }
955
956 #[test]
957 fn navigation_completes_when_main_frame_loaded_despite_child_frames() {
958 let timeout = Duration::from_secs(30);
959 let mut fm = FrameManager::new(timeout);
960
961 let main_id = FrameId::new("main");
963 let mut main_frame = Frame::new(main_id.clone());
964 main_frame.loader_id = Some(LoaderId::from("loader-old".to_string()));
965 main_frame.lifecycle_events.insert("load".into());
966 fm.frames.insert(main_id.clone(), main_frame);
967 fm.main_frame = Some(main_id.clone());
968
969 let child_id = FrameId::new("child-ad");
971 let child = Frame::with_parent(child_id.clone(), fm.frames.get_mut(&main_id).unwrap());
972 fm.frames.insert(child_id, child);
973
974 let watcher = NavigationWatcher::until_load(
976 NavigationId(0),
977 main_id.clone(),
978 Some(LoaderId::from("loader-old".to_string())),
979 );
980
981 fm.frames.get_mut(&main_id).unwrap().loader_id =
983 Some(LoaderId::from("loader-new".to_string()));
984
985 let main_frame = fm.frames.get(&main_id).unwrap();
988 let result = fm.check_lifecycle_complete(&watcher, main_frame);
989 assert!(
990 result.is_some(),
991 "navigation should complete without waiting for child frames"
992 );
993 }
994
995 #[test]
996 fn navigation_watcher_tracks_main_frame_id_change() {
997 let timeout = Duration::from_secs(30);
998 let mut fm = FrameManager::new(timeout);
999
1000 let old_id = FrameId::new("old-main");
1002 let mut main_frame = Frame::new(old_id.clone());
1003 main_frame.loader_id = Some(LoaderId::from("loader-1".to_string()));
1004 fm.frames.insert(old_id.clone(), main_frame);
1005 fm.main_frame = Some(old_id.clone());
1006
1007 let watcher = NavigationWatcher::until_load(
1010 NavigationId(0),
1011 old_id.clone(),
1012 Some(LoaderId::from("loader-1".to_string())),
1013 );
1014 let deadline = Instant::now() + timeout;
1015 fm.navigation = Some((watcher, deadline));
1016
1017 let new_id = FrameId::new("new-main");
1021 if let Some(mut old_frame) = fm.frames.remove(&old_id) {
1022 old_frame.child_frames.clear();
1023 old_frame.id = new_id.clone();
1024 fm.frames.insert(new_id.clone(), old_frame);
1025 }
1026 fm.main_frame = Some(new_id.clone());
1027
1028 if let Some((watcher, _)) = fm.navigation.as_mut() {
1030 if watcher.frame_id == old_id {
1031 watcher.frame_id = new_id.clone();
1032 }
1033 }
1034
1035 let (watcher, _) = fm.navigation.as_ref().unwrap();
1037 assert_eq!(
1038 watcher.frame_id, new_id,
1039 "watcher should follow the main frame ID change"
1040 );
1041
1042 fm.frames.get_mut(&new_id).unwrap().loader_id =
1044 Some(LoaderId::from("loader-2".to_string()));
1045 fm.frames
1046 .get_mut(&new_id)
1047 .unwrap()
1048 .lifecycle_events
1049 .insert("load".into());
1050
1051 let event = fm.poll(Instant::now());
1052 assert!(
1053 matches!(event, Some(FrameEvent::NavigationResult(Ok(_)))),
1054 "navigation should complete on the new frame"
1055 );
1056 }
1057
1058 fn seed_main_frame(fm: &mut FrameManager, loader: &str) -> FrameId {
1061 let id = FrameId::new("main");
1062 let mut frame = Frame::new(id.clone());
1063 frame.loader_id = Some(LoaderId::from(loader.to_string()));
1064 fm.frames.insert(id.clone(), frame);
1065 fm.main_frame = Some(id.clone());
1066 id
1067 }
1068
1069 fn active_watcher(fm: &mut FrameManager, frame_id: FrameId) {
1070 let watcher = NavigationWatcher::until_load(
1071 NavigationId(0),
1072 frame_id.clone(),
1073 fm.frames.get(&frame_id).and_then(|f| f.loader_id.clone()),
1074 );
1075 let deadline = Instant::now() + Duration::from_secs(30);
1076 fm.navigation = Some((watcher, deadline));
1077 }
1078
1079 #[test]
1080 fn nav_loop_guard_none_allows_unlimited() {
1081 let mut fm = FrameManager::new(Duration::from_secs(30));
1082 let id = seed_main_frame(&mut fm, "loader-0");
1084 active_watcher(&mut fm, id);
1085
1086 for _ in 0..25 {
1088 fm.main_frame_nav_count = fm.main_frame_nav_count.saturating_add(1);
1089 }
1090
1091 let event = fm.poll(Instant::now());
1095 assert!(
1096 !matches!(
1097 event,
1098 Some(FrameEvent::NavigationResult(Err(
1099 NavigationError::TooManyNavigations { .. }
1100 )))
1101 ),
1102 "None cap must never emit TooManyNavigations"
1103 );
1104 }
1105
1106 #[test]
1107 fn nav_loop_guard_caps_and_reports_count() {
1108 let mut fm = FrameManager::new(Duration::from_secs(30));
1109 fm.set_max_main_frame_navigations(Some(3));
1110 let id = seed_main_frame(&mut fm, "loader-0");
1111 active_watcher(&mut fm, id);
1112
1113 for _ in 0..5 {
1115 fm.main_frame_nav_count = fm.main_frame_nav_count.saturating_add(1);
1116 }
1117
1118 match fm.poll(Instant::now()) {
1119 Some(FrameEvent::NavigationResult(Err(NavigationError::TooManyNavigations {
1120 count,
1121 ..
1122 }))) => {
1123 assert_eq!(count, 5, "reported count must be the observed value");
1124 }
1125 other => panic!("expected TooManyNavigations, got {other:?}"),
1126 }
1127 }
1128
1129 #[test]
1130 fn nav_loop_guard_resets_on_goto() {
1131 let mut fm = FrameManager::new(Duration::from_secs(30));
1132 fm.set_max_main_frame_navigations(Some(3));
1133 let id = seed_main_frame(&mut fm, "loader-0");
1134 active_watcher(&mut fm, id.clone());
1135
1136 fm.main_frame_nav_count = 10;
1138 assert!(matches!(
1139 fm.poll(Instant::now()),
1140 Some(FrameEvent::NavigationResult(Err(
1141 NavigationError::TooManyNavigations { .. }
1142 )))
1143 ));
1144
1145 fm.navigate_frame(
1147 id.clone(),
1148 FrameRequestedNavigation::new(
1149 NavigationId(1),
1150 Request::new("Page.navigate".into(), serde_json::json!({})),
1151 Duration::from_secs(30),
1152 ),
1153 );
1154 assert_eq!(
1155 fm.main_frame_nav_count, 0,
1156 "navigate_frame must reset the main-frame nav counter"
1157 );
1158 }
1159
1160 #[test]
1161 fn nav_loop_guard_same_document_not_counted() {
1162 let mut fm = FrameManager::new(Duration::from_secs(30));
1168 fm.set_max_main_frame_navigations(Some(2));
1169 let id = seed_main_frame(&mut fm, "loader-0");
1170
1171 active_watcher(&mut fm, id.clone());
1173
1174 for _ in 0..10 {
1176 fm.on_frame_navigated_within_document(
1177 &chromiumoxide_cdp::cdp::browser_protocol::page::EventNavigatedWithinDocument {
1178 frame_id: id.clone(),
1179 url: "https://example.com/#a".into(),
1180 navigation_type:
1181 chromiumoxide_cdp::cdp::browser_protocol::page::NavigatedWithinDocumentNavigationType::Fragment,
1182 },
1183 );
1184 }
1185
1186 assert_eq!(
1187 fm.main_frame_nav_count, 0,
1188 "same-document navigations must not count against the cross-document cap"
1189 );
1190 }
1191}