viewpoint_core/page/frame/
mod.rs1use std::sync::Arc;
10use std::time::Duration;
11
12use parking_lot::RwLock;
13use tracing::{debug, info, instrument};
14use viewpoint_cdp::protocol::page::{NavigateParams, NavigateResult};
15use viewpoint_cdp::protocol::runtime::EvaluateParams;
16use viewpoint_cdp::CdpConnection;
17
18use crate::error::{NavigationError, PageError};
19use crate::wait::{DocumentLoadState, LoadStateWaiter};
20
21const DEFAULT_NAVIGATION_TIMEOUT: Duration = Duration::from_secs(30);
23
24#[derive(Debug, Clone)]
26struct FrameData {
27 url: String,
29 name: String,
31 detached: bool,
33}
34
35#[derive(Debug)]
40pub struct Frame {
41 connection: Arc<CdpConnection>,
43 session_id: String,
45 id: String,
47 parent_id: Option<String>,
49 loader_id: String,
51 data: RwLock<FrameData>,
53}
54
55impl Frame {
56 pub(crate) fn new(
58 connection: Arc<CdpConnection>,
59 session_id: String,
60 id: String,
61 parent_id: Option<String>,
62 loader_id: String,
63 url: String,
64 name: String,
65 ) -> Self {
66 Self {
67 connection,
68 session_id,
69 id,
70 parent_id,
71 loader_id,
72 data: RwLock::new(FrameData {
73 url,
74 name,
75 detached: false,
76 }),
77 }
78 }
79
80 pub fn id(&self) -> &str {
82 &self.id
83 }
84
85 pub fn parent_id(&self) -> Option<&str> {
89 self.parent_id.as_deref()
90 }
91
92 pub fn is_main(&self) -> bool {
94 self.parent_id.is_none()
95 }
96
97 pub fn loader_id(&self) -> &str {
99 &self.loader_id
100 }
101
102 pub fn url(&self) -> String {
104 self.data.read().url.clone()
105 }
106
107 pub fn name(&self) -> String {
109 self.data.read().name.clone()
110 }
111
112 pub fn is_detached(&self) -> bool {
114 self.data.read().detached
115 }
116
117 pub(crate) fn set_url(&self, url: String) {
119 self.data.write().url = url;
120 }
121
122 pub(crate) fn set_name(&self, name: String) {
124 self.data.write().name = name;
125 }
126
127 pub(crate) fn set_detached(&self) {
129 self.data.write().detached = true;
130 }
131
132 #[instrument(level = "debug", skip(self), fields(frame_id = %self.id))]
138 pub async fn content(&self) -> Result<String, PageError> {
139 if self.is_detached() {
140 return Err(PageError::EvaluationFailed("Frame is detached".to_string()));
141 }
142
143 let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
144 .connection
145 .send_command(
146 "Runtime.evaluate",
147 Some(EvaluateParams {
148 expression: "document.documentElement.outerHTML".to_string(),
149 object_group: None,
150 include_command_line_api: None,
151 silent: Some(true),
152 context_id: None, return_by_value: Some(true),
154 await_promise: Some(false),
155 }),
156 Some(&self.session_id),
157 )
158 .await?;
159
160 result
161 .result
162 .value
163 .and_then(|v| v.as_str().map(ToString::to_string))
164 .ok_or_else(|| PageError::EvaluationFailed("Failed to get content".to_string()))
165 }
166
167 #[instrument(level = "debug", skip(self), fields(frame_id = %self.id))]
173 pub async fn title(&self) -> Result<String, PageError> {
174 if self.is_detached() {
175 return Err(PageError::EvaluationFailed("Frame is detached".to_string()));
176 }
177
178 let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
179 .connection
180 .send_command(
181 "Runtime.evaluate",
182 Some(EvaluateParams {
183 expression: "document.title".to_string(),
184 object_group: None,
185 include_command_line_api: None,
186 silent: Some(true),
187 context_id: None, return_by_value: Some(true),
189 await_promise: Some(false),
190 }),
191 Some(&self.session_id),
192 )
193 .await?;
194
195 result
196 .result
197 .value
198 .and_then(|v| v.as_str().map(ToString::to_string))
199 .ok_or_else(|| PageError::EvaluationFailed("Failed to get title".to_string()))
200 }
201
202 #[instrument(level = "info", skip(self), fields(frame_id = %self.id, url = %url))]
208 pub async fn goto(&self, url: &str) -> Result<(), NavigationError> {
209 self.goto_with_options(url, DocumentLoadState::Load, DEFAULT_NAVIGATION_TIMEOUT)
210 .await
211 }
212
213 #[instrument(level = "info", skip(self), fields(frame_id = %self.id, url = %url, wait_until = ?wait_until))]
219 pub async fn goto_with_options(
220 &self,
221 url: &str,
222 wait_until: DocumentLoadState,
223 timeout: Duration,
224 ) -> Result<(), NavigationError> {
225 if self.is_detached() {
226 return Err(NavigationError::Cancelled);
227 }
228
229 info!("Navigating frame to URL");
230
231 let event_rx = self.connection.subscribe_events();
233 let mut waiter = LoadStateWaiter::new(event_rx, self.session_id.clone(), self.id.clone());
234
235 debug!("Sending Page.navigate command for frame");
237 let result: NavigateResult = self
238 .connection
239 .send_command(
240 "Page.navigate",
241 Some(NavigateParams {
242 url: url.to_string(),
243 referrer: None,
244 transition_type: None,
245 frame_id: Some(self.id.clone()),
246 }),
247 Some(&self.session_id),
248 )
249 .await?;
250
251 debug!(frame_id = %result.frame_id, "Page.navigate completed for frame");
252
253 if let Some(error_text) = result.error_text {
255 return Err(NavigationError::NetworkError(error_text));
256 }
257
258 waiter.set_commit_received().await;
260
261 debug!(wait_until = ?wait_until, "Waiting for load state");
263 waiter
264 .wait_for_load_state_with_timeout(wait_until, timeout)
265 .await?;
266
267 self.set_url(url.to_string());
269
270 info!(frame_id = %self.id, "Frame navigation completed");
271 Ok(())
272 }
273
274 #[instrument(level = "info", skip(self, html), fields(frame_id = %self.id))]
280 pub async fn set_content(&self, html: &str) -> Result<(), PageError> {
281 if self.is_detached() {
282 return Err(PageError::EvaluationFailed("Frame is detached".to_string()));
283 }
284
285 use viewpoint_cdp::protocol::page::SetDocumentContentParams;
286
287 self.connection
288 .send_command::<_, serde_json::Value>(
289 "Page.setDocumentContent",
290 Some(SetDocumentContentParams {
291 frame_id: self.id.clone(),
292 html: html.to_string(),
293 }),
294 Some(&self.session_id),
295 )
296 .await?;
297
298 info!("Frame content set");
299 Ok(())
300 }
301
302 #[instrument(level = "debug", skip(self), fields(frame_id = %self.id, state = ?state))]
308 pub async fn wait_for_load_state(&self, state: DocumentLoadState) -> Result<(), NavigationError> {
309 self.wait_for_load_state_with_timeout(state, DEFAULT_NAVIGATION_TIMEOUT)
310 .await
311 }
312
313 #[instrument(level = "debug", skip(self), fields(frame_id = %self.id, state = ?state, timeout_ms = timeout.as_millis()))]
319 pub async fn wait_for_load_state_with_timeout(
320 &self,
321 state: DocumentLoadState,
322 timeout: Duration,
323 ) -> Result<(), NavigationError> {
324 if self.is_detached() {
325 return Err(NavigationError::Cancelled);
326 }
327
328 let event_rx = self.connection.subscribe_events();
329 let mut waiter = LoadStateWaiter::new(event_rx, self.session_id.clone(), self.id.clone());
330
331 waiter.set_commit_received().await;
333
334 waiter.wait_for_load_state_with_timeout(state, timeout).await?;
335
336 debug!("Frame reached load state {:?}", state);
337 Ok(())
338 }
339
340 pub(crate) fn session_id(&self) -> &str {
342 &self.session_id
343 }
344
345 pub(crate) fn connection(&self) -> &Arc<CdpConnection> {
347 &self.connection
348 }
349
350 #[instrument(level = "debug", skip(self), fields(frame_id = %self.id))]
358 pub async fn child_frames(&self) -> Result<Vec<Frame>, PageError> {
359 if self.is_detached() {
360 return Err(PageError::EvaluationFailed("Frame is detached".to_string()));
361 }
362
363 let result: viewpoint_cdp::protocol::page::GetFrameTreeResult = self
365 .connection
366 .send_command("Page.getFrameTree", None::<()>, Some(&self.session_id))
367 .await?;
368
369 let children = find_child_frames(
371 &result.frame_tree,
372 &self.id,
373 &self.connection,
374 &self.session_id,
375 );
376
377 Ok(children)
378 }
379
380 #[instrument(level = "debug", skip(self), fields(frame_id = %self.id))]
388 pub async fn parent_frame(&self) -> Result<Option<Frame>, PageError> {
389 if self.is_detached() {
390 return Err(PageError::EvaluationFailed("Frame is detached".to_string()));
391 }
392
393 if self.is_main() {
395 return Ok(None);
396 }
397
398 let result: viewpoint_cdp::protocol::page::GetFrameTreeResult = self
400 .connection
401 .send_command("Page.getFrameTree", None::<()>, Some(&self.session_id))
402 .await?;
403
404 let parent = find_parent_frame(
406 &result.frame_tree,
407 &self.id,
408 &self.connection,
409 &self.session_id,
410 );
411
412 Ok(parent)
413 }
414}
415
416fn find_child_frames(
418 tree: &viewpoint_cdp::protocol::page::FrameTree,
419 parent_id: &str,
420 connection: &Arc<CdpConnection>,
421 session_id: &str,
422) -> Vec<Frame> {
423 let mut children = Vec::new();
424
425 if tree.frame.id == parent_id {
427 if let Some(ref child_frames) = tree.child_frames {
429 for child in child_frames {
430 children.push(Frame::new(
431 connection.clone(),
432 session_id.to_string(),
433 child.frame.id.clone(),
434 Some(parent_id.to_string()),
435 child.frame.loader_id.clone(),
436 child.frame.url.clone(),
437 child.frame.name.clone().unwrap_or_default(),
438 ));
439 }
440 }
441 } else {
442 if let Some(ref child_frames) = tree.child_frames {
444 for child in child_frames {
445 let found = find_child_frames(child, parent_id, connection, session_id);
446 children.extend(found);
447 }
448 }
449 }
450
451 children
452}
453
454fn find_parent_frame(
456 tree: &viewpoint_cdp::protocol::page::FrameTree,
457 frame_id: &str,
458 connection: &Arc<CdpConnection>,
459 session_id: &str,
460) -> Option<Frame> {
461 if let Some(ref child_frames) = tree.child_frames {
463 for child in child_frames {
464 if child.frame.id == frame_id {
465 return Some(Frame::new(
467 connection.clone(),
468 session_id.to_string(),
469 tree.frame.id.clone(),
470 tree.frame.parent_id.clone(),
471 tree.frame.loader_id.clone(),
472 tree.frame.url.clone(),
473 tree.frame.name.clone().unwrap_or_default(),
474 ));
475 }
476 }
477
478 for child in child_frames {
480 if let Some(parent) = find_parent_frame(child, frame_id, connection, session_id) {
481 return Some(parent);
482 }
483 }
484 }
485
486 None
487}
488
489