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_network_idle(&self) -> bool {
194 self.lifecycle_events.contains("networkIdle")
195 }
196
197 pub fn is_network_almost_idle(&self) -> bool {
199 self.lifecycle_events.contains("networkAlmostIdle")
200 }
201
202 pub fn clear_contexts(&mut self) {
203 self.main_world.take_context();
204 self.secondary_world.take_context();
205 }
206
207 pub fn destroy_context(&mut self, ctx_unique_id: &str) {
208 if self.main_world.execution_context_unique_id() == Some(ctx_unique_id) {
209 self.main_world.take_context();
210 } else if self.secondary_world.execution_context_unique_id() == Some(ctx_unique_id) {
211 self.secondary_world.take_context();
212 }
213 }
214
215 pub fn execution_context(&self) -> Option<ExecutionContextId> {
216 self.main_world.execution_context()
217 }
218
219 pub fn set_request(&mut self, request: HttpRequest) {
220 self.http_request = Some(Arc::new(request))
221 }
222}
223
224#[derive(Debug)]
228pub struct FrameManager {
229 main_frame: Option<FrameId>,
230 frames: HashMap<FrameId, Frame>,
231 context_ids: HashMap<String, FrameId>,
233 isolated_worlds: HashSet<String>,
234 request_timeout: Duration,
237 pending_navigations: VecDeque<(FrameRequestedNavigation, NavigationWatcher)>,
239 navigation: Option<(NavigationWatcher, Instant)>,
241}
242
243impl FrameManager {
244 pub fn new(request_timeout: Duration) -> Self {
245 FrameManager {
246 main_frame: None,
247 frames: Default::default(),
248 context_ids: Default::default(),
249 isolated_worlds: Default::default(),
250 request_timeout,
251 pending_navigations: Default::default(),
252 navigation: None,
253 }
254 }
255
256 pub fn init_commands(timeout: Duration) -> CommandChain {
258 let enable = page::EnableParams::default();
259 let get_tree = page::GetFrameTreeParams::default();
260 let set_lifecycle = page::SetLifecycleEventsEnabledParams::new(true);
261 let mut commands = Vec::with_capacity(3);
265
266 let enable_id = enable.identifier();
267 let get_tree_id = get_tree.identifier();
268 let set_lifecycle_id = set_lifecycle.identifier();
269 if let Ok(value) = serde_json::to_value(enable) {
273 commands.push((enable_id, value));
274 }
275
276 if let Ok(value) = serde_json::to_value(get_tree) {
277 commands.push((get_tree_id, value));
278 }
279
280 if let Ok(value) = serde_json::to_value(set_lifecycle) {
281 commands.push((set_lifecycle_id, value));
282 }
283
284 CommandChain::new(commands, timeout)
293 }
294
295 pub fn main_frame(&self) -> Option<&Frame> {
296 self.main_frame.as_ref().and_then(|id| self.frames.get(id))
297 }
298
299 pub fn main_frame_mut(&mut self) -> Option<&mut Frame> {
300 if let Some(id) = self.main_frame.as_ref() {
301 self.frames.get_mut(id)
302 } else {
303 None
304 }
305 }
306
307 pub fn get_isolated_world_name(&self) -> Option<&String> {
309 self.main_frame
310 .as_ref()
311 .and_then(|id| self.frames.get(id).map(|fid| fid.get_isolated_world_name()))
312 }
313
314 pub fn frames(&self) -> impl Iterator<Item = &Frame> + '_ {
315 self.frames.values()
316 }
317
318 pub fn frame(&self, id: &FrameId) -> Option<&Frame> {
319 self.frames.get(id)
320 }
321
322 fn check_lifecycle(&self, watcher: &NavigationWatcher, frame: &Frame) -> bool {
323 watcher.expected_lifecycle.iter().all(|ev| {
324 frame.lifecycle_events.contains(ev)
325 || (frame.url.is_none() && frame.lifecycle_events.contains("DOMContentLoaded"))
326 })
327 }
328
329 fn check_lifecycle_complete(
330 &self,
331 watcher: &NavigationWatcher,
332 frame: &Frame,
333 ) -> Option<NavigationOk> {
334 if !self.check_lifecycle(watcher, frame) {
335 return None;
336 }
337 if frame.loader_id == watcher.loader_id && !watcher.same_document_navigation {
338 return None;
339 }
340 if watcher.same_document_navigation {
341 return Some(NavigationOk::SameDocumentNavigation(watcher.id));
342 }
343 if frame.loader_id != watcher.loader_id {
344 return Some(NavigationOk::NewDocumentNavigation(watcher.id));
345 }
346 None
347 }
348
349 pub fn on_http_request_finished(&mut self, request: HttpRequest) {
351 if let Some(id) = request.frame.as_ref() {
352 if let Some(frame) = self.frames.get_mut(id) {
353 frame.set_request(request);
354 }
355 }
356 }
357
358 pub fn poll(&mut self, now: Instant) -> Option<FrameEvent> {
359 if let Some((watcher, deadline)) = self.navigation.take() {
361 if now > deadline {
362 return Some(FrameEvent::NavigationResult(Err(
364 NavigationError::Timeout {
365 err: DeadlineExceeded::new(now, deadline),
366 id: watcher.id,
367 },
368 )));
369 }
370
371 if let Some(frame) = self.frames.get(&watcher.frame_id) {
372 if let Some(nav) = self.check_lifecycle_complete(&watcher, frame) {
373 return Some(FrameEvent::NavigationResult(Ok(nav)));
376 } else {
377 self.navigation = Some((watcher, deadline));
379 }
380 } else {
381 return Some(FrameEvent::NavigationResult(Err(
382 NavigationError::FrameNotFound {
383 frame: watcher.frame_id,
384 id: watcher.id,
385 },
386 )));
387 }
388 } else if let Some((req, watcher)) = self.pending_navigations.pop_front() {
389 let deadline = Instant::now() + req.timeout;
391 self.navigation = Some((watcher, deadline));
392 return Some(FrameEvent::NavigationRequest(req.id, req.req));
393 }
394 None
395 }
396
397 pub fn goto(&mut self, req: FrameRequestedNavigation) {
399 if let Some(frame_id) = &self.main_frame {
400 self.navigate_frame(frame_id.clone(), req);
401 }
402 }
403
404 pub fn navigate_frame(&mut self, frame_id: FrameId, mut req: FrameRequestedNavigation) {
406 let loader_id = self.frames.get(&frame_id).and_then(|f| f.loader_id.clone());
407 let watcher = NavigationWatcher::until_load(req.id, frame_id.clone(), loader_id);
408
409 req.set_frame_id(frame_id);
411
412 self.pending_navigations.push_back((req, watcher))
413 }
414
415 pub fn on_attached_to_target(&mut self, _event: &EventAttachedToTarget) {
417 }
419
420 pub fn on_frame_tree(&mut self, frame_tree: FrameTree) {
421 self.on_frame_attached(
422 frame_tree.frame.id.clone(),
423 frame_tree.frame.parent_id.clone(),
424 );
425 self.on_frame_navigated(&frame_tree.frame);
426 if let Some(children) = frame_tree.child_frames {
427 for child_tree in children {
428 self.on_frame_tree(child_tree);
429 }
430 }
431 }
432
433 pub fn on_frame_attached(&mut self, frame_id: FrameId, parent_frame_id: Option<FrameId>) {
434 if self.frames.contains_key(&frame_id) {
435 return;
436 }
437 if let Some(parent_frame_id) = parent_frame_id {
438 if let Some(parent_frame) = self.frames.get_mut(&parent_frame_id) {
439 let frame = Frame::with_parent(frame_id.clone(), parent_frame);
440 self.frames.insert(frame_id, frame);
441 }
442 }
443 }
444
445 pub fn on_frame_detached(&mut self, event: &EventFrameDetached) {
446 self.remove_frames_recursively(&event.frame_id);
447 }
448
449 pub fn on_frame_navigated(&mut self, frame: &CdpFrame) {
450 if frame.parent_id.is_some() {
451 if let Some((id, mut f)) = self.frames.remove_entry(&frame.id) {
452 for child in f.child_frames.drain() {
453 self.remove_frames_recursively(&child);
454 }
455 f.navigated(frame);
456 self.frames.insert(id, f);
457 }
458 } else {
459 let old_main = self.main_frame.take();
460 let mut f = if let Some(main) = old_main.as_ref() {
461 if let Some(mut main_frame) = self.frames.remove(main) {
463 for child in &main_frame.child_frames {
464 self.remove_frames_recursively(child);
465 }
466 main_frame.child_frames.clear();
468 main_frame.id = frame.id.clone();
469 main_frame
470 } else {
471 Frame::new(frame.id.clone())
472 }
473 } else {
474 Frame::new(frame.id.clone())
476 };
477 f.navigated(frame);
478 let new_id = f.id.clone();
479 self.main_frame = Some(new_id.clone());
480 self.frames.insert(new_id.clone(), f);
481
482 if old_main.as_ref() != Some(&new_id) {
485 if let Some((watcher, _)) = self.navigation.as_mut() {
486 if old_main.as_ref() == Some(&watcher.frame_id) {
487 watcher.frame_id = new_id;
488 }
489 }
490 }
491 }
492 }
493
494 pub fn on_frame_navigated_within_document(&mut self, event: &EventNavigatedWithinDocument) {
495 if let Some(frame) = self.frames.get_mut(&event.frame_id) {
496 frame.navigated_within_url(event.url.clone());
497 }
498 if let Some((watcher, _)) = self.navigation.as_mut() {
499 watcher.on_frame_navigated_within_document(event);
500 }
501 }
502
503 pub fn on_frame_stopped_loading(&mut self, event: &EventFrameStoppedLoading) {
504 if let Some(frame) = self.frames.get_mut(&event.frame_id) {
505 frame.on_loading_stopped();
506 }
507 }
508
509 pub fn on_frame_started_loading(&mut self, event: &EventFrameStartedLoading) {
511 if let Some(frame) = self.frames.get_mut(&event.frame_id) {
512 frame.on_loading_started();
513 }
514 }
515
516 pub fn on_runtime_binding_called(&mut self, _ev: &EventBindingCalled) {}
518
519 pub fn on_frame_execution_context_created(&mut self, event: &EventExecutionContextCreated) {
521 if let Some(frame_id) = event
522 .context
523 .aux_data
524 .as_ref()
525 .and_then(|v| v["frameId"].as_str())
526 {
527 if let Some(frame) = self.frames.get_mut(frame_id) {
528 if event
529 .context
530 .aux_data
531 .as_ref()
532 .and_then(|v| v["isDefault"].as_bool())
533 .unwrap_or_default()
534 {
535 frame
536 .main_world
537 .set_context(event.context.id, event.context.unique_id.clone());
538 } else if event.context.name == frame.isolated_world_name
539 && frame.secondary_world.execution_context().is_none()
540 {
541 frame
542 .secondary_world
543 .set_context(event.context.id, event.context.unique_id.clone());
544 }
545 self.context_ids
546 .insert(event.context.unique_id.clone(), frame.id.clone());
547 }
548 }
549 if event
550 .context
551 .aux_data
552 .as_ref()
553 .filter(|v| v["type"].as_str() == Some("isolated"))
554 .is_some()
555 {
556 self.isolated_worlds.insert(event.context.name.clone());
557 }
558 }
559
560 pub fn on_frame_execution_context_destroyed(&mut self, event: &EventExecutionContextDestroyed) {
562 if let Some(id) = self.context_ids.remove(&event.execution_context_unique_id) {
563 if let Some(frame) = self.frames.get_mut(&id) {
564 frame.destroy_context(&event.execution_context_unique_id);
565 }
566 }
567 }
568
569 pub fn on_execution_contexts_cleared(&mut self) {
571 for id in self.context_ids.values() {
572 if let Some(frame) = self.frames.get_mut(id) {
573 frame.clear_contexts();
574 }
575 }
576 self.context_ids.clear()
577 }
578
579 pub fn on_page_lifecycle_event(&mut self, event: &EventLifecycleEvent) {
581 if let Some(frame) = self.frames.get_mut(&event.frame_id) {
582 if event.name == "init" {
583 frame.loader_id = Some(event.loader_id.clone());
584 frame.lifecycle_events.clear();
585 }
586 frame.lifecycle_events.insert(event.name.clone().into());
587 }
588 }
589
590 fn remove_frames_recursively(&mut self, id: &FrameId) -> Option<Frame> {
592 if let Some(mut frame) = self.frames.remove(id) {
593 for child in &frame.child_frames {
594 self.remove_frames_recursively(child);
595 }
596 if let Some(parent_id) = frame.parent_frame.take() {
597 if let Some(parent) = self.frames.get_mut(&parent_id) {
598 parent.child_frames.remove(&frame.id);
599 }
600 }
601 Some(frame)
602 } else {
603 None
604 }
605 }
606
607 pub fn ensure_isolated_world(&mut self, world_name: &str) -> Option<CommandChain> {
608 if self.isolated_worlds.contains(world_name) {
609 return None;
610 }
611
612 self.isolated_worlds.insert(world_name.to_string());
613
614 if let Ok(cmd) = AddScriptToEvaluateOnNewDocumentParams::builder()
615 .source(format!("//# sourceURL={}", *EVALUATION_SCRIPT_URL))
616 .world_name(world_name)
617 .build()
618 {
619 let mut cmds = Vec::with_capacity(self.frames.len() + 1);
620 let identifier = cmd.identifier();
621
622 if let Ok(cmd) = serde_json::to_value(cmd) {
623 cmds.push((identifier, cmd));
624 }
625
626 let cm = self.frames.keys().filter_map(|id| {
627 if let Ok(cmd) = CreateIsolatedWorldParams::builder()
628 .frame_id(id.clone())
629 .grant_univeral_access(true)
630 .world_name(world_name)
631 .build()
632 {
633 let cm = (
634 cmd.identifier(),
635 serde_json::to_value(cmd).unwrap_or_default(),
636 );
637
638 Some(cm)
639 } else {
640 None
641 }
642 });
643
644 cmds.extend(cm);
645
646 Some(CommandChain::new(cmds, self.request_timeout))
647 } else {
648 None
649 }
650 }
651}
652
653#[derive(Debug)]
654pub enum FrameEvent {
655 NavigationResult(Result<NavigationOk, NavigationError>),
657 NavigationRequest(NavigationId, Request),
659 }
662
663#[derive(Debug)]
664pub enum NavigationError {
665 Timeout {
666 id: NavigationId,
667 err: DeadlineExceeded,
668 },
669 FrameNotFound {
670 id: NavigationId,
671 frame: FrameId,
672 },
673}
674
675impl NavigationError {
676 pub fn navigation_id(&self) -> &NavigationId {
677 match self {
678 NavigationError::Timeout { id, .. } => id,
679 NavigationError::FrameNotFound { id, .. } => id,
680 }
681 }
682}
683
684#[derive(Debug, Clone, Eq, PartialEq)]
685pub enum NavigationOk {
686 SameDocumentNavigation(NavigationId),
687 NewDocumentNavigation(NavigationId),
688}
689
690impl NavigationOk {
691 pub fn navigation_id(&self) -> &NavigationId {
692 match self {
693 NavigationOk::SameDocumentNavigation(id) => id,
694 NavigationOk::NewDocumentNavigation(id) => id,
695 }
696 }
697}
698
699#[derive(Debug)]
701pub struct NavigationWatcher {
702 id: NavigationId,
703 expected_lifecycle: HashSet<MethodId>,
704 frame_id: FrameId,
705 loader_id: Option<LoaderId>,
706 same_document_navigation: bool,
711}
712
713impl NavigationWatcher {
714 pub fn until_lifecycle(
717 id: NavigationId,
718 frame: FrameId,
719 loader_id: Option<LoaderId>,
720 events: &[LifecycleEvent],
721 ) -> Self {
722 let expected_lifecycle = events.iter().map(LifecycleEvent::to_method_id).collect();
723
724 Self {
725 id,
726 expected_lifecycle,
727 frame_id: frame,
728 loader_id,
729 same_document_navigation: false,
730 }
731 }
732
733 pub fn until_load(id: NavigationId, frame: FrameId, loader_id: Option<LoaderId>) -> Self {
735 Self::until_lifecycle(id, frame, loader_id, &[LifecycleEvent::Load])
736 }
737
738 pub fn until_domcontent_loaded(
740 id: NavigationId,
741 frame: FrameId,
742 loader_id: Option<LoaderId>,
743 ) -> Self {
744 Self::until_lifecycle(id, frame, loader_id, &[LifecycleEvent::DomcontentLoaded])
745 }
746
747 pub fn until_network_idle(
749 id: NavigationId,
750 frame: FrameId,
751 loader_id: Option<LoaderId>,
752 ) -> Self {
753 Self::until_lifecycle(id, frame, loader_id, &[LifecycleEvent::NetworkIdle])
754 }
755
756 pub fn until_network_almost_idle(
758 id: NavigationId,
759 frame: FrameId,
760 loader_id: Option<LoaderId>,
761 ) -> Self {
762 Self::until_lifecycle(id, frame, loader_id, &[LifecycleEvent::NetworkAlmostIdle])
763 }
764
765 pub fn until_domcontent_and_network_idle(
767 id: NavigationId,
768 frame: FrameId,
769 loader_id: Option<LoaderId>,
770 ) -> Self {
771 Self::until_lifecycle(
772 id,
773 frame,
774 loader_id,
775 &[
776 LifecycleEvent::DomcontentLoaded,
777 LifecycleEvent::NetworkIdle,
778 ],
779 )
780 }
781
782 pub fn is_lifecycle_complete(&self) -> bool {
784 self.expected_lifecycle.is_empty()
785 }
786
787 fn on_frame_navigated_within_document(&mut self, ev: &EventNavigatedWithinDocument) {
788 if self.frame_id == ev.frame_id {
789 self.same_document_navigation = true;
790 }
791 }
792}
793
794#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
796pub struct NavigationId(pub usize);
797
798#[derive(Debug)]
800pub struct FrameRequestedNavigation {
801 pub id: NavigationId,
803 pub req: Request,
805 pub timeout: Duration,
807}
808
809impl FrameRequestedNavigation {
810 pub fn new(id: NavigationId, req: Request, request_timeout: Duration) -> Self {
811 Self {
812 id,
813 req,
814 timeout: request_timeout,
815 }
816 }
817
818 pub fn set_frame_id(&mut self, frame_id: FrameId) {
820 if let Some(params) = self.req.params.as_object_mut() {
821 if let Entry::Vacant(entry) = params.entry("frameId") {
822 entry.insert(serde_json::Value::String(frame_id.into()));
823 }
824 }
825 }
826}
827
828#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
829pub enum LifecycleEvent {
830 #[default]
831 Load,
832 DomcontentLoaded,
833 NetworkIdle,
834 NetworkAlmostIdle,
835}
836
837impl LifecycleEvent {
838 #[inline]
839 pub fn to_method_id(&self) -> MethodId {
840 match self {
841 LifecycleEvent::Load => "load".into(),
842 LifecycleEvent::DomcontentLoaded => "DOMContentLoaded".into(),
843 LifecycleEvent::NetworkIdle => "networkIdle".into(),
844 LifecycleEvent::NetworkAlmostIdle => "networkAlmostIdle".into(),
845 }
846 }
847}
848
849impl AsRef<str> for LifecycleEvent {
850 fn as_ref(&self) -> &str {
851 match self {
852 LifecycleEvent::Load => "load",
853 LifecycleEvent::DomcontentLoaded => "DOMContentLoaded",
854 LifecycleEvent::NetworkIdle => "networkIdle",
855 LifecycleEvent::NetworkAlmostIdle => "networkAlmostIdle",
856 }
857 }
858}
859
860#[cfg(test)]
861mod tests {
862 use super::*;
863
864 #[test]
865 fn frame_lifecycle_events_cleared_on_loading_started() {
866 let mut frame = Frame::new(FrameId::new("test"));
867
868 frame.lifecycle_events.insert("load".into());
870 frame.lifecycle_events.insert("DOMContentLoaded".into());
871 assert!(frame.is_loaded());
872
873 frame.on_loading_started();
875 assert!(!frame.is_loaded());
876 }
877
878 #[test]
879 fn frame_loading_stopped_inserts_load_events() {
880 let mut frame = Frame::new(FrameId::new("test"));
881 assert!(!frame.is_loaded());
882
883 frame.on_loading_stopped();
884 assert!(frame.is_loaded());
885 }
886
887 #[test]
888 fn navigation_completes_when_main_frame_loaded_despite_child_frames() {
889 let timeout = Duration::from_secs(30);
890 let mut fm = FrameManager::new(timeout);
891
892 let main_id = FrameId::new("main");
894 let mut main_frame = Frame::new(main_id.clone());
895 main_frame.loader_id = Some(LoaderId::from("loader-old".to_string()));
896 main_frame.lifecycle_events.insert("load".into());
897 fm.frames.insert(main_id.clone(), main_frame);
898 fm.main_frame = Some(main_id.clone());
899
900 let child_id = FrameId::new("child-ad");
902 let child = Frame::with_parent(
903 child_id.clone(),
904 fm.frames.get_mut(&main_id).unwrap(),
905 );
906 fm.frames.insert(child_id, child);
907
908 let watcher = NavigationWatcher::until_load(
910 NavigationId(0),
911 main_id.clone(),
912 Some(LoaderId::from("loader-old".to_string())),
913 );
914
915 fm.frames.get_mut(&main_id).unwrap().loader_id =
917 Some(LoaderId::from("loader-new".to_string()));
918
919 let main_frame = fm.frames.get(&main_id).unwrap();
922 let result = fm.check_lifecycle_complete(&watcher, main_frame);
923 assert!(
924 result.is_some(),
925 "navigation should complete without waiting for child frames"
926 );
927 }
928
929 #[test]
930 fn navigation_watcher_tracks_main_frame_id_change() {
931 let timeout = Duration::from_secs(30);
932 let mut fm = FrameManager::new(timeout);
933
934 let old_id = FrameId::new("old-main");
936 let mut main_frame = Frame::new(old_id.clone());
937 main_frame.loader_id = Some(LoaderId::from("loader-1".to_string()));
938 fm.frames.insert(old_id.clone(), main_frame);
939 fm.main_frame = Some(old_id.clone());
940
941 let watcher = NavigationWatcher::until_load(
944 NavigationId(0),
945 old_id.clone(),
946 Some(LoaderId::from("loader-1".to_string())),
947 );
948 let deadline = Instant::now() + timeout;
949 fm.navigation = Some((watcher, deadline));
950
951 let new_id = FrameId::new("new-main");
955 if let Some(mut old_frame) = fm.frames.remove(&old_id) {
956 old_frame.child_frames.clear();
957 old_frame.id = new_id.clone();
958 fm.frames.insert(new_id.clone(), old_frame);
959 }
960 fm.main_frame = Some(new_id.clone());
961
962 if let Some((watcher, _)) = fm.navigation.as_mut() {
964 if watcher.frame_id == old_id {
965 watcher.frame_id = new_id.clone();
966 }
967 }
968
969 let (watcher, _) = fm.navigation.as_ref().unwrap();
971 assert_eq!(
972 watcher.frame_id, new_id,
973 "watcher should follow the main frame ID change"
974 );
975
976 fm.frames.get_mut(&new_id).unwrap().loader_id =
978 Some(LoaderId::from("loader-2".to_string()));
979 fm.frames
980 .get_mut(&new_id)
981 .unwrap()
982 .lifecycle_events
983 .insert("load".into());
984
985 let event = fm.poll(Instant::now());
986 assert!(
987 matches!(event, Some(FrameEvent::NavigationResult(Ok(_)))),
988 "navigation should complete on the new frame"
989 );
990 }
991}