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