use std::time::Duration;
use super::locator::{AriaRole, LocatorOptions, Selector};
use crate::Page;
use viewpoint_js::js;
use viewpoint_js_core::escape_js_string_single;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug, Clone)]
pub struct FrameLocator<'a> {
page: &'a Page,
frame_selector: String,
parent_selectors: Vec<String>,
timeout: Duration,
}
impl<'a> FrameLocator<'a> {
pub(crate) fn new(page: &'a Page, selector: impl Into<String>) -> Self {
Self {
page,
frame_selector: selector.into(),
parent_selectors: Vec::new(),
timeout: DEFAULT_TIMEOUT,
}
}
fn with_parent(
page: &'a Page,
frame_selector: String,
mut parent_selectors: Vec<String>,
parent_selector: String,
) -> Self {
parent_selectors.push(parent_selector);
Self {
page,
frame_selector,
parent_selectors,
timeout: DEFAULT_TIMEOUT,
}
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn page(&self) -> &'a Page {
self.page
}
pub fn locator(&self, selector: impl Into<String>) -> FrameElementLocator<'a> {
FrameElementLocator::new(self.clone(), Selector::Css(selector.into()))
}
pub fn get_by_text(&self, text: impl Into<String>) -> FrameElementLocator<'a> {
FrameElementLocator::new(
self.clone(),
Selector::Text {
text: text.into(),
exact: false,
},
)
}
pub fn get_by_text_exact(&self, text: impl Into<String>) -> FrameElementLocator<'a> {
FrameElementLocator::new(
self.clone(),
Selector::Text {
text: text.into(),
exact: true,
},
)
}
pub fn get_by_role(&self, role: AriaRole) -> FrameRoleLocatorBuilder<'a> {
FrameRoleLocatorBuilder::new(self.clone(), role)
}
pub fn get_by_test_id(&self, test_id: impl Into<String>) -> FrameElementLocator<'a> {
FrameElementLocator::new(self.clone(), Selector::TestId(test_id.into()))
}
pub fn get_by_label(&self, label: impl Into<String>) -> FrameElementLocator<'a> {
FrameElementLocator::new(self.clone(), Selector::Label(label.into()))
}
pub fn get_by_placeholder(&self, placeholder: impl Into<String>) -> FrameElementLocator<'a> {
FrameElementLocator::new(self.clone(), Selector::Placeholder(placeholder.into()))
}
pub fn frame_locator(&self, selector: impl Into<String>) -> FrameLocator<'a> {
FrameLocator::with_parent(
self.page,
selector.into(),
self.parent_selectors.clone(),
self.frame_selector.clone(),
)
}
pub fn selector(&self) -> &str {
&self.frame_selector
}
pub fn parent_selectors(&self) -> &[String] {
&self.parent_selectors
}
pub(crate) fn to_js_frame_access(&self) -> String {
let mut js = String::new();
js.push_str("(function() {\n");
js.push_str(" let doc = document;\n");
for parent_selector in &self.parent_selectors {
let escaped_selector = escape_js_string_single(parent_selector);
js.push_str(" const parent = doc.querySelector(");
js.push_str(&escaped_selector);
js.push_str(");\n");
js.push_str(" if (!parent || !parent.contentDocument) return null;\n");
js.push_str(" doc = parent.contentDocument;\n");
}
let escaped_frame_selector = escape_js_string_single(&self.frame_selector);
js.push_str(" const frame = doc.querySelector(");
js.push_str(&escaped_frame_selector);
js.push_str(");\n");
js.push_str(" if (!frame || !frame.contentDocument) return null;\n");
js.push_str(" return frame.contentDocument;\n");
js.push_str("})()");
js
}
}
#[derive(Debug, Clone)]
pub struct FrameElementLocator<'a> {
frame_locator: FrameLocator<'a>,
selector: Selector,
options: LocatorOptions,
}
impl<'a> FrameElementLocator<'a> {
fn new(frame_locator: FrameLocator<'a>, selector: Selector) -> Self {
Self {
frame_locator,
selector,
options: LocatorOptions::default(),
}
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.options.timeout = timeout;
self
}
#[must_use]
pub fn locator(&self, selector: impl Into<String>) -> FrameElementLocator<'a> {
FrameElementLocator {
frame_locator: self.frame_locator.clone(),
selector: Selector::Chained(
Box::new(self.selector.clone()),
Box::new(Selector::Css(selector.into())),
),
options: self.options.clone(),
}
}
#[must_use]
pub fn first(&self) -> FrameElementLocator<'a> {
FrameElementLocator {
frame_locator: self.frame_locator.clone(),
selector: Selector::Nth {
base: Box::new(self.selector.clone()),
index: 0,
},
options: self.options.clone(),
}
}
#[must_use]
pub fn last(&self) -> FrameElementLocator<'a> {
FrameElementLocator {
frame_locator: self.frame_locator.clone(),
selector: Selector::Nth {
base: Box::new(self.selector.clone()),
index: -1,
},
options: self.options.clone(),
}
}
#[must_use]
pub fn nth(&self, index: i32) -> FrameElementLocator<'a> {
FrameElementLocator {
frame_locator: self.frame_locator.clone(),
selector: Selector::Nth {
base: Box::new(self.selector.clone()),
index,
},
options: self.options.clone(),
}
}
pub fn frame_locator(&self) -> &FrameLocator<'a> {
&self.frame_locator
}
pub fn selector(&self) -> &Selector {
&self.selector
}
pub(crate) fn options(&self) -> &LocatorOptions {
&self.options
}
fn to_js_expression(&self) -> String {
let frame_access = self.frame_locator.to_js_frame_access();
let element_selector = self.selector.to_js_expression();
js! {
(function() {
const frameDoc = @{frame_access};
if (!frameDoc) return { found: false, count: 0, error: "Frame not found or not accessible" };
const originalDocument = document;
try {
const elements = (function() {
const document = frameDoc;
return Array.from(@{element_selector});
})();
return elements;
} catch (e) {
return [];
}
})()
}
}
}
#[derive(Debug)]
pub struct FrameRoleLocatorBuilder<'a> {
frame_locator: FrameLocator<'a>,
role: AriaRole,
name: Option<String>,
}
impl<'a> FrameRoleLocatorBuilder<'a> {
fn new(frame_locator: FrameLocator<'a>, role: AriaRole) -> Self {
Self {
frame_locator,
role,
name: None,
}
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn build(self) -> FrameElementLocator<'a> {
FrameElementLocator::new(
self.frame_locator,
Selector::Role {
role: self.role,
name: self.name,
},
)
}
}
impl<'a> From<FrameRoleLocatorBuilder<'a>> for FrameElementLocator<'a> {
fn from(builder: FrameRoleLocatorBuilder<'a>) -> Self {
builder.build()
}
}