use crate::element_handle::ElementHandle;
use crate::error::{Error, Result};
use crate::options::{
CheckOptions, ClickOptions, DragToOptions, FillOptions, GotoOptions, HoverOptions, PressOptions,
PressSequentiallyOptions, SelectOption, SelectOptions, WaitForFunctionOptions, WaitForOptions, WaitUntil,
};
use crate::page::Page;
use crate::response::Response;
use crate::types::AriaRole;
use serde_json::{json, Value};
use std::time::Duration;
use tokio::sync::broadcast;
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct AddScriptTagOptions {
pub url: Option<String>,
pub source: Option<String>,
pub r#type: Option<String>,
}
impl AddScriptTagOptions {
pub fn new() -> Self {
Self::default()
}
pub fn url(mut self, v: impl Into<String>) -> Self {
self.url = Some(v.into());
self
}
pub fn source(mut self, v: impl Into<String>) -> Self {
self.source = Some(v.into());
self
}
pub fn type_(mut self, v: impl Into<String>) -> Self {
self.r#type = Some(v.into());
self
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct AddStyleTagOptions {
pub url: Option<String>,
pub source: Option<String>,
}
impl AddStyleTagOptions {
pub fn new() -> Self {
Self::default()
}
pub fn url(mut self, v: impl Into<String>) -> Self {
self.url = Some(v.into());
self
}
pub fn source(mut self, v: impl Into<String>) -> Self {
self.source = Some(v.into());
self
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct FrameWaitForUrlOptions {
pub timeout: Option<Duration>,
pub wait_until: Option<WaitUntil>,
}
impl FrameWaitForUrlOptions {
pub fn new() -> Self {
Self::default()
}
pub fn timeout(mut self, v: Duration) -> Self {
self.timeout = Some(v);
self
}
pub fn wait_until(mut self, v: WaitUntil) -> Self {
self.wait_until = Some(v);
self
}
}
#[derive(Clone)]
pub struct Frame {
page: Page,
frame_id: String,
}
impl Frame {
pub(crate) fn new(page: Page, frame_id: String) -> Self {
Self { page, frame_id }
}
pub(crate) fn main(page: Page) -> Self {
let frame_id = page.main_frame_id().unwrap_or_default();
Self { page, frame_id }
}
pub fn page(&self) -> Page {
self.page.clone()
}
pub fn frame_id(&self) -> &str {
&self.frame_id
}
pub fn name(&self) -> String {
self.page
.frame_data(&self.frame_id)
.map(|d| d.name)
.unwrap_or_default()
}
pub fn url(&self) -> String {
self.page
.frame_data(&self.frame_id)
.map(|d| d.url)
.unwrap_or_default()
}
pub fn parent_frame(&self) -> Option<Frame> {
let parent = self.page.frame_data(&self.frame_id)?.parent_id.clone()?;
Some(Frame::new(self.page.clone(), parent))
}
pub fn child_frames(&self) -> Vec<Frame> {
self.page
.frame_ids_with_parent(&self.frame_id)
.into_iter()
.map(|id| Frame::new(self.page.clone(), id))
.collect()
}
pub fn is_detached(&self) -> bool {
self.page
.frame_data(&self.frame_id)
.map(|d| d.detached)
.unwrap_or(false)
}
pub async fn evaluate<R: serde::de::DeserializeOwned>(&self, expression: &str) -> Result<R> {
let ctx = self.page.ctx_for_frame(&self.frame_id).await;
let function = format!("(arg) => {{ return ({expression}); }}");
let v = crate::selectors::eval_context(self.page.session(), ctx, &function, Value::Null)
.await?;
serde_json::from_value::<R>(v).map_err(Error::from)
}
pub async fn evaluate_handle(&self, expression: &str) -> Result<crate::js_handle::JSHandle> {
let ctx = self.page.ctx_for_frame(&self.frame_id).await;
let function = format!("(arg) => {{ return ({expression}); }}");
let oid = crate::selectors::eval_context_handle(
self.page.session(),
ctx,
&function,
Value::Null,
)
.await?
.ok_or_else(|| {
Error::ProtocolError(
"evaluate_handle did not return a remote object (no objectId)".into(),
)
})?;
Ok(crate::js_handle::JSHandle::new(self.page.session_arc(), oid))
}
pub async fn goto(
&self,
url: &str,
opts: Option<GotoOptions>,
) -> Result<Option<Response>> {
if self.frame_id != self.page.main_frame_id().unwrap_or_default() {
return Err(Error::InvalidArgument(
"Frame::goto on non-main frames is not supported".into(),
));
}
self.page.goto(url, opts).await
}
pub async fn wait_for_load_state(&self, state: Option<WaitUntil>) -> Result<()> {
self.page.wait_for_load_state(state).await
}
pub async fn wait_for_function<R: serde::de::DeserializeOwned>(
&self,
expression: &str,
arg: Option<serde_json::Value>,
options: Option<WaitForFunctionOptions>,
) -> Result<R> {
self.page.wait_for_function(expression, arg, options).await
}
pub async fn content(&self) -> Result<String> {
self.evaluate::<String>("document.documentElement.outerHTML").await
}
pub async fn set_content(&self, html: &str) -> Result<()> {
let ctx = self.page.ctx_for_frame(&self.frame_id).await;
let _ = crate::selectors::eval_context(
self.page.session(),
ctx,
"(html) => { document.open(); document.write(html); document.close(); }",
json!(html),
)
.await?;
Ok(())
}
pub async fn title(&self) -> Result<String> {
self.evaluate::<String>("document.title").await
}
pub fn locator(&self, selector: impl Into<String>) -> crate::locator::Locator {
let ctx = self.page.context_for_frame(&self.frame_id);
crate::locator::Locator::new_in_frame_ctx(
self.page.clone(),
ctx,
selector.into(),
true,
None,
)
}
pub fn get_by_text(&self, text: &str, exact: bool) -> crate::locator::Locator {
let sel = if exact {
format!("text=\"{text}\"")
} else {
format!("text={text}")
};
self.locator(sel)
}
pub fn get_by_label(&self, text: &str) -> crate::locator::Locator {
self.locator(format!("[aria-label=\"{}\"]", attr_escape(text)))
}
pub fn get_by_role(
&self,
role: AriaRole,
opts: Option<crate::options::GetByRoleOptions>,
) -> crate::locator::Locator {
let opts = opts.unwrap_or_default();
let mut sel = format!("role={}", role.as_str());
if let Some(name) = &opts.name {
sel.push_str(&format!("[name=\"{name}\"]"));
}
if opts.exact == Some(true) {
sel.push_str("[exact=\"true\"]");
}
self.locator(sel)
}
pub fn get_by_placeholder(&self, text: &str) -> crate::locator::Locator {
self.locator(format!("[placeholder=\"{}\"]", attr_escape(text)))
}
pub fn get_by_alt_text(&self, text: &str) -> crate::locator::Locator {
self.locator(format!("[alt=\"{}\"]", attr_escape(text)))
}
pub fn get_by_title(&self, text: &str) -> crate::locator::Locator {
self.locator(format!("[title=\"{}\"]", attr_escape(text)))
}
pub fn get_by_test_id(&self, text: &str) -> crate::locator::Locator {
self.locator(format!("[data-testid=\"{}\"]", attr_escape(text)))
}
pub async fn add_script_tag(&self, opts: Option<AddScriptTagOptions>) -> Result<()> {
let opts = opts.unwrap_or_default();
if opts.url.is_none() && opts.source.is_none() {
return Err(Error::InvalidArgument(
"AddScriptTagOptions requires either `url` or `source`".into(),
));
}
let arg = json!({ "url": opts.url, "source": opts.source, "type": opts.r#type });
let ctx = self.page.ctx_for_frame(&self.frame_id).await;
let _ = crate::selectors::eval_context(
self.page.session(),
ctx,
"(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); }",
arg,
)
.await?;
Ok(())
}
pub async fn add_style_tag(&self, opts: Option<AddStyleTagOptions>) -> Result<()> {
let opts = opts.unwrap_or_default();
if opts.url.is_none() && opts.source.is_none() {
return Err(Error::InvalidArgument(
"AddStyleTagOptions requires either `url` or `source`".into(),
));
}
let arg = json!({ "url": opts.url, "source": opts.source });
let ctx = self.page.ctx_for_frame(&self.frame_id).await;
let _ = crate::selectors::eval_context(
self.page.session(),
ctx,
"(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); } }",
arg,
)
.await?;
Ok(())
}
pub async fn wait_for_url(&self, url: &str, opts: Option<FrameWaitForUrlOptions>) -> Result<()> {
let opts = opts.unwrap_or_default();
let timeout = opts
.timeout
.unwrap_or_else(|| self.page.default_navigation_timeout());
let deadline = tokio::time::Instant::now() + timeout;
if glob_matches(url, &self.url()) {
if let Some(state) = opts.wait_until {
self.wait_for_load_state(Some(state)).await?;
}
return Ok(());
}
let mut rx = self.page.session().subscribe();
loop {
if glob_matches(url, &self.url()) {
break;
}
if tokio::time::Instant::now() >= deadline {
return Err(Error::Timeout(format!(
"wait_for_url '{url}' timed out after {}ms (last: '{}')",
timeout.as_millis(),
self.url()
)));
}
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
match tokio::time::timeout(remaining, rx.recv()).await {
Ok(Ok(ev))
if frame_event_matches(&ev, &self.frame_id) =>
{
continue;
}
Ok(Ok(_)) => continue,
Ok(Err(broadcast::error::RecvError::Closed)) => {
return Err(Error::ChannelClosed);
}
Ok(Err(broadcast::error::RecvError::Lagged(_))) => continue,
Err(_) => {
return Err(Error::Timeout(format!(
"wait_for_url '{url}' timed out after {}ms (last: '{}')",
timeout.as_millis(),
self.url()
)));
}
}
}
if let Some(state) = opts.wait_until {
self.wait_for_load_state(Some(state)).await?;
}
Ok(())
}
pub async fn click(&self, selector: &str, opts: Option<ClickOptions>) -> Result<()> {
self.locator(selector).click(opts).await
}
pub async fn dblclick(&self, selector: &str, opts: Option<ClickOptions>) -> Result<()> {
self.locator(selector).dblclick(opts).await
}
pub async fn fill(&self, selector: &str, value: &str, opts: Option<FillOptions>) -> Result<()> {
self.locator(selector).fill(value, opts).await
}
pub async fn focus(&self, selector: &str) -> Result<()> {
self.locator(selector).focus().await
}
pub async fn hover(&self, selector: &str, opts: Option<HoverOptions>) -> Result<()> {
self.locator(selector).hover(opts).await
}
pub async fn press(&self, selector: &str, key: &str, opts: Option<PressOptions>) -> Result<()> {
self.locator(selector).press(key, opts).await
}
pub async fn select_option(
&self,
selector: &str,
value: impl Into<SelectOption>,
opts: Option<SelectOptions>,
) -> Result<Vec<String>> {
self.locator(selector).select_option(value, opts).await
}
pub async fn set_input_files(&self, selector: &str, files: &[&str]) -> Result<()> {
self.locator(selector).set_input_files(files).await
}
pub async fn tap(&self, selector: &str, opts: Option<ClickOptions>) -> Result<()> {
self.locator(selector).tap(opts).await
}
pub async fn check(&self, selector: &str, opts: Option<CheckOptions>) -> Result<()> {
self.locator(selector).check(opts).await
}
pub async fn uncheck(&self, selector: &str, opts: Option<CheckOptions>) -> Result<()> {
self.locator(selector).uncheck(opts).await
}
pub async fn set_checked(
&self,
selector: &str,
checked: bool,
opts: Option<CheckOptions>,
) -> Result<()> {
self.locator(selector)
.set_checked(checked, opts)
.await
}
pub async fn type_(
&self,
selector: &str,
text: &str,
opts: Option<PressSequentiallyOptions>,
) -> Result<()> {
self.locator(selector).type_(text, opts).await
}
pub async fn text_content(&self, selector: &str) -> Result<Option<String>> {
self.locator(selector).text_content().await
}
pub async fn inner_text(&self, selector: &str) -> Result<String> {
self.locator(selector).inner_text().await
}
pub async fn inner_html(&self, selector: &str) -> Result<String> {
self.locator(selector).inner_html().await
}
pub async fn get_attribute(&self, selector: &str, name: &str) -> Result<Option<String>> {
self.locator(selector).get_attribute(name).await
}
pub async fn count(&self, selector: &str) -> Result<usize> {
self.locator(selector).count().await
}
pub async fn wait_for_selector(
&self,
selector: &str,
opts: Option<WaitForOptions>,
) -> Result<()> {
self.locator(selector).wait_for(opts).await
}
pub async fn eval_on_selector<R: serde::de::DeserializeOwned>(
&self,
selector: &str,
expression: &str,
) -> Result<R> {
let ctx = self.page.ctx_for_frame(&self.frame_id).await;
let object_id = crate::selectors::element_at(self.page.session(), ctx, selector, 0)
.await?
.ok_or_else(|| Error::ElementNotFound(selector.to_string()))?;
let function = format!("(el) => {{ return ({expression}); }}");
let v = crate::selectors::eval_object(self.page.session(), &object_id, &function, Value::Null)
.await?;
self.release_object(&object_id).await;
serde_json::from_value::<R>(v).map_err(Error::from)
}
pub async fn eval_on_selector_all<R: serde::de::DeserializeOwned>(
&self,
selector: &str,
expression: &str,
) -> Result<Vec<R>> {
let ctx = self.page.ctx_for_frame(&self.frame_id).await;
let n = crate::selectors::count(self.page.session(), ctx, selector).await?;
let mut out = Vec::with_capacity(n);
for i in 0..n {
if let Some(oid) =
crate::selectors::element_at(self.page.session(), ctx, selector, i).await?
{
let function = format!("(el) => {{ return ({expression}); }}");
let v =
crate::selectors::eval_object(self.page.session(), &oid, &function, Value::Null)
.await?;
self.release_object(&oid).await;
out.push(serde_json::from_value::<R>(v).map_err(Error::from)?);
}
}
Ok(out)
}
pub async fn dispatch_event(
&self,
selector: &str,
type_: &str,
init: Option<Value>,
) -> Result<()> {
self.locator(selector)
.dispatch_event(type_, init)
.await
}
pub async fn drag_and_drop(
&self,
source: &str,
target: &str,
opts: Option<DragToOptions>,
) -> Result<()> {
self.locator(source).drag_to(&self.locator(target), opts).await
}
pub async fn query_selector(&self, selector: &str) -> Result<Option<ElementHandle>> {
let ctx = self.page.ctx_for_frame(&self.frame_id).await;
let oid = crate::selectors::element_at(self.page.session(), ctx, selector, 0)
.await?
.map(|oid| ElementHandle::new(self.page.clone(), oid));
Ok(oid)
}
pub async fn query_selector_all(&self, selector: &str) -> Result<Vec<ElementHandle>> {
let ctx = self.page.ctx_for_frame(&self.frame_id).await;
let n = crate::selectors::count(self.page.session(), ctx, selector).await?;
let mut out = Vec::with_capacity(n);
for i in 0..n {
if let Some(oid) =
crate::selectors::element_at(self.page.session(), ctx, selector, i).await?
{
out.push(ElementHandle::new(self.page.clone(), oid));
}
}
Ok(out)
}
async fn release_object(&self, object_id: &str) {
let _ = self
.page
.session()
.send("Runtime.releaseObject", json!({ "objectId": object_id }))
.await;
}
#[allow(dead_code)]
fn _json_marker(&self) -> Value {
serde_json::json!({})
}
}
fn attr_escape(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
fn frame_event_matches(ev: &crate::cdp::CdpEvent, frame_id: &str) -> bool {
match ev.method.as_str() {
"Page.frameNavigated" => ev
.params
.get("frame")
.and_then(|f| f.get("id"))
.and_then(|v| v.as_str())
== Some(frame_id),
"Page.lifecycleEvent" => {
ev.params.get("frameId").and_then(|v| v.as_str()) == Some(frame_id)
}
_ => false,
}
}
fn glob_matches(pattern: &str, value: &str) -> bool {
if pattern == value {
return true;
}
if pattern.len() >= 2 && pattern.starts_with('*') && pattern.ends_with('*') {
return value.contains(&pattern[1..pattern.len() - 1]);
}
if let Some(rest) = pattern.strip_prefix('*') {
if value.ends_with(rest) {
return true;
}
}
if let Some(rest) = pattern.strip_suffix('*') {
if value.starts_with(rest) {
return true;
}
}
false
}