use std::time::Duration;
use crate::cdp_page::CdpPage;
#[derive(Debug)]
pub struct Locator {
pub selector: String,
pub page: CdpPage,
}
impl Locator {
#[must_use]
pub fn new(selector: impl Into<String>, page: CdpPage) -> Self {
Self {
selector: selector.into(),
page,
}
}
pub async fn click(&self) -> crate::cdp_client::error::Result<()> {
let escaped = escape_selector(&self.selector);
let js = format!("document.querySelector('{escaped}').click()");
self.page.evaluate::<()>(&js).await
}
pub async fn type_text(&self, text: &str) -> crate::cdp_client::error::Result<()> {
let escaped = escape_selector(&self.selector);
let focus_js = format!("document.querySelector('{escaped}').focus()");
self.page.evaluate::<()>(&focus_js).await?;
for ch in text.chars() {
let char_str = ch.escape_default().to_string();
self.page
.cdp
.send_command_with_session(
"Input.dispatchKeyEvent",
Some(serde_json::json!({
"type": "keyDown",
"text": char_str,
"key": char_str,
})),
&self.page.session_id,
)
.await?;
self.page
.cdp
.send_command_with_session(
"Input.dispatchKeyEvent",
Some(serde_json::json!({
"type": "keyUp",
"key": char_str,
})),
&self.page.session_id,
)
.await?;
}
Ok(())
}
pub async fn wait_for(&self, timeout: Duration) -> crate::cdp_client::error::Result<()> {
let escaped = escape_selector(&self.selector);
let js = format!("document.querySelector('{escaped}') !== null");
let deadline = tokio::time::Instant::now() + timeout;
loop {
let found: bool = self.page.evaluate(&js).await?;
if found {
return Ok(());
}
if tokio::time::Instant::now() >= deadline {
return Err(crate::cdp_client::error::CdpClientError::command_failed(
"wait_for",
&format!("element '{}' not found within timeout", self.selector),
));
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
pub async fn inner_text(&self) -> crate::cdp_client::error::Result<String> {
let escaped = escape_selector(&self.selector);
let js = format!("document.querySelector('{escaped}').innerText");
self.page.evaluate(&js).await
}
pub async fn get_attribute(
&self,
name: &str,
) -> crate::cdp_client::error::Result<Option<String>> {
let escaped = escape_selector(&self.selector);
let escaped_name = escape_selector(name);
let js = format!("document.querySelector('{escaped}').getAttribute('{escaped_name}')");
self.page.evaluate(&js).await
}
}
#[must_use]
pub fn escape_selector(s: &str) -> String {
crate::utils::escape_js_string(s)
}
impl CdpPage {
#[must_use]
pub fn locator(&self, selector: impl Into<String>) -> Locator {
Locator::new(selector, self.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escape_selector_replaces_single_quotes() {
assert_eq!(escape_selector("button#'"), "button#\\'");
assert_eq!(escape_selector("it's"), "it\\'s");
assert_eq!(escape_selector("no-quotes"), "no-quotes");
}
#[test]
fn escape_selector_empty_string() {
assert_eq!(escape_selector(""), "");
}
#[test]
fn locator_new_sets_fields() {
let client = std::sync::Arc::new(crate::cdp_client::cdp::CdpClient::new());
let page = CdpPage::new("t".into(), "s".into(), client);
let loc = Locator::new("#btn", page);
assert_eq!(loc.selector, "#btn");
assert_eq!(loc.page.target_id, "t");
}
#[test]
fn click_generates_correct_js() {
let escaped = escape_selector(".my-class");
let js = format!("document.querySelector('{escaped}').click()");
assert_eq!(js, "document.querySelector('.my-class').click()");
}
#[test]
fn inner_text_generates_correct_js() {
let escaped = escape_selector("#text");
let js = format!("document.querySelector('{escaped}').innerText");
assert_eq!(js, "document.querySelector('#text').innerText");
}
#[test]
fn get_attribute_generates_correct_js() {
let escaped = escape_selector("#el");
let name = escape_selector("href");
let js = format!("document.querySelector('{escaped}').getAttribute('{name}')");
assert_eq!(js, "document.querySelector('#el').getAttribute('href')");
}
#[test]
fn wait_for_generates_correct_js() {
let escaped = escape_selector("[data-testid='form']");
let js = format!("document.querySelector('{escaped}') !== null");
assert_eq!(
js,
"document.querySelector('[data-testid=\\'form\\']') !== null"
);
}
#[test]
fn focus_generates_correct_js() {
let escaped = escape_selector("#input");
let js = format!("document.querySelector('{escaped}').focus()");
assert_eq!(js, "document.querySelector('#input').focus()");
}
#[test]
fn page_locator_factory() {
let client = std::sync::Arc::new(crate::cdp_client::cdp::CdpClient::new());
let page = CdpPage::new("t".into(), "s".into(), client);
let loc = page.locator("#btn");
assert_eq!(loc.selector, "#btn");
assert_eq!(loc.page.session_id, "s");
}
}