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::CdpConnection;
15use viewpoint_cdp::protocol::page::{NavigateParams, NavigateResult};
16use viewpoint_cdp::protocol::runtime::EvaluateParams;
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(
309 &self,
310 state: DocumentLoadState,
311 ) -> Result<(), NavigationError> {
312 self.wait_for_load_state_with_timeout(state, DEFAULT_NAVIGATION_TIMEOUT)
313 .await
314 }
315
316 #[instrument(level = "debug", skip(self), fields(frame_id = %self.id, state = ?state, timeout_ms = timeout.as_millis()))]
322 pub async fn wait_for_load_state_with_timeout(
323 &self,
324 state: DocumentLoadState,
325 timeout: Duration,
326 ) -> Result<(), NavigationError> {
327 if self.is_detached() {
328 return Err(NavigationError::Cancelled);
329 }
330
331 let event_rx = self.connection.subscribe_events();
332 let mut waiter = LoadStateWaiter::new(event_rx, self.session_id.clone(), self.id.clone());
333
334 waiter.set_commit_received().await;
336
337 waiter
338 .wait_for_load_state_with_timeout(state, timeout)
339 .await?;
340
341 debug!("Frame reached load state {:?}", state);
342 Ok(())
343 }
344
345 pub(crate) fn session_id(&self) -> &str {
347 &self.session_id
348 }
349
350 pub(crate) fn connection(&self) -> &Arc<CdpConnection> {
352 &self.connection
353 }
354
355 #[instrument(level = "debug", skip(self), fields(frame_id = %self.id))]
363 pub async fn child_frames(&self) -> Result<Vec<Frame>, PageError> {
364 if self.is_detached() {
365 return Err(PageError::EvaluationFailed("Frame is detached".to_string()));
366 }
367
368 let result: viewpoint_cdp::protocol::page::GetFrameTreeResult = self
370 .connection
371 .send_command("Page.getFrameTree", None::<()>, Some(&self.session_id))
372 .await?;
373
374 let children = find_child_frames(
376 &result.frame_tree,
377 &self.id,
378 &self.connection,
379 &self.session_id,
380 );
381
382 Ok(children)
383 }
384
385 #[instrument(level = "debug", skip(self), fields(frame_id = %self.id))]
393 pub async fn parent_frame(&self) -> Result<Option<Frame>, PageError> {
394 if self.is_detached() {
395 return Err(PageError::EvaluationFailed("Frame is detached".to_string()));
396 }
397
398 if self.is_main() {
400 return Ok(None);
401 }
402
403 let result: viewpoint_cdp::protocol::page::GetFrameTreeResult = self
405 .connection
406 .send_command("Page.getFrameTree", None::<()>, Some(&self.session_id))
407 .await?;
408
409 let parent = find_parent_frame(
411 &result.frame_tree,
412 &self.id,
413 &self.connection,
414 &self.session_id,
415 );
416
417 Ok(parent)
418 }
419}
420
421fn find_child_frames(
423 tree: &viewpoint_cdp::protocol::page::FrameTree,
424 parent_id: &str,
425 connection: &Arc<CdpConnection>,
426 session_id: &str,
427) -> Vec<Frame> {
428 let mut children = Vec::new();
429
430 if tree.frame.id == parent_id {
432 if let Some(ref child_frames) = tree.child_frames {
434 for child in child_frames {
435 children.push(Frame::new(
436 connection.clone(),
437 session_id.to_string(),
438 child.frame.id.clone(),
439 Some(parent_id.to_string()),
440 child.frame.loader_id.clone(),
441 child.frame.url.clone(),
442 child.frame.name.clone().unwrap_or_default(),
443 ));
444 }
445 }
446 } else {
447 if let Some(ref child_frames) = tree.child_frames {
449 for child in child_frames {
450 let found = find_child_frames(child, parent_id, connection, session_id);
451 children.extend(found);
452 }
453 }
454 }
455
456 children
457}
458
459fn find_parent_frame(
461 tree: &viewpoint_cdp::protocol::page::FrameTree,
462 frame_id: &str,
463 connection: &Arc<CdpConnection>,
464 session_id: &str,
465) -> Option<Frame> {
466 if let Some(ref child_frames) = tree.child_frames {
468 for child in child_frames {
469 if child.frame.id == frame_id {
470 return Some(Frame::new(
472 connection.clone(),
473 session_id.to_string(),
474 tree.frame.id.clone(),
475 tree.frame.parent_id.clone(),
476 tree.frame.loader_id.clone(),
477 tree.frame.url.clone(),
478 tree.frame.name.clone().unwrap_or_default(),
479 ));
480 }
481 }
482
483 for child in child_frames {
485 if let Some(parent) = find_parent_frame(child, frame_id, connection, session_id) {
486 return Some(parent);
487 }
488 }
489 }
490
491 None
492}