mod actions;
pub mod aria;
pub(crate) mod aria_js;
mod aria_role;
mod builders;
mod debug;
mod element;
mod evaluation;
mod files;
mod filter;
mod helpers;
mod queries;
mod select;
pub(crate) mod selector;
use std::time::Duration;
pub use aria::{AriaCheckedState, AriaSnapshot};
pub use builders::{ClickBuilder, HoverBuilder, TapBuilder, TypeBuilder};
pub use element::{BoundingBox, BoxModel, ElementHandle};
pub use filter::{FilterBuilder, RoleLocatorBuilder};
pub use selector::{AriaRole, Selector, TextOptions};
use crate::Page;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug, Clone)]
pub struct Locator<'a> {
page: &'a Page,
selector: Selector,
options: LocatorOptions,
}
#[derive(Debug, Clone)]
pub struct LocatorOptions {
pub timeout: Duration,
}
impl Default for LocatorOptions {
fn default() -> Self {
Self {
timeout: DEFAULT_TIMEOUT,
}
}
}
impl<'a> Locator<'a> {
pub(crate) fn new(page: &'a Page, selector: Selector) -> Self {
Self {
page,
selector,
options: LocatorOptions::default(),
}
}
pub(crate) fn with_options(
page: &'a Page,
selector: Selector,
options: LocatorOptions,
) -> Self {
Self {
page,
selector,
options,
}
}
pub fn page(&self) -> &'a Page {
self.page
}
pub fn selector(&self) -> &Selector {
&self.selector
}
pub fn options(&self) -> &LocatorOptions {
&self.options
}
#[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>) -> Locator<'a> {
Locator {
page: self.page,
selector: Selector::Chained(
Box::new(self.selector.clone()),
Box::new(Selector::Css(selector.into())),
),
options: self.options.clone(),
}
}
#[must_use]
pub fn first(&self) -> Locator<'a> {
Locator {
page: self.page,
selector: Selector::Nth {
base: Box::new(self.selector.clone()),
index: 0,
},
options: self.options.clone(),
}
}
#[must_use]
pub fn last(&self) -> Locator<'a> {
Locator {
page: self.page,
selector: Selector::Nth {
base: Box::new(self.selector.clone()),
index: -1,
},
options: self.options.clone(),
}
}
#[must_use]
pub fn nth(&self, index: i32) -> Locator<'a> {
Locator {
page: self.page,
selector: Selector::Nth {
base: Box::new(self.selector.clone()),
index,
},
options: self.options.clone(),
}
}
pub(crate) fn to_js_selector(&self) -> String {
self.selector.to_js_expression()
}
#[must_use]
pub fn and(&self, other: Locator<'a>) -> Locator<'a> {
Locator {
page: self.page,
selector: Selector::And(Box::new(self.selector.clone()), Box::new(other.selector)),
options: self.options.clone(),
}
}
#[must_use]
pub fn or(&self, other: Locator<'a>) -> Locator<'a> {
Locator {
page: self.page,
selector: Selector::Or(Box::new(self.selector.clone()), Box::new(other.selector)),
options: self.options.clone(),
}
}
pub fn filter(&self) -> FilterBuilder<'a> {
FilterBuilder::new(self.page, self.selector.clone(), self.options.clone())
}
pub async fn aria_snapshot(&self) -> Result<AriaSnapshot, crate::error::LocatorError> {
use crate::error::LocatorError;
use viewpoint_js::js;
if self.page.is_closed() {
return Err(LocatorError::PageClosed);
}
let js_selector = self.selector.to_js_expression();
let snapshot_fn = aria::aria_snapshot_js();
let js_code = js! {
(function() {
const elements = @{js_selector};
const element = elements && elements[0] ? elements[0] : elements;
if (!element) {
return { error: "Element not found" };
}
const getSnapshot = @{snapshot_fn};
return getSnapshot(element);
})()
};
let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
.page
.connection()
.send_command(
"Runtime.evaluate",
Some(viewpoint_cdp::protocol::runtime::EvaluateParams {
expression: js_code,
object_group: None,
include_command_line_api: None,
silent: Some(true),
context_id: None,
return_by_value: Some(true),
await_promise: Some(false),
}),
Some(self.page.session_id()),
)
.await?;
if let Some(exception) = result.exception_details {
return Err(LocatorError::EvaluationError(exception.text));
}
let value = result.result.value.ok_or_else(|| {
LocatorError::EvaluationError("No result from aria snapshot".to_string())
})?;
if let Some(error) = value.get("error").and_then(|e| e.as_str()) {
return Err(LocatorError::NotFound(error.to_string()));
}
let snapshot: AriaSnapshot = serde_json::from_value(value).map_err(|e| {
LocatorError::EvaluationError(format!("Failed to parse aria snapshot: {e}"))
})?;
Ok(snapshot)
}
}