use crate::error::Result;
use crate::protocol::Frame;
use serde::Deserialize;
pub(crate) trait HasTimeout {
fn timeout_ref(&self) -> &Option<f64>;
fn timeout_ref_mut(&mut self) -> &mut Option<f64>;
}
macro_rules! impl_has_timeout {
($($ty:ty),+ $(,)?) => {
$(impl HasTimeout for $ty {
fn timeout_ref(&self) -> &Option<f64> { &self.timeout }
fn timeout_ref_mut(&mut self) -> &mut Option<f64> { &mut self.timeout }
})+
};
}
impl_has_timeout!(
crate::protocol::ClickOptions,
crate::protocol::FillOptions,
crate::protocol::PressOptions,
crate::protocol::CheckOptions,
crate::protocol::HoverOptions,
crate::protocol::SelectOptions,
crate::protocol::ScreenshotOptions,
crate::protocol::TapOptions,
crate::protocol::DragToOptions,
crate::protocol::WaitForOptions,
);
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct BoundingBox {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
fn escape_for_selector(text: &str, exact: bool) -> String {
let suffix = if exact { "s" } else { "i" };
let escaped = serde_json::to_string(text).unwrap_or_else(|_| format!("\"{}\"", text));
format!("{}{}", escaped, suffix)
}
pub(crate) fn get_by_text_selector(text: &str, exact: bool) -> String {
format!("internal:text={}", escape_for_selector(text, exact))
}
pub(crate) fn get_by_label_selector(text: &str, exact: bool) -> String {
format!("internal:label={}", escape_for_selector(text, exact))
}
pub(crate) fn get_by_placeholder_selector(text: &str, exact: bool) -> String {
format!(
"internal:attr=[placeholder={}]",
escape_for_selector(text, exact)
)
}
pub(crate) fn get_by_alt_text_selector(text: &str, exact: bool) -> String {
format!("internal:attr=[alt={}]", escape_for_selector(text, exact))
}
pub(crate) fn get_by_title_selector(text: &str, exact: bool) -> String {
format!("internal:attr=[title={}]", escape_for_selector(text, exact))
}
pub(crate) fn get_by_test_id_selector(test_id: &str) -> String {
get_by_test_id_selector_with_attr(test_id, "data-testid")
}
pub(crate) fn get_by_test_id_selector_with_attr(test_id: &str, attribute: &str) -> String {
format!(
"internal:testid=[{}={}]",
attribute,
escape_for_selector(test_id, true)
)
}
fn escape_for_attribute_selector(text: &str, exact: bool) -> String {
let suffix = if exact { "s" } else { "i" };
let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{}\"{}", escaped, suffix)
}
pub(crate) fn get_by_role_selector(role: AriaRole, options: Option<GetByRoleOptions>) -> String {
let mut selector = format!("internal:role={}", role.as_str());
if let Some(opts) = options {
if let Some(checked) = opts.checked {
selector.push_str(&format!("[checked={}]", checked));
}
if let Some(disabled) = opts.disabled {
selector.push_str(&format!("[disabled={}]", disabled));
}
if let Some(selected) = opts.selected {
selector.push_str(&format!("[selected={}]", selected));
}
if let Some(expanded) = opts.expanded {
selector.push_str(&format!("[expanded={}]", expanded));
}
if let Some(include_hidden) = opts.include_hidden {
selector.push_str(&format!("[include-hidden={}]", include_hidden));
}
if let Some(level) = opts.level {
selector.push_str(&format!("[level={}]", level));
}
if let Some(name) = &opts.name {
let exact = opts.exact.unwrap_or(false);
selector.push_str(&format!(
"[name={}]",
escape_for_attribute_selector(name, exact)
));
}
if let Some(pressed) = opts.pressed {
selector.push_str(&format!("[pressed={}]", pressed));
}
}
selector
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AriaRole {
Alert,
Alertdialog,
Application,
Article,
Banner,
Blockquote,
Button,
Caption,
Cell,
Checkbox,
Code,
Columnheader,
Combobox,
Complementary,
Contentinfo,
Definition,
Deletion,
Dialog,
Directory,
Document,
Emphasis,
Feed,
Figure,
Form,
Generic,
Grid,
Gridcell,
Group,
Heading,
Img,
Insertion,
Link,
List,
Listbox,
Listitem,
Log,
Main,
Marquee,
Math,
Meter,
Menu,
Menubar,
Menuitem,
Menuitemcheckbox,
Menuitemradio,
Navigation,
None,
Note,
Option,
Paragraph,
Presentation,
Progressbar,
Radio,
Radiogroup,
Region,
Row,
Rowgroup,
Rowheader,
Scrollbar,
Search,
Searchbox,
Separator,
Slider,
Spinbutton,
Status,
Strong,
Subscript,
Superscript,
Switch,
Tab,
Table,
Tablist,
Tabpanel,
Term,
Textbox,
Time,
Timer,
Toolbar,
Tooltip,
Tree,
Treegrid,
Treeitem,
}
impl AriaRole {
pub fn as_str(&self) -> &'static str {
match self {
Self::Alert => "alert",
Self::Alertdialog => "alertdialog",
Self::Application => "application",
Self::Article => "article",
Self::Banner => "banner",
Self::Blockquote => "blockquote",
Self::Button => "button",
Self::Caption => "caption",
Self::Cell => "cell",
Self::Checkbox => "checkbox",
Self::Code => "code",
Self::Columnheader => "columnheader",
Self::Combobox => "combobox",
Self::Complementary => "complementary",
Self::Contentinfo => "contentinfo",
Self::Definition => "definition",
Self::Deletion => "deletion",
Self::Dialog => "dialog",
Self::Directory => "directory",
Self::Document => "document",
Self::Emphasis => "emphasis",
Self::Feed => "feed",
Self::Figure => "figure",
Self::Form => "form",
Self::Generic => "generic",
Self::Grid => "grid",
Self::Gridcell => "gridcell",
Self::Group => "group",
Self::Heading => "heading",
Self::Img => "img",
Self::Insertion => "insertion",
Self::Link => "link",
Self::List => "list",
Self::Listbox => "listbox",
Self::Listitem => "listitem",
Self::Log => "log",
Self::Main => "main",
Self::Marquee => "marquee",
Self::Math => "math",
Self::Meter => "meter",
Self::Menu => "menu",
Self::Menubar => "menubar",
Self::Menuitem => "menuitem",
Self::Menuitemcheckbox => "menuitemcheckbox",
Self::Menuitemradio => "menuitemradio",
Self::Navigation => "navigation",
Self::None => "none",
Self::Note => "note",
Self::Option => "option",
Self::Paragraph => "paragraph",
Self::Presentation => "presentation",
Self::Progressbar => "progressbar",
Self::Radio => "radio",
Self::Radiogroup => "radiogroup",
Self::Region => "region",
Self::Row => "row",
Self::Rowgroup => "rowgroup",
Self::Rowheader => "rowheader",
Self::Scrollbar => "scrollbar",
Self::Search => "search",
Self::Searchbox => "searchbox",
Self::Separator => "separator",
Self::Slider => "slider",
Self::Spinbutton => "spinbutton",
Self::Status => "status",
Self::Strong => "strong",
Self::Subscript => "subscript",
Self::Superscript => "superscript",
Self::Switch => "switch",
Self::Tab => "tab",
Self::Table => "table",
Self::Tablist => "tablist",
Self::Tabpanel => "tabpanel",
Self::Term => "term",
Self::Textbox => "textbox",
Self::Time => "time",
Self::Timer => "timer",
Self::Toolbar => "toolbar",
Self::Tooltip => "tooltip",
Self::Tree => "tree",
Self::Treegrid => "treegrid",
Self::Treeitem => "treeitem",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct GetByRoleOptions {
pub checked: Option<bool>,
pub disabled: Option<bool>,
pub selected: Option<bool>,
pub expanded: Option<bool>,
pub include_hidden: Option<bool>,
pub level: Option<u32>,
pub name: Option<String>,
pub exact: Option<bool>,
pub pressed: Option<bool>,
}
#[derive(Debug, Clone, Default)]
pub struct FilterOptions {
pub has_text: Option<String>,
pub has_not_text: Option<String>,
pub has: Option<Locator>,
pub has_not: Option<Locator>,
}
#[derive(Clone)]
pub struct Locator {
frame: Arc<Frame>,
selector: String,
page: crate::protocol::Page,
}
impl Locator {
pub(crate) fn new(frame: Arc<Frame>, selector: String, page: crate::protocol::Page) -> Self {
Self {
frame,
selector,
page,
}
}
pub fn selector(&self) -> &str {
&self.selector
}
pub(crate) fn frame(&self) -> &Arc<Frame> {
&self.frame
}
pub fn frame_locator(&self, selector: &str) -> crate::protocol::FrameLocator {
crate::protocol::FrameLocator::new(
Arc::clone(&self.frame),
format!("{} >> {}", self.selector, selector),
self.page.clone(),
)
}
pub fn page(&self) -> Result<crate::protocol::Page> {
Ok(self.page.clone())
}
pub(crate) async fn evaluate_js<T: serde::Serialize>(
&self,
expression: &str,
_arg: Option<T>,
) -> Result<()> {
self.frame
.frame_evaluate_expression(expression)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub fn first(&self) -> Locator {
Locator::new(
Arc::clone(&self.frame),
format!("{} >> nth=0", self.selector),
self.page.clone(),
)
}
pub fn last(&self) -> Locator {
Locator::new(
Arc::clone(&self.frame),
format!("{} >> nth=-1", self.selector),
self.page.clone(),
)
}
pub fn nth(&self, index: i32) -> Locator {
Locator::new(
Arc::clone(&self.frame),
format!("{} >> nth={}", self.selector, index),
self.page.clone(),
)
}
pub fn get_by_text(&self, text: &str, exact: bool) -> Locator {
self.locator(&get_by_text_selector(text, exact))
}
pub fn get_by_label(&self, text: &str, exact: bool) -> Locator {
self.locator(&get_by_label_selector(text, exact))
}
pub fn get_by_placeholder(&self, text: &str, exact: bool) -> Locator {
self.locator(&get_by_placeholder_selector(text, exact))
}
pub fn get_by_alt_text(&self, text: &str, exact: bool) -> Locator {
self.locator(&get_by_alt_text_selector(text, exact))
}
pub fn get_by_title(&self, text: &str, exact: bool) -> Locator {
self.locator(&get_by_title_selector(text, exact))
}
pub fn get_by_test_id(&self, test_id: &str) -> Locator {
use crate::server::channel_owner::ChannelOwner as _;
let attr = self.frame.connection().selectors().test_id_attribute();
self.locator(&get_by_test_id_selector_with_attr(test_id, &attr))
}
pub fn get_by_role(&self, role: AriaRole, options: Option<GetByRoleOptions>) -> Locator {
self.locator(&get_by_role_selector(role, options))
}
pub fn locator(&self, selector: &str) -> Locator {
Locator::new(
Arc::clone(&self.frame),
format!("{} >> {}", self.selector, selector),
self.page.clone(),
)
}
pub fn filter(&self, options: FilterOptions) -> Locator {
let mut selector = self.selector.clone();
if let Some(text) = &options.has_text {
let escaped = escape_for_selector(text, false);
selector = format!("{} >> internal:has-text={}", selector, escaped);
}
if let Some(text) = &options.has_not_text {
let escaped = escape_for_selector(text, false);
selector = format!("{} >> internal:has-not-text={}", selector, escaped);
}
if let Some(locator) = &options.has {
let inner = serde_json::to_string(&locator.selector)
.unwrap_or_else(|_| format!("\"{}\"", locator.selector));
selector = format!("{} >> internal:has={}", selector, inner);
}
if let Some(locator) = &options.has_not {
let inner = serde_json::to_string(&locator.selector)
.unwrap_or_else(|_| format!("\"{}\"", locator.selector));
selector = format!("{} >> internal:has-not={}", selector, inner);
}
Locator::new(Arc::clone(&self.frame), selector, self.page.clone())
}
pub fn and_(&self, locator: &Locator) -> Locator {
let inner = serde_json::to_string(&locator.selector)
.unwrap_or_else(|_| format!("\"{}\"", locator.selector));
Locator::new(
Arc::clone(&self.frame),
format!("{} >> internal:and={}", self.selector, inner),
self.page.clone(),
)
}
pub fn or_(&self, locator: &Locator) -> Locator {
let inner = serde_json::to_string(&locator.selector)
.unwrap_or_else(|_| format!("\"{}\"", locator.selector));
Locator::new(
Arc::clone(&self.frame),
format!("{} >> internal:or={}", self.selector, inner),
self.page.clone(),
)
}
pub async fn count(&self) -> Result<usize> {
self.frame
.locator_count(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn all(&self) -> Result<Vec<Locator>> {
let count = self.count().await?;
Ok((0..count).map(|i| self.nth(i as i32)).collect())
}
pub async fn text_content(&self) -> Result<Option<String>> {
self.frame
.locator_text_content(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn inner_text(&self) -> Result<String> {
self.frame
.locator_inner_text(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn inner_html(&self) -> Result<String> {
self.frame
.locator_inner_html(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
self.frame
.locator_get_attribute(&self.selector, name)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn is_visible(&self) -> Result<bool> {
self.frame
.locator_is_visible(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn is_enabled(&self) -> Result<bool> {
self.frame
.locator_is_enabled(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn is_checked(&self) -> Result<bool> {
self.frame
.locator_is_checked(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn is_editable(&self) -> Result<bool> {
self.frame
.locator_is_editable(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn is_hidden(&self) -> Result<bool> {
self.frame
.locator_is_hidden(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn is_disabled(&self) -> Result<bool> {
self.frame
.locator_is_disabled(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn is_focused(&self) -> Result<bool> {
self.frame
.locator_is_focused(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn click(&self, options: Option<crate::protocol::ClickOptions>) -> Result<()> {
self.frame
.locator_click(&self.selector, Some(self.with_timeout(options)))
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
fn with_timeout<T: HasTimeout + Default>(&self, options: Option<T>) -> T {
let mut opts = options.unwrap_or_default();
if opts.timeout_ref().is_none() {
*opts.timeout_ref_mut() = Some(self.page.default_timeout_ms());
}
opts
}
fn wrap_error_with_selector(&self, error: crate::error::Error) -> crate::error::Error {
match &error {
crate::error::Error::ProtocolError(msg) => {
crate::error::Error::ProtocolError(format!("{} [selector: {}]", msg, self.selector))
}
crate::error::Error::Timeout(msg) => {
crate::error::Error::Timeout(format!("{} [selector: {}]", msg, self.selector))
}
_ => error, }
}
pub async fn dblclick(&self, options: Option<crate::protocol::ClickOptions>) -> Result<()> {
self.frame
.locator_dblclick(&self.selector, Some(self.with_timeout(options)))
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn fill(
&self,
text: &str,
options: Option<crate::protocol::FillOptions>,
) -> Result<()> {
self.frame
.locator_fill(&self.selector, text, Some(self.with_timeout(options)))
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn clear(&self, options: Option<crate::protocol::FillOptions>) -> Result<()> {
self.frame
.locator_clear(&self.selector, Some(self.with_timeout(options)))
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn press(
&self,
key: &str,
options: Option<crate::protocol::PressOptions>,
) -> Result<()> {
self.frame
.locator_press(&self.selector, key, Some(self.with_timeout(options)))
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn focus(&self) -> Result<()> {
self.frame
.locator_focus(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn blur(&self) -> Result<()> {
self.frame
.locator_blur(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn press_sequentially(
&self,
text: &str,
options: Option<crate::protocol::PressSequentiallyOptions>,
) -> Result<()> {
self.frame
.locator_press_sequentially(&self.selector, text, options)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn all_inner_texts(&self) -> Result<Vec<String>> {
self.frame
.locator_all_inner_texts(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn all_text_contents(&self) -> Result<Vec<String>> {
self.frame
.locator_all_text_contents(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn check(&self, options: Option<crate::protocol::CheckOptions>) -> Result<()> {
self.frame
.locator_check(&self.selector, Some(self.with_timeout(options)))
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn uncheck(&self, options: Option<crate::protocol::CheckOptions>) -> Result<()> {
self.frame
.locator_uncheck(&self.selector, Some(self.with_timeout(options)))
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn set_checked(
&self,
checked: bool,
options: Option<crate::protocol::CheckOptions>,
) -> Result<()> {
if checked {
self.check(options).await
} else {
self.uncheck(options).await
}
}
pub async fn hover(&self, options: Option<crate::protocol::HoverOptions>) -> Result<()> {
self.frame
.locator_hover(&self.selector, Some(self.with_timeout(options)))
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn input_value(&self, _options: Option<()>) -> Result<String> {
self.frame
.locator_input_value(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn select_option(
&self,
value: impl Into<crate::protocol::SelectOption>,
options: Option<crate::protocol::SelectOptions>,
) -> Result<Vec<String>> {
self.frame
.locator_select_option(
&self.selector,
value.into(),
Some(self.with_timeout(options)),
)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn select_option_multiple(
&self,
values: &[impl Into<crate::protocol::SelectOption> + Clone],
options: Option<crate::protocol::SelectOptions>,
) -> Result<Vec<String>> {
let select_options: Vec<crate::protocol::SelectOption> =
values.iter().map(|v| v.clone().into()).collect();
self.frame
.locator_select_option_multiple(
&self.selector,
select_options,
Some(self.with_timeout(options)),
)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn set_input_files(
&self,
file: &std::path::PathBuf,
_options: Option<()>,
) -> Result<()> {
self.frame
.locator_set_input_files(&self.selector, file)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn set_input_files_multiple(
&self,
files: &[&std::path::PathBuf],
_options: Option<()>,
) -> Result<()> {
self.frame
.locator_set_input_files_multiple(&self.selector, files)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn set_input_files_payload(
&self,
file: crate::protocol::FilePayload,
_options: Option<()>,
) -> Result<()> {
self.frame
.locator_set_input_files_payload(&self.selector, file)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn set_input_files_payload_multiple(
&self,
files: &[crate::protocol::FilePayload],
_options: Option<()>,
) -> Result<()> {
self.frame
.locator_set_input_files_payload_multiple(&self.selector, files)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn dispatch_event(
&self,
type_: &str,
event_init: Option<serde_json::Value>,
) -> Result<()> {
self.frame
.locator_dispatch_event(&self.selector, type_, event_init)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn bounding_box(&self) -> Result<Option<BoundingBox>> {
self.frame
.locator_bounding_box(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn scroll_into_view_if_needed(&self) -> Result<()> {
self.frame
.locator_scroll_into_view_if_needed(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn screenshot(
&self,
options: Option<crate::protocol::ScreenshotOptions>,
) -> Result<Vec<u8>> {
let element = self
.frame
.query_selector(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))?
.ok_or_else(|| {
crate::error::Error::ElementNotFound(format!(
"Element not found: {}",
self.selector
))
})?;
element
.screenshot(Some(self.with_timeout(options)))
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn tap(&self, options: Option<crate::protocol::TapOptions>) -> Result<()> {
self.frame
.locator_tap(&self.selector, Some(self.with_timeout(options)))
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn drag_to(
&self,
target: &Locator,
options: Option<crate::protocol::DragToOptions>,
) -> Result<()> {
self.frame
.locator_drag_to(
&self.selector,
&target.selector,
Some(self.with_timeout(options)),
)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn wait_for(&self, options: Option<crate::protocol::WaitForOptions>) -> Result<()> {
self.frame
.locator_wait_for(&self.selector, Some(self.with_timeout(options)))
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub async fn evaluate<R, T>(&self, expression: &str, arg: Option<T>) -> Result<R>
where
R: serde::de::DeserializeOwned,
T: serde::Serialize,
{
let raw = self
.frame
.locator_evaluate(&self.selector, expression, arg)
.await
.map_err(|e| self.wrap_error_with_selector(e))?;
serde_json::from_value(raw).map_err(|e| {
crate::error::Error::ProtocolError(format!(
"evaluate result deserialization failed: {}",
e
))
})
}
pub async fn evaluate_all<R, T>(&self, expression: &str, arg: Option<T>) -> Result<R>
where
R: serde::de::DeserializeOwned,
T: serde::Serialize,
{
let raw = self
.frame
.locator_evaluate_all(&self.selector, expression, arg)
.await
.map_err(|e| self.wrap_error_with_selector(e))?;
serde_json::from_value(raw).map_err(|e| {
crate::error::Error::ProtocolError(format!(
"evaluate_all result deserialization failed: {}",
e
))
})
}
pub async fn aria_snapshot(&self) -> Result<String> {
self.frame
.locator_aria_snapshot(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub fn describe(&self, description: &str) -> Locator {
let escaped =
serde_json::to_string(description).unwrap_or_else(|_| format!("\"{}\"", description));
Locator::new(
Arc::clone(&self.frame),
format!("{} >> internal:describe={}", self.selector, escaped),
self.page.clone(),
)
}
pub async fn highlight(&self) -> Result<()> {
self.frame
.locator_highlight(&self.selector)
.await
.map_err(|e| self.wrap_error_with_selector(e))
}
pub fn content_frame(&self) -> crate::protocol::FrameLocator {
crate::protocol::FrameLocator::new(
Arc::clone(&self.frame),
self.selector.clone(),
self.page.clone(),
)
}
}
impl std::fmt::Debug for Locator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Locator")
.field("selector", &self.selector)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_escape_for_selector_case_insensitive() {
assert_eq!(escape_for_selector("hello", false), "\"hello\"i");
}
#[test]
fn test_escape_for_selector_exact() {
assert_eq!(escape_for_selector("hello", true), "\"hello\"s");
}
#[test]
fn test_escape_for_selector_with_quotes() {
assert_eq!(
escape_for_selector("say \"hi\"", false),
"\"say \\\"hi\\\"\"i"
);
}
#[test]
fn test_get_by_text_selector_case_insensitive() {
assert_eq!(
get_by_text_selector("Click me", false),
"internal:text=\"Click me\"i"
);
}
#[test]
fn test_get_by_text_selector_exact() {
assert_eq!(
get_by_text_selector("Click me", true),
"internal:text=\"Click me\"s"
);
}
#[test]
fn test_get_by_label_selector() {
assert_eq!(
get_by_label_selector("Email", false),
"internal:label=\"Email\"i"
);
}
#[test]
fn test_get_by_placeholder_selector() {
assert_eq!(
get_by_placeholder_selector("Enter name", false),
"internal:attr=[placeholder=\"Enter name\"i]"
);
}
#[test]
fn test_get_by_alt_text_selector() {
assert_eq!(
get_by_alt_text_selector("Logo", true),
"internal:attr=[alt=\"Logo\"s]"
);
}
#[test]
fn test_get_by_title_selector() {
assert_eq!(
get_by_title_selector("Help", false),
"internal:attr=[title=\"Help\"i]"
);
}
#[test]
fn test_get_by_test_id_selector() {
assert_eq!(
get_by_test_id_selector("submit-btn"),
"internal:testid=[data-testid=\"submit-btn\"s]"
);
}
#[test]
fn test_escape_for_attribute_selector_case_insensitive() {
assert_eq!(
escape_for_attribute_selector("Submit", false),
"\"Submit\"i"
);
}
#[test]
fn test_escape_for_attribute_selector_exact() {
assert_eq!(escape_for_attribute_selector("Submit", true), "\"Submit\"s");
}
#[test]
fn test_escape_for_attribute_selector_escapes_quotes() {
assert_eq!(
escape_for_attribute_selector("Say \"hello\"", false),
"\"Say \\\"hello\\\"\"i"
);
}
#[test]
fn test_escape_for_attribute_selector_escapes_backslashes() {
assert_eq!(
escape_for_attribute_selector("path\\to", true),
"\"path\\\\to\"s"
);
}
#[test]
fn test_get_by_role_selector_role_only() {
assert_eq!(
get_by_role_selector(AriaRole::Button, None),
"internal:role=button"
);
}
#[test]
fn test_get_by_role_selector_with_name() {
let opts = GetByRoleOptions {
name: Some("Submit".to_string()),
..Default::default()
};
assert_eq!(
get_by_role_selector(AriaRole::Button, Some(opts)),
"internal:role=button[name=\"Submit\"i]"
);
}
#[test]
fn test_get_by_role_selector_with_name_exact() {
let opts = GetByRoleOptions {
name: Some("Submit".to_string()),
exact: Some(true),
..Default::default()
};
assert_eq!(
get_by_role_selector(AriaRole::Button, Some(opts)),
"internal:role=button[name=\"Submit\"s]"
);
}
#[test]
fn test_get_by_role_selector_with_checked() {
let opts = GetByRoleOptions {
checked: Some(true),
..Default::default()
};
assert_eq!(
get_by_role_selector(AriaRole::Checkbox, Some(opts)),
"internal:role=checkbox[checked=true]"
);
}
#[test]
fn test_get_by_role_selector_with_level() {
let opts = GetByRoleOptions {
level: Some(2),
..Default::default()
};
assert_eq!(
get_by_role_selector(AriaRole::Heading, Some(opts)),
"internal:role=heading[level=2]"
);
}
#[test]
fn test_get_by_role_selector_with_disabled() {
let opts = GetByRoleOptions {
disabled: Some(true),
..Default::default()
};
assert_eq!(
get_by_role_selector(AriaRole::Button, Some(opts)),
"internal:role=button[disabled=true]"
);
}
#[test]
fn test_get_by_role_selector_include_hidden() {
let opts = GetByRoleOptions {
include_hidden: Some(true),
..Default::default()
};
assert_eq!(
get_by_role_selector(AriaRole::Button, Some(opts)),
"internal:role=button[include-hidden=true]"
);
}
#[test]
fn test_get_by_role_selector_property_order() {
let opts = GetByRoleOptions {
pressed: Some(true),
name: Some("OK".to_string()),
checked: Some(false),
disabled: Some(true),
..Default::default()
};
assert_eq!(
get_by_role_selector(AriaRole::Button, Some(opts)),
"internal:role=button[checked=false][disabled=true][name=\"OK\"i][pressed=true]"
);
}
#[test]
fn test_get_by_role_selector_name_with_special_chars() {
let opts = GetByRoleOptions {
name: Some("Click \"here\" now".to_string()),
exact: Some(true),
..Default::default()
};
assert_eq!(
get_by_role_selector(AriaRole::Link, Some(opts)),
"internal:role=link[name=\"Click \\\"here\\\" now\"s]"
);
}
#[test]
fn test_aria_role_as_str() {
assert_eq!(AriaRole::Button.as_str(), "button");
assert_eq!(AriaRole::Heading.as_str(), "heading");
assert_eq!(AriaRole::Link.as_str(), "link");
assert_eq!(AriaRole::Checkbox.as_str(), "checkbox");
assert_eq!(AriaRole::Alert.as_str(), "alert");
assert_eq!(AriaRole::Navigation.as_str(), "navigation");
assert_eq!(AriaRole::Progressbar.as_str(), "progressbar");
assert_eq!(AriaRole::Treeitem.as_str(), "treeitem");
}
}