use std::sync::Arc;
use crate::backend::FrameInfo;
use crate::locator::Locator;
use crate::options::{RoleOptions, TextOptions, WaitOptions};
use crate::page::Page;
#[derive(Clone)]
pub struct Frame {
page: Arc<Page>,
pub(crate) id: Arc<str>,
pub(crate) parent_id: Option<String>,
name_str: String,
url_str: String,
}
impl Frame {
pub(crate) fn from_info(page: Arc<Page>, info: FrameInfo) -> Self {
Self {
page,
id: Arc::from(info.frame_id),
parent_id: info.parent_frame_id,
name_str: info.name,
url_str: info.url,
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name_str
}
#[must_use]
pub fn url(&self) -> &str {
&self.url_str
}
#[must_use]
pub fn is_main_frame(&self) -> bool {
self.parent_id.is_none()
}
pub async fn parent_frame(&self) -> Result<Option<Frame>, String> {
if let Some(pid) = &self.parent_id {
let frames = self.page.frames().await?;
Ok(frames.into_iter().find(|f| &*f.id == pid.as_str()))
} else {
Ok(None)
}
}
pub async fn child_frames(&self) -> Result<Vec<Frame>, String> {
let frames = self.page.frames().await?;
Ok(
frames
.into_iter()
.filter(|f| f.parent_id.as_deref() == Some(&*self.id))
.collect(),
)
}
pub async fn evaluate(&self, expression: &str) -> Result<Option<serde_json::Value>, String> {
if self.is_main_frame() {
self.page.evaluate(expression).await
} else {
self.page.inner.evaluate_in_frame(expression, &self.id).await
}
}
pub async fn evaluate_str(&self, expression: &str) -> Result<String, String> {
self.evaluate(expression).await.map(|v| {
v.map(|val| {
if let Some(s) = val.as_str() {
s.to_string()
} else {
val.to_string()
}
})
.unwrap_or_default()
})
}
#[must_use]
pub fn locator(&self, selector: &str) -> Locator {
Locator {
page: Arc::clone(&self.page),
selector: selector.to_string(),
frame_id: Some(self.id.clone()),
}
}
#[must_use]
pub fn get_by_role(&self, role: &str, opts: &RoleOptions) -> Locator {
let sel = crate::locator::build_role_selector(role, opts);
Locator {
page: Arc::clone(&self.page),
selector: sel,
frame_id: Some(self.id.clone()),
}
}
#[must_use]
pub fn get_by_text(&self, text: &str, opts: &TextOptions) -> Locator {
let sel = if opts.exact == Some(true) {
format!("text=\"{text}\"")
} else {
format!("text={text}")
};
Locator {
page: Arc::clone(&self.page),
selector: sel,
frame_id: Some(self.id.clone()),
}
}
#[must_use]
pub fn get_by_test_id(&self, test_id: &str) -> Locator {
Locator {
page: Arc::clone(&self.page),
selector: format!("testid={test_id}"),
frame_id: Some(self.id.clone()),
}
}
#[must_use]
pub fn get_by_label(&self, text: &str, opts: &TextOptions) -> Locator {
let sel = if opts.exact == Some(true) {
format!("label=\"{text}\"")
} else {
format!("label={text}")
};
Locator {
page: Arc::clone(&self.page),
selector: sel,
frame_id: Some(self.id.clone()),
}
}
#[must_use]
pub fn get_by_placeholder(&self, text: &str, opts: &TextOptions) -> Locator {
let sel = if opts.exact == Some(true) {
format!("placeholder=\"{text}\"")
} else {
format!("placeholder={text}")
};
Locator {
page: Arc::clone(&self.page),
selector: sel,
frame_id: Some(self.id.clone()),
}
}
pub async fn content(&self) -> Result<String, String> {
let r = self.evaluate("document.documentElement.outerHTML").await?;
Ok(
r.and_then(|v| v.as_str().map(std::string::ToString::to_string))
.unwrap_or_default(),
)
}
pub async fn title(&self) -> Result<String, String> {
let r = self.evaluate("document.title").await?;
Ok(
r.and_then(|v| v.as_str().map(std::string::ToString::to_string))
.unwrap_or_default(),
)
}
pub async fn goto(&self, url: &str) -> Result<(), String> {
if self.is_main_frame() {
self.page.goto(url, None).await
} else {
self
.evaluate(&format!("window.location.href = '{}'", url.replace('\'', "\\'")))
.await?;
Ok(())
}
}
pub async fn wait_for_selector(&self, selector: &str, opts: WaitOptions) -> Result<(), String> {
self.locator(selector).wait_for(opts).await
}
pub async fn is_detached(&self) -> Result<bool, String> {
let frames = self.page.inner().get_frame_tree().await?;
Ok(!frames.iter().any(|f| f.frame_id.as_str() == &*self.id))
}
#[must_use]
pub fn page(&self) -> &Page {
&self.page
}
pub async fn set_content(&self, html: &str) -> Result<(), String> {
let escaped = crate::steps::js_escape(html);
self
.evaluate(&format!("document.documentElement.innerHTML = '{escaped}'"))
.await?;
Ok(())
}
pub async fn add_script_tag(
&self,
url: Option<&str>,
content: Option<&str>,
script_type: Option<&str>,
) -> Result<(), String> {
let t = script_type.unwrap_or("text/javascript");
if let Some(url) = url {
self.evaluate(&format!(
"(function(){{return new Promise(function(r,j){{var s=document.createElement('script');\
s.type='{}';s.src='{}';s.onload=r;s.onerror=function(){{j(new Error('Failed'))}};document.head.appendChild(s)}})}})();",
crate::steps::js_escape(t), crate::steps::js_escape(url)
)).await?;
} else if let Some(content) = content {
self.evaluate(&format!(
"(function(){{var s=document.createElement('script');s.type='{}';s.text='{}';document.head.appendChild(s)}})()",
crate::steps::js_escape(t), crate::steps::js_escape(content)
)).await?;
}
Ok(())
}
pub async fn add_style_tag(&self, url: Option<&str>, content: Option<&str>) -> Result<(), String> {
if let Some(url) = url {
self.evaluate(&format!(
"(function(){{return new Promise(function(r,j){{var l=document.createElement('link');\
l.rel='stylesheet';l.href='{}';l.onload=r;l.onerror=function(){{j(new Error('Failed'))}};document.head.appendChild(l)}})}})();",
crate::steps::js_escape(url)
)).await?;
} else if let Some(content) = content {
self
.evaluate(&format!(
"(function(){{var s=document.createElement('style');s.textContent='{}';document.head.appendChild(s)}})()",
crate::steps::js_escape(content)
))
.await?;
}
Ok(())
}
pub async fn wait_for_load_state(&self) -> Result<(), String> {
if self.is_main_frame() {
self.page.wait_for_load_state(None).await
} else {
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(30);
loop {
if tokio::time::Instant::now() >= deadline {
return Err("Timeout waiting for frame load state".into());
}
if let Ok(Some(v)) = self.evaluate("document.readyState").await {
if v.as_str() == Some("complete") {
return Ok(());
}
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
}
}
}
impl std::fmt::Debug for Frame {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Frame")
.field("id", &self.id)
.field("parent_id", &self.parent_id)
.field("name", &self.name_str)
.field("url", &self.url_str)
.finish_non_exhaustive()
}
}