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 self.isolated_worlds.clear();
631 }
632
633 pub fn evict_stale_context_ids(&mut self) {
637 if !self.context_ids.is_empty() {
638 self.context_ids
639 .retain(|_, fid| self.frames.contains_key(fid));
640 }
641 }
642
643 pub fn on_page_lifecycle_event(&mut self, event: &EventLifecycleEvent) {
645 if let Some(frame) = self.frames.get_mut(&event.frame_id) {
646 if event.name == "init" {
647 frame.loader_id = Some(event.loader_id.clone());
648 frame.lifecycle_events.clear();
649 }
650 frame.lifecycle_events.insert(event.name.clone().into());
651 }
652 }
653
654 fn remove_frames_recursively(&mut self, id: &FrameId) -> Option<Frame> {
656 if let Some(mut frame) = self.frames.remove(id) {
657 for child in &frame.child_frames {
658 self.remove_frames_recursively(child);
659 }
660 if let Some(parent_id) = frame.parent_frame.take() {
661 if let Some(parent) = self.frames.get_mut(&parent_id) {
662 parent.child_frames.remove(&frame.id);
663 }
664 }
665 Some(frame)
666 } else {
667 None
668 }
669 }
670
671 pub fn ensure_isolated_world(&mut self, world_name: &str) -> Option<CommandChain> {
672 if self.isolated_worlds.contains(world_name) {
673 return None;
674 }
675
676 self.isolated_worlds.insert(world_name.to_string());
677
678 if let Ok(cmd) = AddScriptToEvaluateOnNewDocumentParams::builder()
679 .source(format!("//# sourceURL={}", *EVALUATION_SCRIPT_URL))
680 .world_name(world_name)
681 .build()
682 {
683 let mut cmds = Vec::with_capacity(self.frames.len() + 1);
684 let identifier = cmd.identifier();
685
686 if let Ok(cmd) = serde_json::to_value(cmd) {
687 cmds.push((identifier, cmd));
688 }
689
690 let cm = self.frames.keys().filter_map(|id| {
691 if let Ok(cmd) = CreateIsolatedWorldParams::builder()
692 .frame_id(id.clone())
693 .grant_univeral_access(true)
694 .world_name(world_name)
695 .build()
696 {
697 let cm = (
698 cmd.identifier(),
699 serde_json::to_value(cmd).unwrap_or_default(),
700 );
701
702 Some(cm)
703 } else {
704 None
705 }
706 });
707
708 cmds.extend(cm);
709
710 Some(CommandChain::new(cmds, self.request_timeout))
711 } else {
712 None
713 }
714 }
715}
716
717#[derive(Debug)]
718pub enum FrameEvent {
719 NavigationResult(Result<NavigationOk, NavigationError>),
721 NavigationRequest(NavigationId, Request),
723 }
726
727#[derive(Debug)]
728pub enum NavigationError {
729 Timeout {
730 id: NavigationId,
731 err: DeadlineExceeded,
732 },
733 FrameNotFound {
734 id: NavigationId,
735 frame: FrameId,
736 },
737 TooManyNavigations {
742 id: NavigationId,
743 count: u32,
745 },
746}
747
748impl NavigationError {
749 pub fn navigation_id(&self) -> &NavigationId {
750 match self {
751 NavigationError::Timeout { id, .. } => id,
752 NavigationError::FrameNotFound { id, .. } => id,
753 NavigationError::TooManyNavigations { id, .. } => id,
754 }
755 }
756}
757
758#[derive(Debug, Clone, Eq, PartialEq)]
759pub enum NavigationOk {
760 SameDocumentNavigation(NavigationId),
761 NewDocumentNavigation(NavigationId),
762}
763
764impl NavigationOk {
765 pub fn navigation_id(&self) -> &NavigationId {
766 match self {
767 NavigationOk::SameDocumentNavigation(id) => id,
768 NavigationOk::NewDocumentNavigation(id) => id,
769 }
770 }
771}
772
773#[derive(Debug)]
775pub struct NavigationWatcher {
776 id: NavigationId,
777 expected_lifecycle: HashSet<MethodId>,
778 frame_id: FrameId,
779 loader_id: Option<LoaderId>,
780 same_document_navigation: bool,
785}
786
787impl NavigationWatcher {
788 pub fn until_lifecycle(
791 id: NavigationId,
792 frame: FrameId,
793 loader_id: Option<LoaderId>,
794 events: &[LifecycleEvent],
795 ) -> Self {
796 let expected_lifecycle = events.iter().map(LifecycleEvent::to_method_id).collect();
797
798 Self {
799 id,
800 expected_lifecycle,
801 frame_id: frame,
802 loader_id,
803 same_document_navigation: false,
804 }
805 }
806
807 pub fn until_load(id: NavigationId, frame: FrameId, loader_id: Option<LoaderId>) -> Self {
809 Self::until_lifecycle(id, frame, loader_id, &[LifecycleEvent::Load])
810 }
811
812 pub fn until_domcontent_loaded(
814 id: NavigationId,
815 frame: FrameId,
816 loader_id: Option<LoaderId>,
817 ) -> Self {
818 Self::until_lifecycle(id, frame, loader_id, &[LifecycleEvent::DomcontentLoaded])
819 }
820
821 pub fn until_network_idle(
823 id: NavigationId,
824 frame: FrameId,
825 loader_id: Option<LoaderId>,
826 ) -> Self {
827 Self::until_lifecycle(id, frame, loader_id, &[LifecycleEvent::NetworkIdle])
828 }
829
830 pub fn until_network_almost_idle(
832 id: NavigationId,
833 frame: FrameId,
834 loader_id: Option<LoaderId>,
835 ) -> Self {
836 Self::until_lifecycle(id, frame, loader_id, &[LifecycleEvent::NetworkAlmostIdle])
837 }
838
839 pub fn until_domcontent_and_network_idle(
841 id: NavigationId,
842 frame: FrameId,
843 loader_id: Option<LoaderId>,
844 ) -> Self {
845 Self::until_lifecycle(
846 id,
847 frame,
848 loader_id,
849 &[
850 LifecycleEvent::DomcontentLoaded,
851 LifecycleEvent::NetworkIdle,
852 ],
853 )
854 }
855
856 pub fn is_lifecycle_complete(&self) -> bool {
858 self.expected_lifecycle.is_empty()
859 }
860
861 fn on_frame_navigated_within_document(&mut self, ev: &EventNavigatedWithinDocument) {
862 if self.frame_id == ev.frame_id {
863 self.same_document_navigation = true;
864 }
865 }
866}
867
868#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
870pub struct NavigationId(pub usize);
871
872#[derive(Debug)]
874pub struct FrameRequestedNavigation {
875 pub id: NavigationId,
877 pub req: Request,
879 pub timeout: Duration,
881}
882
883impl FrameRequestedNavigation {
884 pub fn new(id: NavigationId, req: Request, request_timeout: Duration) -> Self {
885 Self {
886 id,
887 req,
888 timeout: request_timeout,
889 }
890 }
891
892 pub fn set_frame_id(&mut self, frame_id: FrameId) {
894 if let Some(params) = self.req.params.as_object_mut() {
895 if let Entry::Vacant(entry) = params.entry("frameId") {
896 entry.insert(serde_json::Value::String(frame_id.into()));
897 }
898 }
899 }
900}
901
902#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
903pub enum LifecycleEvent {
904 #[default]
905 Load,
906 DomcontentLoaded,
907 NetworkIdle,
908 NetworkAlmostIdle,
909}
910
911impl LifecycleEvent {
912 #[inline]
913 pub fn to_method_id(&self) -> MethodId {
914 match self {
915 LifecycleEvent::Load => "load".into(),
916 LifecycleEvent::DomcontentLoaded => "DOMContentLoaded".into(),
917 LifecycleEvent::NetworkIdle => "networkIdle".into(),
918 LifecycleEvent::NetworkAlmostIdle => "networkAlmostIdle".into(),
919 }
920 }
921}
922
923impl AsRef<str> for LifecycleEvent {
924 fn as_ref(&self) -> &str {
925 match self {
926 LifecycleEvent::Load => "load",
927 LifecycleEvent::DomcontentLoaded => "DOMContentLoaded",
928 LifecycleEvent::NetworkIdle => "networkIdle",
929 LifecycleEvent::NetworkAlmostIdle => "networkAlmostIdle",
930 }
931 }
932}
933
934#[cfg(test)]
935mod tests {
936 use super::*;
937
938 #[test]
939 fn frame_lifecycle_events_cleared_on_loading_started() {
940 let mut frame = Frame::new(FrameId::new("test"));
941
942 frame.lifecycle_events.insert("load".into());
944 frame.lifecycle_events.insert("DOMContentLoaded".into());
945 assert!(frame.is_loaded());
946
947 frame.on_loading_started();
949 assert!(!frame.is_loaded());
950 }
951
952 #[test]
953 fn frame_loading_stopped_inserts_load_events() {
954 let mut frame = Frame::new(FrameId::new("test"));
955 assert!(!frame.is_loaded());
956
957 frame.on_loading_stopped();
958 assert!(frame.is_loaded());
959 }
960
961 #[test]
962 fn navigation_completes_when_main_frame_loaded_despite_child_frames() {
963 let timeout = Duration::from_secs(30);
964 let mut fm = FrameManager::new(timeout);
965
966 let main_id = FrameId::new("main");
968 let mut main_frame = Frame::new(main_id.clone());
969 main_frame.loader_id = Some(LoaderId::from("loader-old".to_string()));
970 main_frame.lifecycle_events.insert("load".into());
971 fm.frames.insert(main_id.clone(), main_frame);
972 fm.main_frame = Some(main_id.clone());
973
974 let child_id = FrameId::new("child-ad");
976 let child = Frame::with_parent(child_id.clone(), fm.frames.get_mut(&main_id).unwrap());
977 fm.frames.insert(child_id, child);
978
979 let watcher = NavigationWatcher::until_load(
981 NavigationId(0),
982 main_id.clone(),
983 Some(LoaderId::from("loader-old".to_string())),
984 );
985
986 fm.frames.get_mut(&main_id).unwrap().loader_id =
988 Some(LoaderId::from("loader-new".to_string()));
989
990 let main_frame = fm.frames.get(&main_id).unwrap();
993 let result = fm.check_lifecycle_complete(&watcher, main_frame);
994 assert!(
995 result.is_some(),
996 "navigation should complete without waiting for child frames"
997 );
998 }
999
1000 #[test]
1001 fn navigation_watcher_tracks_main_frame_id_change() {
1002 let timeout = Duration::from_secs(30);
1003 let mut fm = FrameManager::new(timeout);
1004
1005 let old_id = FrameId::new("old-main");
1007 let mut main_frame = Frame::new(old_id.clone());
1008 main_frame.loader_id = Some(LoaderId::from("loader-1".to_string()));
1009 fm.frames.insert(old_id.clone(), main_frame);
1010 fm.main_frame = Some(old_id.clone());
1011
1012 let watcher = NavigationWatcher::until_load(
1015 NavigationId(0),
1016 old_id.clone(),
1017 Some(LoaderId::from("loader-1".to_string())),
1018 );
1019 let deadline = Instant::now() + timeout;
1020 fm.navigation = Some((watcher, deadline));
1021
1022 let new_id = FrameId::new("new-main");
1026 if let Some(mut old_frame) = fm.frames.remove(&old_id) {
1027 old_frame.child_frames.clear();
1028 old_frame.id = new_id.clone();
1029 fm.frames.insert(new_id.clone(), old_frame);
1030 }
1031 fm.main_frame = Some(new_id.clone());
1032
1033 if let Some((watcher, _)) = fm.navigation.as_mut() {
1035 if watcher.frame_id == old_id {
1036 watcher.frame_id = new_id.clone();
1037 }
1038 }
1039
1040 let (watcher, _) = fm.navigation.as_ref().unwrap();
1042 assert_eq!(
1043 watcher.frame_id, new_id,
1044 "watcher should follow the main frame ID change"
1045 );
1046
1047 fm.frames.get_mut(&new_id).unwrap().loader_id =
1049 Some(LoaderId::from("loader-2".to_string()));
1050 fm.frames
1051 .get_mut(&new_id)
1052 .unwrap()
1053 .lifecycle_events
1054 .insert("load".into());
1055
1056 let event = fm.poll(Instant::now());
1057 assert!(
1058 matches!(event, Some(FrameEvent::NavigationResult(Ok(_)))),
1059 "navigation should complete on the new frame"
1060 );
1061 }
1062
1063 fn seed_main_frame(fm: &mut FrameManager, loader: &str) -> FrameId {
1066 let id = FrameId::new("main");
1067 let mut frame = Frame::new(id.clone());
1068 frame.loader_id = Some(LoaderId::from(loader.to_string()));
1069 fm.frames.insert(id.clone(), frame);
1070 fm.main_frame = Some(id.clone());
1071 id
1072 }
1073
1074 fn active_watcher(fm: &mut FrameManager, frame_id: FrameId) {
1075 let watcher = NavigationWatcher::until_load(
1076 NavigationId(0),
1077 frame_id.clone(),
1078 fm.frames.get(&frame_id).and_then(|f| f.loader_id.clone()),
1079 );
1080 let deadline = Instant::now() + Duration::from_secs(30);
1081 fm.navigation = Some((watcher, deadline));
1082 }
1083
1084 #[test]
1085 fn nav_loop_guard_none_allows_unlimited() {
1086 let mut fm = FrameManager::new(Duration::from_secs(30));
1087 let id = seed_main_frame(&mut fm, "loader-0");
1089 active_watcher(&mut fm, id);
1090
1091 for _ in 0..25 {
1093 fm.main_frame_nav_count = fm.main_frame_nav_count.saturating_add(1);
1094 }
1095
1096 let event = fm.poll(Instant::now());
1100 assert!(
1101 !matches!(
1102 event,
1103 Some(FrameEvent::NavigationResult(Err(
1104 NavigationError::TooManyNavigations { .. }
1105 )))
1106 ),
1107 "None cap must never emit TooManyNavigations"
1108 );
1109 }
1110
1111 #[test]
1112 fn nav_loop_guard_caps_and_reports_count() {
1113 let mut fm = FrameManager::new(Duration::from_secs(30));
1114 fm.set_max_main_frame_navigations(Some(3));
1115 let id = seed_main_frame(&mut fm, "loader-0");
1116 active_watcher(&mut fm, id);
1117
1118 for _ in 0..5 {
1120 fm.main_frame_nav_count = fm.main_frame_nav_count.saturating_add(1);
1121 }
1122
1123 match fm.poll(Instant::now()) {
1124 Some(FrameEvent::NavigationResult(Err(NavigationError::TooManyNavigations {
1125 count,
1126 ..
1127 }))) => {
1128 assert_eq!(count, 5, "reported count must be the observed value");
1129 }
1130 other => panic!("expected TooManyNavigations, got {other:?}"),
1131 }
1132 }
1133
1134 #[test]
1135 fn nav_loop_guard_resets_on_goto() {
1136 let mut fm = FrameManager::new(Duration::from_secs(30));
1137 fm.set_max_main_frame_navigations(Some(3));
1138 let id = seed_main_frame(&mut fm, "loader-0");
1139 active_watcher(&mut fm, id.clone());
1140
1141 fm.main_frame_nav_count = 10;
1143 assert!(matches!(
1144 fm.poll(Instant::now()),
1145 Some(FrameEvent::NavigationResult(Err(
1146 NavigationError::TooManyNavigations { .. }
1147 )))
1148 ));
1149
1150 fm.navigate_frame(
1152 id.clone(),
1153 FrameRequestedNavigation::new(
1154 NavigationId(1),
1155 Request::new("Page.navigate".into(), serde_json::json!({})),
1156 Duration::from_secs(30),
1157 ),
1158 );
1159 assert_eq!(
1160 fm.main_frame_nav_count, 0,
1161 "navigate_frame must reset the main-frame nav counter"
1162 );
1163 }
1164
1165 #[test]
1166 fn nav_loop_guard_same_document_not_counted() {
1167 let mut fm = FrameManager::new(Duration::from_secs(30));
1173 fm.set_max_main_frame_navigations(Some(2));
1174 let id = seed_main_frame(&mut fm, "loader-0");
1175
1176 active_watcher(&mut fm, id.clone());
1178
1179 for _ in 0..10 {
1181 fm.on_frame_navigated_within_document(
1182 &chromiumoxide_cdp::cdp::browser_protocol::page::EventNavigatedWithinDocument {
1183 frame_id: id.clone(),
1184 url: "https://example.com/#a".into(),
1185 navigation_type:
1186 chromiumoxide_cdp::cdp::browser_protocol::page::NavigatedWithinDocumentNavigationType::Fragment,
1187 },
1188 );
1189 }
1190
1191 assert_eq!(
1192 fm.main_frame_nav_count, 0,
1193 "same-document navigations must not count against the cross-document cap"
1194 );
1195 }
1196
1197 #[test]
1198 fn execution_contexts_cleared_resets_isolated_worlds() {
1199 let mut fm = FrameManager::new(Duration::from_secs(30));
1205
1206 let frame_id = FrameId::new("main");
1207 let frame = Frame::new(frame_id.clone());
1208 let world_name = frame.get_isolated_world_name().clone();
1209 fm.frames.insert(frame_id.clone(), frame);
1210 fm.main_frame = Some(frame_id);
1211
1212 let first = fm.ensure_isolated_world(&world_name);
1215 assert!(
1216 first.is_some(),
1217 "first ensure_isolated_world must emit a creation command chain"
1218 );
1219 assert!(
1220 fm.isolated_worlds.contains(&world_name),
1221 "isolated_worlds must record the ensured world"
1222 );
1223
1224 let second = fm.ensure_isolated_world(&world_name);
1226 assert!(
1227 second.is_none(),
1228 "second ensure_isolated_world must short-circuit while membership is present"
1229 );
1230
1231 fm.on_execution_contexts_cleared();
1233 assert!(
1234 fm.context_ids.is_empty(),
1235 "context_ids must be cleared after executionContextsCleared"
1236 );
1237 assert!(
1238 fm.isolated_worlds.is_empty(),
1239 "isolated_worlds must be cleared after executionContextsCleared"
1240 );
1241
1242 let third = fm.ensure_isolated_world(&world_name);
1245 assert!(
1246 third.is_some(),
1247 "ensure_isolated_world must re-emit a creation chain after a context wipe"
1248 );
1249 assert!(
1250 fm.isolated_worlds.contains(&world_name),
1251 "isolated_worlds must re-record the world after re-ensuring"
1252 );
1253 }
1254}