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 abandon_navigation(&mut self, id: NavigationId) {
385 if self
386 .navigation
387 .as_ref()
388 .is_some_and(|(nav, _)| nav.id == id)
389 {
390 self.navigation = None;
391 }
392 self.pending_navigations.retain(|(req, _)| req.id != id);
397 }
398
399 pub fn poll(&mut self, now: Instant) -> Option<FrameEvent> {
400 if let Some((watcher, deadline)) = self.navigation.take() {
402 if let Some(cap) = self.max_main_frame_navigations {
405 if self.main_frame_nav_count > cap {
406 let count = self.main_frame_nav_count;
407 return Some(FrameEvent::NavigationResult(Err(
410 NavigationError::TooManyNavigations {
411 id: watcher.id,
412 count,
413 },
414 )));
415 }
416 }
417
418 if now > deadline {
419 return Some(FrameEvent::NavigationResult(Err(
421 NavigationError::Timeout {
422 err: DeadlineExceeded::new(now, deadline),
423 id: watcher.id,
424 },
425 )));
426 }
427
428 if let Some(frame) = self.frames.get(&watcher.frame_id) {
429 if let Some(nav) = self.check_lifecycle_complete(&watcher, frame) {
430 return Some(FrameEvent::NavigationResult(Ok(nav)));
433 } else {
434 self.navigation = Some((watcher, deadline));
436 }
437 } else {
438 return Some(FrameEvent::NavigationResult(Err(
439 NavigationError::FrameNotFound {
440 frame: watcher.frame_id,
441 id: watcher.id,
442 },
443 )));
444 }
445 } else if let Some((req, watcher)) = self.pending_navigations.pop_front() {
446 let deadline = Instant::now() + req.timeout;
448 self.navigation = Some((watcher, deadline));
449 return Some(FrameEvent::NavigationRequest(req.id, req.req));
450 }
451 None
452 }
453
454 pub fn goto(&mut self, req: FrameRequestedNavigation) {
456 if let Some(frame_id) = &self.main_frame {
457 self.navigate_frame(frame_id.clone(), req);
458 }
459 }
460
461 pub fn navigate_frame(&mut self, frame_id: FrameId, mut req: FrameRequestedNavigation) {
463 let loader_id = self.frames.get(&frame_id).and_then(|f| f.loader_id.clone());
464 let watcher = NavigationWatcher::until_load(req.id, frame_id.clone(), loader_id);
465
466 req.set_frame_id(frame_id);
468
469 self.main_frame_nav_count = 0;
471
472 self.pending_navigations.push_back((req, watcher))
473 }
474
475 pub fn on_attached_to_target(&mut self, _event: &EventAttachedToTarget) {
477 }
479
480 pub fn on_frame_tree(&mut self, frame_tree: FrameTree) {
481 self.on_frame_attached(
482 frame_tree.frame.id.clone(),
483 frame_tree.frame.parent_id.clone(),
484 );
485 self.on_frame_navigated(&frame_tree.frame);
486 if let Some(children) = frame_tree.child_frames {
487 for child_tree in children {
488 self.on_frame_tree(child_tree);
489 }
490 }
491 }
492
493 pub fn on_frame_attached(&mut self, frame_id: FrameId, parent_frame_id: Option<FrameId>) {
494 if self.frames.contains_key(&frame_id) {
495 return;
496 }
497 if let Some(parent_frame_id) = parent_frame_id {
498 if let Some(parent_frame) = self.frames.get_mut(&parent_frame_id) {
499 let frame = Frame::with_parent(frame_id.clone(), parent_frame);
500 self.frames.insert(frame_id, frame);
501 }
502 }
503 }
504
505 pub fn on_frame_detached(&mut self, event: &EventFrameDetached) {
506 self.remove_frames_recursively(&event.frame_id);
507 }
508
509 pub fn on_frame_navigated(&mut self, frame: &CdpFrame) {
510 if frame.parent_id.is_some() {
511 if let Some((id, mut f)) = self.frames.remove_entry(&frame.id) {
512 for child in f.child_frames.drain() {
513 self.remove_frames_recursively(&child);
514 }
515 f.navigated(frame);
516 self.frames.insert(id, f);
517 }
518 } else {
519 self.main_frame_nav_count = self.main_frame_nav_count.saturating_add(1);
523
524 let old_main = self.main_frame.take();
525 let mut f = if let Some(main) = old_main.as_ref() {
526 if let Some(mut main_frame) = self.frames.remove(main) {
528 for child in &main_frame.child_frames {
529 self.remove_frames_recursively(child);
530 }
531 main_frame.child_frames.clear();
533 main_frame.id = frame.id.clone();
534 main_frame
535 } else {
536 Frame::new(frame.id.clone())
537 }
538 } else {
539 Frame::new(frame.id.clone())
541 };
542 f.navigated(frame);
543 let new_id = f.id.clone();
544 self.main_frame = Some(new_id.clone());
545 self.frames.insert(new_id.clone(), f);
546
547 if old_main.as_ref() != Some(&new_id) {
550 if let Some((watcher, _)) = self.navigation.as_mut() {
551 if old_main.as_ref() == Some(&watcher.frame_id) {
552 watcher.frame_id = new_id;
553 }
554 }
555 }
556 }
557 }
558
559 pub fn on_frame_navigated_within_document(&mut self, event: &EventNavigatedWithinDocument) {
560 if let Some(frame) = self.frames.get_mut(&event.frame_id) {
561 frame.navigated_within_url(event.url.clone());
562 }
563 if let Some((watcher, _)) = self.navigation.as_mut() {
564 watcher.on_frame_navigated_within_document(event);
565 }
566 }
567
568 pub fn on_frame_stopped_loading(&mut self, event: &EventFrameStoppedLoading) {
569 if let Some(frame) = self.frames.get_mut(&event.frame_id) {
570 frame.on_loading_stopped();
571 }
572 }
573
574 pub fn on_frame_started_loading(&mut self, event: &EventFrameStartedLoading) {
576 if let Some(frame) = self.frames.get_mut(&event.frame_id) {
577 frame.on_loading_started();
578 }
579 }
580
581 pub fn on_runtime_binding_called(&mut self, _ev: &EventBindingCalled) {}
583
584 pub fn on_frame_execution_context_created(&mut self, event: &EventExecutionContextCreated) {
586 if let Some(frame_id) = event
587 .context
588 .aux_data
589 .as_ref()
590 .and_then(|v| v["frameId"].as_str())
591 {
592 if let Some(frame) = self.frames.get_mut(frame_id) {
593 if event
594 .context
595 .aux_data
596 .as_ref()
597 .and_then(|v| v["isDefault"].as_bool())
598 .unwrap_or_default()
599 {
600 frame
601 .main_world
602 .set_context(event.context.id, event.context.unique_id.clone());
603 } else if event.context.name == frame.isolated_world_name
604 && frame.secondary_world.execution_context().is_none()
605 {
606 frame
607 .secondary_world
608 .set_context(event.context.id, event.context.unique_id.clone());
609 }
610 self.context_ids
611 .insert(event.context.unique_id.clone(), frame.id.clone());
612 }
613 }
614 if event
615 .context
616 .aux_data
617 .as_ref()
618 .filter(|v| v["type"].as_str() == Some("isolated"))
619 .is_some()
620 {
621 self.isolated_worlds.insert(event.context.name.clone());
622 }
623 }
624
625 pub fn on_frame_execution_context_destroyed(&mut self, event: &EventExecutionContextDestroyed) {
627 if let Some(id) = self.context_ids.remove(&event.execution_context_unique_id) {
628 if let Some(frame) = self.frames.get_mut(&id) {
629 frame.destroy_context(&event.execution_context_unique_id);
630 }
631 }
632 }
633
634 pub fn on_execution_contexts_cleared(&mut self) {
636 for id in self.context_ids.values() {
637 if let Some(frame) = self.frames.get_mut(id) {
638 frame.clear_contexts();
639 }
640 }
641 self.context_ids.clear();
642 self.isolated_worlds.clear();
647 }
648
649 pub fn evict_stale_context_ids(&mut self) {
653 if !self.context_ids.is_empty() {
654 self.context_ids
655 .retain(|_, fid| self.frames.contains_key(fid));
656 }
657 }
658
659 pub fn on_page_lifecycle_event(&mut self, event: &EventLifecycleEvent) {
661 if let Some(frame) = self.frames.get_mut(&event.frame_id) {
662 if event.name == "init" {
663 frame.loader_id = Some(event.loader_id.clone());
664 frame.lifecycle_events.clear();
665 }
666 frame.lifecycle_events.insert(event.name.clone().into());
667 }
668 }
669
670 fn remove_frames_recursively(&mut self, id: &FrameId) -> Option<Frame> {
672 if let Some(mut frame) = self.frames.remove(id) {
673 for child in &frame.child_frames {
674 self.remove_frames_recursively(child);
675 }
676 if let Some(parent_id) = frame.parent_frame.take() {
677 if let Some(parent) = self.frames.get_mut(&parent_id) {
678 parent.child_frames.remove(&frame.id);
679 }
680 }
681 Some(frame)
682 } else {
683 None
684 }
685 }
686
687 pub fn ensure_isolated_world(&mut self, world_name: &str) -> Option<CommandChain> {
688 if self.isolated_worlds.contains(world_name) {
689 return None;
690 }
691
692 self.isolated_worlds.insert(world_name.to_string());
693
694 if let Ok(cmd) = AddScriptToEvaluateOnNewDocumentParams::builder()
695 .source(format!("//# sourceURL={}", *EVALUATION_SCRIPT_URL))
696 .world_name(world_name)
697 .build()
698 {
699 let mut cmds = Vec::with_capacity(self.frames.len() + 1);
700 let identifier = cmd.identifier();
701
702 if let Ok(cmd) = serde_json::to_value(cmd) {
703 cmds.push((identifier, cmd));
704 }
705
706 let cm = self.frames.keys().filter_map(|id| {
707 if let Ok(cmd) = CreateIsolatedWorldParams::builder()
708 .frame_id(id.clone())
709 .grant_univeral_access(true)
710 .world_name(world_name)
711 .build()
712 {
713 let cm = (
714 cmd.identifier(),
715 serde_json::to_value(cmd).unwrap_or_default(),
716 );
717
718 Some(cm)
719 } else {
720 None
721 }
722 });
723
724 cmds.extend(cm);
725
726 Some(CommandChain::new(cmds, self.request_timeout))
727 } else {
728 None
729 }
730 }
731}
732
733#[derive(Debug)]
734pub enum FrameEvent {
735 NavigationResult(Result<NavigationOk, NavigationError>),
737 NavigationRequest(NavigationId, Request),
739 }
742
743#[derive(Debug)]
744pub enum NavigationError {
745 Timeout {
746 id: NavigationId,
747 err: DeadlineExceeded,
748 },
749 FrameNotFound {
750 id: NavigationId,
751 frame: FrameId,
752 },
753 TooManyNavigations {
758 id: NavigationId,
759 count: u32,
761 },
762}
763
764impl NavigationError {
765 pub fn navigation_id(&self) -> &NavigationId {
766 match self {
767 NavigationError::Timeout { id, .. } => id,
768 NavigationError::FrameNotFound { id, .. } => id,
769 NavigationError::TooManyNavigations { id, .. } => id,
770 }
771 }
772}
773
774#[derive(Debug, Clone, Eq, PartialEq)]
775pub enum NavigationOk {
776 SameDocumentNavigation(NavigationId),
777 NewDocumentNavigation(NavigationId),
778}
779
780impl NavigationOk {
781 pub fn navigation_id(&self) -> &NavigationId {
782 match self {
783 NavigationOk::SameDocumentNavigation(id) => id,
784 NavigationOk::NewDocumentNavigation(id) => id,
785 }
786 }
787}
788
789#[derive(Debug)]
791pub struct NavigationWatcher {
792 id: NavigationId,
793 expected_lifecycle: HashSet<MethodId>,
794 frame_id: FrameId,
795 loader_id: Option<LoaderId>,
796 same_document_navigation: bool,
801}
802
803impl NavigationWatcher {
804 pub fn until_lifecycle(
807 id: NavigationId,
808 frame: FrameId,
809 loader_id: Option<LoaderId>,
810 events: &[LifecycleEvent],
811 ) -> Self {
812 let expected_lifecycle = events.iter().map(LifecycleEvent::to_method_id).collect();
813
814 Self {
815 id,
816 expected_lifecycle,
817 frame_id: frame,
818 loader_id,
819 same_document_navigation: false,
820 }
821 }
822
823 pub fn until_load(id: NavigationId, frame: FrameId, loader_id: Option<LoaderId>) -> Self {
825 Self::until_lifecycle(id, frame, loader_id, &[LifecycleEvent::Load])
826 }
827
828 pub fn until_domcontent_loaded(
830 id: NavigationId,
831 frame: FrameId,
832 loader_id: Option<LoaderId>,
833 ) -> Self {
834 Self::until_lifecycle(id, frame, loader_id, &[LifecycleEvent::DomcontentLoaded])
835 }
836
837 pub fn until_network_idle(
839 id: NavigationId,
840 frame: FrameId,
841 loader_id: Option<LoaderId>,
842 ) -> Self {
843 Self::until_lifecycle(id, frame, loader_id, &[LifecycleEvent::NetworkIdle])
844 }
845
846 pub fn until_network_almost_idle(
848 id: NavigationId,
849 frame: FrameId,
850 loader_id: Option<LoaderId>,
851 ) -> Self {
852 Self::until_lifecycle(id, frame, loader_id, &[LifecycleEvent::NetworkAlmostIdle])
853 }
854
855 pub fn until_domcontent_and_network_idle(
857 id: NavigationId,
858 frame: FrameId,
859 loader_id: Option<LoaderId>,
860 ) -> Self {
861 Self::until_lifecycle(
862 id,
863 frame,
864 loader_id,
865 &[
866 LifecycleEvent::DomcontentLoaded,
867 LifecycleEvent::NetworkIdle,
868 ],
869 )
870 }
871
872 pub fn is_lifecycle_complete(&self) -> bool {
874 self.expected_lifecycle.is_empty()
875 }
876
877 fn on_frame_navigated_within_document(&mut self, ev: &EventNavigatedWithinDocument) {
878 if self.frame_id == ev.frame_id {
879 self.same_document_navigation = true;
880 }
881 }
882}
883
884#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
886pub struct NavigationId(pub usize);
887
888#[derive(Debug)]
890pub struct FrameRequestedNavigation {
891 pub id: NavigationId,
893 pub req: Request,
895 pub timeout: Duration,
897}
898
899impl FrameRequestedNavigation {
900 pub fn new(id: NavigationId, req: Request, request_timeout: Duration) -> Self {
901 Self {
902 id,
903 req,
904 timeout: request_timeout,
905 }
906 }
907
908 pub fn set_frame_id(&mut self, frame_id: FrameId) {
910 if let Some(params) = self.req.params.as_object_mut() {
911 if let Entry::Vacant(entry) = params.entry("frameId") {
912 entry.insert(serde_json::Value::String(frame_id.into()));
913 }
914 }
915 }
916}
917
918#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
919pub enum LifecycleEvent {
920 #[default]
921 Load,
922 DomcontentLoaded,
923 NetworkIdle,
924 NetworkAlmostIdle,
925}
926
927impl LifecycleEvent {
928 #[inline]
929 pub fn to_method_id(&self) -> MethodId {
930 match self {
931 LifecycleEvent::Load => "load".into(),
932 LifecycleEvent::DomcontentLoaded => "DOMContentLoaded".into(),
933 LifecycleEvent::NetworkIdle => "networkIdle".into(),
934 LifecycleEvent::NetworkAlmostIdle => "networkAlmostIdle".into(),
935 }
936 }
937}
938
939impl AsRef<str> for LifecycleEvent {
940 fn as_ref(&self) -> &str {
941 match self {
942 LifecycleEvent::Load => "load",
943 LifecycleEvent::DomcontentLoaded => "DOMContentLoaded",
944 LifecycleEvent::NetworkIdle => "networkIdle",
945 LifecycleEvent::NetworkAlmostIdle => "networkAlmostIdle",
946 }
947 }
948}
949
950#[cfg(test)]
951mod tests {
952 use super::*;
953
954 #[test]
955 fn frame_lifecycle_events_cleared_on_loading_started() {
956 let mut frame = Frame::new(FrameId::new("test"));
957
958 frame.lifecycle_events.insert("load".into());
960 frame.lifecycle_events.insert("DOMContentLoaded".into());
961 assert!(frame.is_loaded());
962
963 frame.on_loading_started();
965 assert!(!frame.is_loaded());
966 }
967
968 #[test]
969 fn frame_loading_stopped_inserts_load_events() {
970 let mut frame = Frame::new(FrameId::new("test"));
971 assert!(!frame.is_loaded());
972
973 frame.on_loading_stopped();
974 assert!(frame.is_loaded());
975 }
976
977 #[test]
978 fn navigation_completes_when_main_frame_loaded_despite_child_frames() {
979 let timeout = Duration::from_secs(30);
980 let mut fm = FrameManager::new(timeout);
981
982 let main_id = FrameId::new("main");
984 let mut main_frame = Frame::new(main_id.clone());
985 main_frame.loader_id = Some(LoaderId::from("loader-old".to_string()));
986 main_frame.lifecycle_events.insert("load".into());
987 fm.frames.insert(main_id.clone(), main_frame);
988 fm.main_frame = Some(main_id.clone());
989
990 let child_id = FrameId::new("child-ad");
992 let child = Frame::with_parent(child_id.clone(), fm.frames.get_mut(&main_id).unwrap());
993 fm.frames.insert(child_id, child);
994
995 let watcher = NavigationWatcher::until_load(
997 NavigationId(0),
998 main_id.clone(),
999 Some(LoaderId::from("loader-old".to_string())),
1000 );
1001
1002 fm.frames.get_mut(&main_id).unwrap().loader_id =
1004 Some(LoaderId::from("loader-new".to_string()));
1005
1006 let main_frame = fm.frames.get(&main_id).unwrap();
1009 let result = fm.check_lifecycle_complete(&watcher, main_frame);
1010 assert!(
1011 result.is_some(),
1012 "navigation should complete without waiting for child frames"
1013 );
1014 }
1015
1016 #[test]
1017 fn navigation_watcher_tracks_main_frame_id_change() {
1018 let timeout = Duration::from_secs(30);
1019 let mut fm = FrameManager::new(timeout);
1020
1021 let old_id = FrameId::new("old-main");
1023 let mut main_frame = Frame::new(old_id.clone());
1024 main_frame.loader_id = Some(LoaderId::from("loader-1".to_string()));
1025 fm.frames.insert(old_id.clone(), main_frame);
1026 fm.main_frame = Some(old_id.clone());
1027
1028 let watcher = NavigationWatcher::until_load(
1031 NavigationId(0),
1032 old_id.clone(),
1033 Some(LoaderId::from("loader-1".to_string())),
1034 );
1035 let deadline = Instant::now() + timeout;
1036 fm.navigation = Some((watcher, deadline));
1037
1038 let new_id = FrameId::new("new-main");
1042 if let Some(mut old_frame) = fm.frames.remove(&old_id) {
1043 old_frame.child_frames.clear();
1044 old_frame.id = new_id.clone();
1045 fm.frames.insert(new_id.clone(), old_frame);
1046 }
1047 fm.main_frame = Some(new_id.clone());
1048
1049 if let Some((watcher, _)) = fm.navigation.as_mut() {
1051 if watcher.frame_id == old_id {
1052 watcher.frame_id = new_id.clone();
1053 }
1054 }
1055
1056 let (watcher, _) = fm.navigation.as_ref().unwrap();
1058 assert_eq!(
1059 watcher.frame_id, new_id,
1060 "watcher should follow the main frame ID change"
1061 );
1062
1063 fm.frames.get_mut(&new_id).unwrap().loader_id =
1065 Some(LoaderId::from("loader-2".to_string()));
1066 fm.frames
1067 .get_mut(&new_id)
1068 .unwrap()
1069 .lifecycle_events
1070 .insert("load".into());
1071
1072 let event = fm.poll(Instant::now());
1073 assert!(
1074 matches!(event, Some(FrameEvent::NavigationResult(Ok(_)))),
1075 "navigation should complete on the new frame"
1076 );
1077 }
1078
1079 fn seed_main_frame(fm: &mut FrameManager, loader: &str) -> FrameId {
1082 let id = FrameId::new("main");
1083 let mut frame = Frame::new(id.clone());
1084 frame.loader_id = Some(LoaderId::from(loader.to_string()));
1085 fm.frames.insert(id.clone(), frame);
1086 fm.main_frame = Some(id.clone());
1087 id
1088 }
1089
1090 fn active_watcher(fm: &mut FrameManager, frame_id: FrameId) {
1091 let watcher = NavigationWatcher::until_load(
1092 NavigationId(0),
1093 frame_id.clone(),
1094 fm.frames.get(&frame_id).and_then(|f| f.loader_id.clone()),
1095 );
1096 let deadline = Instant::now() + Duration::from_secs(30);
1097 fm.navigation = Some((watcher, deadline));
1098 }
1099
1100 #[test]
1101 fn nav_loop_guard_none_allows_unlimited() {
1102 let mut fm = FrameManager::new(Duration::from_secs(30));
1103 let id = seed_main_frame(&mut fm, "loader-0");
1105 active_watcher(&mut fm, id);
1106
1107 for _ in 0..25 {
1109 fm.main_frame_nav_count = fm.main_frame_nav_count.saturating_add(1);
1110 }
1111
1112 let event = fm.poll(Instant::now());
1116 assert!(
1117 !matches!(
1118 event,
1119 Some(FrameEvent::NavigationResult(Err(
1120 NavigationError::TooManyNavigations { .. }
1121 )))
1122 ),
1123 "None cap must never emit TooManyNavigations"
1124 );
1125 }
1126
1127 #[test]
1128 fn nav_loop_guard_caps_and_reports_count() {
1129 let mut fm = FrameManager::new(Duration::from_secs(30));
1130 fm.set_max_main_frame_navigations(Some(3));
1131 let id = seed_main_frame(&mut fm, "loader-0");
1132 active_watcher(&mut fm, id);
1133
1134 for _ in 0..5 {
1136 fm.main_frame_nav_count = fm.main_frame_nav_count.saturating_add(1);
1137 }
1138
1139 match fm.poll(Instant::now()) {
1140 Some(FrameEvent::NavigationResult(Err(NavigationError::TooManyNavigations {
1141 count,
1142 ..
1143 }))) => {
1144 assert_eq!(count, 5, "reported count must be the observed value");
1145 }
1146 other => panic!("expected TooManyNavigations, got {other:?}"),
1147 }
1148 }
1149
1150 #[test]
1151 fn nav_loop_guard_resets_on_goto() {
1152 let mut fm = FrameManager::new(Duration::from_secs(30));
1153 fm.set_max_main_frame_navigations(Some(3));
1154 let id = seed_main_frame(&mut fm, "loader-0");
1155 active_watcher(&mut fm, id.clone());
1156
1157 fm.main_frame_nav_count = 10;
1159 assert!(matches!(
1160 fm.poll(Instant::now()),
1161 Some(FrameEvent::NavigationResult(Err(
1162 NavigationError::TooManyNavigations { .. }
1163 )))
1164 ));
1165
1166 fm.navigate_frame(
1168 id.clone(),
1169 FrameRequestedNavigation::new(
1170 NavigationId(1),
1171 Request::new("Page.navigate".into(), serde_json::json!({})),
1172 Duration::from_secs(30),
1173 ),
1174 );
1175 assert_eq!(
1176 fm.main_frame_nav_count, 0,
1177 "navigate_frame must reset the main-frame nav counter"
1178 );
1179 }
1180
1181 #[test]
1182 fn nav_loop_guard_same_document_not_counted() {
1183 let mut fm = FrameManager::new(Duration::from_secs(30));
1189 fm.set_max_main_frame_navigations(Some(2));
1190 let id = seed_main_frame(&mut fm, "loader-0");
1191
1192 active_watcher(&mut fm, id.clone());
1194
1195 for _ in 0..10 {
1197 fm.on_frame_navigated_within_document(
1198 &chromiumoxide_cdp::cdp::browser_protocol::page::EventNavigatedWithinDocument {
1199 frame_id: id.clone(),
1200 url: "https://example.com/#a".into(),
1201 navigation_type:
1202 chromiumoxide_cdp::cdp::browser_protocol::page::NavigatedWithinDocumentNavigationType::Fragment,
1203 },
1204 );
1205 }
1206
1207 assert_eq!(
1208 fm.main_frame_nav_count, 0,
1209 "same-document navigations must not count against the cross-document cap"
1210 );
1211 }
1212
1213 #[test]
1214 fn execution_contexts_cleared_resets_isolated_worlds() {
1215 let mut fm = FrameManager::new(Duration::from_secs(30));
1221
1222 let frame_id = FrameId::new("main");
1223 let frame = Frame::new(frame_id.clone());
1224 let world_name = frame.get_isolated_world_name().clone();
1225 fm.frames.insert(frame_id.clone(), frame);
1226 fm.main_frame = Some(frame_id);
1227
1228 let first = fm.ensure_isolated_world(&world_name);
1231 assert!(
1232 first.is_some(),
1233 "first ensure_isolated_world must emit a creation command chain"
1234 );
1235 assert!(
1236 fm.isolated_worlds.contains(&world_name),
1237 "isolated_worlds must record the ensured world"
1238 );
1239
1240 let second = fm.ensure_isolated_world(&world_name);
1242 assert!(
1243 second.is_none(),
1244 "second ensure_isolated_world must short-circuit while membership is present"
1245 );
1246
1247 fm.on_execution_contexts_cleared();
1249 assert!(
1250 fm.context_ids.is_empty(),
1251 "context_ids must be cleared after executionContextsCleared"
1252 );
1253 assert!(
1254 fm.isolated_worlds.is_empty(),
1255 "isolated_worlds must be cleared after executionContextsCleared"
1256 );
1257
1258 let third = fm.ensure_isolated_world(&world_name);
1261 assert!(
1262 third.is_some(),
1263 "ensure_isolated_world must re-emit a creation chain after a context wipe"
1264 );
1265 assert!(
1266 fm.isolated_worlds.contains(&world_name),
1267 "isolated_worlds must re-record the world after re-ensuring"
1268 );
1269 }
1270}