1use crate::element_handle::ElementHandle;
4use crate::error::{Error, Result};
5use crate::options::{
6 CheckOptions, ClickOptions, DragToOptions, FillOptions, GotoOptions, HoverOptions, PressOptions,
7 PressSequentiallyOptions, SelectOption, SelectOptions, WaitForFunctionOptions, WaitForOptions, WaitUntil,
8};
9use crate::page::Page;
10use crate::response::Response;
11use crate::types::AriaRole;
12use serde_json::{json, Value};
13use std::time::Duration;
14use tokio::sync::broadcast;
15
16#[derive(Debug, Clone, Default)]
22#[non_exhaustive]
23pub struct AddScriptTagOptions {
24 pub url: Option<String>,
26 pub source: Option<String>,
28 pub r#type: Option<String>,
30}
31
32impl AddScriptTagOptions {
33 pub fn new() -> Self {
34 Self::default()
35 }
36 pub fn url(mut self, v: impl Into<String>) -> Self {
37 self.url = Some(v.into());
38 self
39 }
40 pub fn source(mut self, v: impl Into<String>) -> Self {
41 self.source = Some(v.into());
42 self
43 }
44 pub fn type_(mut self, v: impl Into<String>) -> Self {
45 self.r#type = Some(v.into());
46 self
47 }
48}
49
50#[derive(Debug, Clone, Default)]
55#[non_exhaustive]
56pub struct AddStyleTagOptions {
57 pub url: Option<String>,
59 pub source: Option<String>,
61}
62
63impl AddStyleTagOptions {
64 pub fn new() -> Self {
65 Self::default()
66 }
67 pub fn url(mut self, v: impl Into<String>) -> Self {
68 self.url = Some(v.into());
69 self
70 }
71 pub fn source(mut self, v: impl Into<String>) -> Self {
72 self.source = Some(v.into());
73 self
74 }
75}
76
77#[derive(Debug, Clone, Default)]
82#[non_exhaustive]
83pub struct FrameWaitForUrlOptions {
84 pub timeout: Option<Duration>,
85 pub wait_until: Option<WaitUntil>,
86}
87
88impl FrameWaitForUrlOptions {
89 pub fn new() -> Self {
90 Self::default()
91 }
92 pub fn timeout(mut self, v: Duration) -> Self {
93 self.timeout = Some(v);
94 self
95 }
96 pub fn wait_until(mut self, v: WaitUntil) -> Self {
97 self.wait_until = Some(v);
98 self
99 }
100}
101
102#[derive(Clone)]
104pub struct Frame {
105 page: Page,
106 frame_id: String,
107}
108
109impl Frame {
110 pub(crate) fn new(page: Page, frame_id: String) -> Self {
111 Self { page, frame_id }
112 }
113
114 pub(crate) fn main(page: Page) -> Self {
115 let frame_id = page.main_frame_id().unwrap_or_default();
116 Self { page, frame_id }
117 }
118
119 pub fn page(&self) -> Page {
121 self.page.clone()
122 }
123
124 pub fn frame_id(&self) -> &str {
126 &self.frame_id
127 }
128
129 pub fn name(&self) -> String {
130 self.page
131 .frame_data(&self.frame_id)
132 .map(|d| d.name)
133 .unwrap_or_default()
134 }
135
136 pub fn url(&self) -> String {
137 self.page
138 .frame_data(&self.frame_id)
139 .map(|d| d.url)
140 .unwrap_or_default()
141 }
142
143 pub fn parent_frame(&self) -> Option<Frame> {
144 let parent = self.page.frame_data(&self.frame_id)?.parent_id.clone()?;
145 Some(Frame::new(self.page.clone(), parent))
146 }
147
148 pub fn child_frames(&self) -> Vec<Frame> {
149 self.page
150 .frame_ids_with_parent(&self.frame_id)
151 .into_iter()
152 .map(|id| Frame::new(self.page.clone(), id))
153 .collect()
154 }
155
156 pub fn is_detached(&self) -> bool {
157 self.page
158 .frame_data(&self.frame_id)
159 .map(|d| d.detached)
160 .unwrap_or(false)
161 }
162
163 pub async fn evaluate<R: serde::de::DeserializeOwned>(&self, expression: &str) -> Result<R> {
169 let ctx = self.page.ctx_for_frame(&self.frame_id).await;
170 let function = format!("(arg) => {{ return ({expression}); }}");
171 let v = crate::selectors::eval_context(self.page.session(), ctx, &function, Value::Null)
172 .await?;
173 serde_json::from_value::<R>(v).map_err(Error::from)
174 }
175
176 pub async fn evaluate_handle(&self, expression: &str) -> Result<crate::js_handle::JSHandle> {
185 let ctx = self.page.ctx_for_frame(&self.frame_id).await;
186 let function = format!("(arg) => {{ return ({expression}); }}");
187 let oid = crate::selectors::eval_context_handle(
188 self.page.session(),
189 ctx,
190 &function,
191 Value::Null,
192 )
193 .await?
194 .ok_or_else(|| {
195 Error::ProtocolError(
196 "evaluate_handle did not return a remote object (no objectId)".into(),
197 )
198 })?;
199 Ok(crate::js_handle::JSHandle::new(self.page.session_arc(), oid))
200 }
201
202 pub async fn goto(
204 &self,
205 url: &str,
206 opts: Option<GotoOptions>,
207 ) -> Result<Option<Response>> {
208 if self.frame_id != self.page.main_frame_id().unwrap_or_default() {
209 return Err(Error::InvalidArgument(
210 "Frame::goto on non-main frames is not supported".into(),
211 ));
212 }
213 self.page.goto(url, opts).await
214 }
215
216 pub async fn wait_for_load_state(&self, state: Option<WaitUntil>) -> Result<()> {
217 self.page.wait_for_load_state(state).await
218 }
219
220 pub async fn wait_for_function<R: serde::de::DeserializeOwned>(
223 &self,
224 expression: &str,
225 arg: Option<serde_json::Value>,
226 options: Option<WaitForFunctionOptions>,
227 ) -> Result<R> {
228 self.page.wait_for_function(expression, arg, options).await
229 }
230
231 pub async fn content(&self) -> Result<String> {
233 self.evaluate::<String>("document.documentElement.outerHTML").await
234 }
235
236 pub async fn set_content(&self, html: &str) -> Result<()> {
241 let ctx = self.page.ctx_for_frame(&self.frame_id).await;
242 let _ = crate::selectors::eval_context(
243 self.page.session(),
244 ctx,
245 "(html) => { document.open(); document.write(html); document.close(); }",
246 json!(html),
247 )
248 .await?;
249 Ok(())
250 }
251
252 pub async fn title(&self) -> Result<String> {
254 self.evaluate::<String>("document.title").await
255 }
256
257 pub fn locator(&self, selector: impl Into<String>) -> crate::locator::Locator {
263 let ctx = self.page.context_for_frame(&self.frame_id);
264 crate::locator::Locator::new_in_frame_ctx(
265 self.page.clone(),
266 ctx,
267 selector.into(),
268 true,
269 None,
270 )
271 }
272
273 pub fn get_by_text(&self, text: &str, exact: bool) -> crate::locator::Locator {
274 let sel = if exact {
275 format!("text=\"{text}\"")
276 } else {
277 format!("text={text}")
278 };
279 self.locator(sel)
280 }
281
282 pub fn get_by_label(&self, text: &str) -> crate::locator::Locator {
283 self.locator(format!("[aria-label=\"{}\"]", attr_escape(text)))
284 }
285
286 pub fn get_by_role(
287 &self,
288 role: AriaRole,
289 opts: Option<crate::options::GetByRoleOptions>,
290 ) -> crate::locator::Locator {
291 let opts = opts.unwrap_or_default();
292 let mut sel = format!("role={}", role.as_str());
293 if let Some(name) = &opts.name {
294 sel.push_str(&format!("[name=\"{name}\"]"));
295 }
296 if opts.exact == Some(true) {
297 sel.push_str("[exact=\"true\"]");
298 }
299 self.locator(sel)
300 }
301
302 pub fn get_by_placeholder(&self, text: &str) -> crate::locator::Locator {
303 self.locator(format!("[placeholder=\"{}\"]", attr_escape(text)))
304 }
305
306 pub fn get_by_alt_text(&self, text: &str) -> crate::locator::Locator {
307 self.locator(format!("[alt=\"{}\"]", attr_escape(text)))
308 }
309
310 pub fn get_by_title(&self, text: &str) -> crate::locator::Locator {
311 self.locator(format!("[title=\"{}\"]", attr_escape(text)))
312 }
313
314 pub fn get_by_test_id(&self, text: &str) -> crate::locator::Locator {
315 self.locator(format!("[data-testid=\"{}\"]", attr_escape(text)))
316 }
317
318 pub async fn add_script_tag(&self, opts: Option<AddScriptTagOptions>) -> Result<()> {
327 let opts = opts.unwrap_or_default();
328 if opts.url.is_none() && opts.source.is_none() {
329 return Err(Error::InvalidArgument(
330 "AddScriptTagOptions requires either `url` or `source`".into(),
331 ));
332 }
333 let arg = json!({ "url": opts.url, "source": opts.source, "type": opts.r#type });
334 let ctx = self.page.ctx_for_frame(&self.frame_id).await;
335 let _ = crate::selectors::eval_context(
336 self.page.session(),
337 ctx,
338 "(a) => { const s = document.createElement('script'); if (a.type) s.type = a.type; if (a.url) s.src = a.url; if (a.source) s.textContent = a.source; document.head.appendChild(s); }",
339 arg,
340 )
341 .await?;
342 Ok(())
343 }
344
345 pub async fn add_style_tag(&self, opts: Option<AddStyleTagOptions>) -> Result<()> {
351 let opts = opts.unwrap_or_default();
352 if opts.url.is_none() && opts.source.is_none() {
353 return Err(Error::InvalidArgument(
354 "AddStyleTagOptions requires either `url` or `source`".into(),
355 ));
356 }
357 let arg = json!({ "url": opts.url, "source": opts.source });
358 let ctx = self.page.ctx_for_frame(&self.frame_id).await;
359 let _ = crate::selectors::eval_context(
360 self.page.session(),
361 ctx,
362 "(a) => { if (a.url) { const l = document.createElement('link'); l.rel = 'stylesheet'; l.href = a.url; document.head.appendChild(l); } else { const s = document.createElement('style'); s.textContent = a.source; document.head.appendChild(s); } }",
363 arg,
364 )
365 .await?;
366 Ok(())
367 }
368
369 pub async fn wait_for_url(&self, url: &str, opts: Option<FrameWaitForUrlOptions>) -> Result<()> {
375 let opts = opts.unwrap_or_default();
376 let timeout = opts
377 .timeout
378 .unwrap_or_else(|| self.page.default_navigation_timeout());
379 let deadline = tokio::time::Instant::now() + timeout;
380
381 if glob_matches(url, &self.url()) {
383 if let Some(state) = opts.wait_until {
384 self.wait_for_load_state(Some(state)).await?;
385 }
386 return Ok(());
387 }
388
389 let mut rx = self.page.session().subscribe();
393 loop {
394 if glob_matches(url, &self.url()) {
396 break;
397 }
398 if tokio::time::Instant::now() >= deadline {
399 return Err(Error::Timeout(format!(
400 "wait_for_url '{url}' timed out after {}ms (last: '{}')",
401 timeout.as_millis(),
402 self.url()
403 )));
404 }
405 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
407 match tokio::time::timeout(remaining, rx.recv()).await {
408 Ok(Ok(ev))
409 if frame_event_matches(&ev, &self.frame_id) =>
410 {
411 continue;
412 }
413 Ok(Ok(_)) => continue,
414 Ok(Err(broadcast::error::RecvError::Closed)) => {
415 return Err(Error::ChannelClosed);
416 }
417 Ok(Err(broadcast::error::RecvError::Lagged(_))) => continue,
418 Err(_) => {
419 return Err(Error::Timeout(format!(
420 "wait_for_url '{url}' timed out after {}ms (last: '{}')",
421 timeout.as_millis(),
422 self.url()
423 )));
424 }
425 }
426 }
427 if let Some(state) = opts.wait_until {
428 self.wait_for_load_state(Some(state)).await?;
429 }
430 Ok(())
431 }
432
433 pub async fn click(&self, selector: &str, opts: Option<ClickOptions>) -> Result<()> {
440 self.locator(selector).click(opts).await
441 }
442
443 pub async fn dblclick(&self, selector: &str, opts: Option<ClickOptions>) -> Result<()> {
445 self.locator(selector).dblclick(opts).await
446 }
447
448 pub async fn fill(&self, selector: &str, value: &str, opts: Option<FillOptions>) -> Result<()> {
450 self.locator(selector).fill(value, opts).await
451 }
452
453 pub async fn focus(&self, selector: &str) -> Result<()> {
455 self.locator(selector).focus().await
456 }
457
458 pub async fn hover(&self, selector: &str, opts: Option<HoverOptions>) -> Result<()> {
460 self.locator(selector).hover(opts).await
461 }
462
463 pub async fn press(&self, selector: &str, key: &str, opts: Option<PressOptions>) -> Result<()> {
465 self.locator(selector).press(key, opts).await
466 }
467
468 pub async fn select_option(
470 &self,
471 selector: &str,
472 value: impl Into<SelectOption>,
473 opts: Option<SelectOptions>,
474 ) -> Result<Vec<String>> {
475 self.locator(selector).select_option(value, opts).await
476 }
477
478 pub async fn set_input_files(&self, selector: &str, files: &[&str]) -> Result<()> {
480 self.locator(selector).set_input_files(files).await
481 }
482
483 pub async fn tap(&self, selector: &str, opts: Option<ClickOptions>) -> Result<()> {
485 self.locator(selector).tap(opts).await
486 }
487
488 pub async fn check(&self, selector: &str, opts: Option<CheckOptions>) -> Result<()> {
490 self.locator(selector).check(opts).await
491 }
492
493 pub async fn uncheck(&self, selector: &str, opts: Option<CheckOptions>) -> Result<()> {
495 self.locator(selector).uncheck(opts).await
496 }
497
498 pub async fn set_checked(
500 &self,
501 selector: &str,
502 checked: bool,
503 opts: Option<CheckOptions>,
504 ) -> Result<()> {
505 self.locator(selector)
506 .set_checked(checked, opts)
507 .await
508 }
509
510 pub async fn type_(
512 &self,
513 selector: &str,
514 text: &str,
515 opts: Option<PressSequentiallyOptions>,
516 ) -> Result<()> {
517 self.locator(selector).type_(text, opts).await
518 }
519
520 pub async fn text_content(&self, selector: &str) -> Result<Option<String>> {
522 self.locator(selector).text_content().await
523 }
524
525 pub async fn inner_text(&self, selector: &str) -> Result<String> {
527 self.locator(selector).inner_text().await
528 }
529
530 pub async fn inner_html(&self, selector: &str) -> Result<String> {
532 self.locator(selector).inner_html().await
533 }
534
535 pub async fn get_attribute(&self, selector: &str, name: &str) -> Result<Option<String>> {
537 self.locator(selector).get_attribute(name).await
538 }
539
540 pub async fn count(&self, selector: &str) -> Result<usize> {
542 self.locator(selector).count().await
543 }
544
545 pub async fn wait_for_selector(
547 &self,
548 selector: &str,
549 opts: Option<WaitForOptions>,
550 ) -> Result<()> {
551 self.locator(selector).wait_for(opts).await
552 }
553
554 pub async fn eval_on_selector<R: serde::de::DeserializeOwned>(
557 &self,
558 selector: &str,
559 expression: &str,
560 ) -> Result<R> {
561 let ctx = self.page.ctx_for_frame(&self.frame_id).await;
562 let object_id = crate::selectors::element_at(self.page.session(), ctx, selector, 0)
563 .await?
564 .ok_or_else(|| Error::ElementNotFound(selector.to_string()))?;
565 let function = format!("(el) => {{ return ({expression}); }}");
566 let v = crate::selectors::eval_object(self.page.session(), &object_id, &function, Value::Null)
567 .await?;
568 self.release_object(&object_id).await;
569 serde_json::from_value::<R>(v).map_err(Error::from)
570 }
571
572 pub async fn eval_on_selector_all<R: serde::de::DeserializeOwned>(
575 &self,
576 selector: &str,
577 expression: &str,
578 ) -> Result<Vec<R>> {
579 let ctx = self.page.ctx_for_frame(&self.frame_id).await;
580 let n = crate::selectors::count(self.page.session(), ctx, selector).await?;
581 let mut out = Vec::with_capacity(n);
582 for i in 0..n {
583 if let Some(oid) =
584 crate::selectors::element_at(self.page.session(), ctx, selector, i).await?
585 {
586 let function = format!("(el) => {{ return ({expression}); }}");
587 let v =
588 crate::selectors::eval_object(self.page.session(), &oid, &function, Value::Null)
589 .await?;
590 self.release_object(&oid).await;
591 out.push(serde_json::from_value::<R>(v).map_err(Error::from)?);
592 }
593 }
594 Ok(out)
595 }
596
597 pub async fn dispatch_event(
599 &self,
600 selector: &str,
601 type_: &str,
602 init: Option<Value>,
603 ) -> Result<()> {
604 self.locator(selector)
605 .dispatch_event(type_, init)
606 .await
607 }
608
609 pub async fn drag_and_drop(
611 &self,
612 source: &str,
613 target: &str,
614 opts: Option<DragToOptions>,
615 ) -> Result<()> {
616 self.locator(source).drag_to(&self.locator(target), opts).await
617 }
618
619 pub async fn query_selector(&self, selector: &str) -> Result<Option<ElementHandle>> {
621 let ctx = self.page.ctx_for_frame(&self.frame_id).await;
622 let oid = crate::selectors::element_at(self.page.session(), ctx, selector, 0)
623 .await?
624 .map(|oid| ElementHandle::new(self.page.clone(), oid));
625 Ok(oid)
626 }
627
628 pub async fn query_selector_all(&self, selector: &str) -> Result<Vec<ElementHandle>> {
630 let ctx = self.page.ctx_for_frame(&self.frame_id).await;
631 let n = crate::selectors::count(self.page.session(), ctx, selector).await?;
632 let mut out = Vec::with_capacity(n);
633 for i in 0..n {
634 if let Some(oid) =
635 crate::selectors::element_at(self.page.session(), ctx, selector, i).await?
636 {
637 out.push(ElementHandle::new(self.page.clone(), oid));
638 }
639 }
640 Ok(out)
641 }
642
643 async fn release_object(&self, object_id: &str) {
644 let _ = self
645 .page
646 .session()
647 .send("Runtime.releaseObject", json!({ "objectId": object_id }))
648 .await;
649 }
650
651 #[allow(dead_code)]
653 fn _json_marker(&self) -> Value {
654 serde_json::json!({})
655 }
656}
657
658fn attr_escape(s: &str) -> String {
660 s.replace('\\', "\\\\").replace('"', "\\\"")
661}
662
663fn frame_event_matches(ev: &crate::cdp::CdpEvent, frame_id: &str) -> bool {
667 match ev.method.as_str() {
668 "Page.frameNavigated" => ev
669 .params
670 .get("frame")
671 .and_then(|f| f.get("id"))
672 .and_then(|v| v.as_str())
673 == Some(frame_id),
674 "Page.lifecycleEvent" => {
675 ev.params.get("frameId").and_then(|v| v.as_str()) == Some(frame_id)
676 }
677 _ => false,
678 }
679}
680
681fn glob_matches(pattern: &str, value: &str) -> bool {
685 if pattern == value {
686 return true;
687 }
688 if pattern.len() >= 2 && pattern.starts_with('*') && pattern.ends_with('*') {
690 return value.contains(&pattern[1..pattern.len() - 1]);
691 }
692 if let Some(rest) = pattern.strip_prefix('*') {
693 if value.ends_with(rest) {
694 return true;
695 }
696 }
697 if let Some(rest) = pattern.strip_suffix('*') {
698 if value.starts_with(rest) {
699 return true;
700 }
701 }
702 false
703}