use std::future::Future;
use std::time::Duration;
use crate::error::{Error, Result};
use crate::locator::Locator;
use crate::page::Page;
const DEFAULT_TIMEOUT: Duration = Duration::from_millis(5000);
const POLL_INTERVAL: Duration = Duration::from_millis(100);
pub fn expect(locator: Locator) -> LocatorAssertions {
LocatorAssertions::new(locator)
}
pub fn expect_page(page: Page) -> PageAssertions {
PageAssertions::new(page)
}
async fn wait_for<F, Fut, Msg>(cond: F, timeout: Duration, negated: bool, msg: Msg) -> Result<()>
where
F: Fn() -> Fut,
Fut: Future<Output = Result<bool>>,
Msg: FnOnce() -> String,
{
let want = !negated;
let deadline = tokio::time::Instant::now() + timeout;
loop {
let got = cond().await?;
if got == want {
return Ok(());
}
if tokio::time::Instant::now() >= deadline {
return Err(Error::Timeout(msg()));
}
tokio::time::sleep(POLL_INTERVAL).await;
}
}
#[derive(Clone)]
pub struct LocatorAssertions {
locator: Locator,
timeout: Duration,
negated: bool,
}
impl LocatorAssertions {
pub fn new(locator: Locator) -> Self {
Self {
locator,
timeout: DEFAULT_TIMEOUT,
negated: false,
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn set_timeout(&mut self, timeout: Duration) -> &mut Self {
self.timeout = timeout;
self
}
pub fn not(&self) -> Self {
let mut c = self.clone();
c.negated = true;
c
}
fn sel(&self) -> &str {
self.locator.selector()
}
pub async fn to_be_visible(&self) -> Result<()> {
let sel = self.sel().to_string();
let l = self.locator.clone();
let negated = self.negated;
let timeout = self.timeout;
wait_for(
move || {
let l = l.clone();
async move { l.is_visible().await }
},
timeout,
negated,
|| format!("to_be_visible('{sel}') timed out after {}ms", timeout.as_millis()),
)
.await
}
pub async fn to_be_hidden(&self) -> Result<()> {
let sel = self.sel().to_string();
let l = self.locator.clone();
let negated = self.negated;
let timeout = self.timeout;
wait_for(
move || {
let l = l.clone();
async move { l.is_hidden().await }
},
timeout,
negated,
|| format!("to_be_hidden('{sel}') timed out after {}ms", timeout.as_millis()),
)
.await
}
pub async fn to_be_enabled(&self) -> Result<()> {
let sel = self.sel().to_string();
let l = self.locator.clone();
let negated = self.negated;
let timeout = self.timeout;
wait_for(
move || {
let l = l.clone();
async move { l.is_enabled().await }
},
timeout,
negated,
|| format!("to_be_enabled('{sel}') timed out after {}ms", timeout.as_millis()),
)
.await
}
pub async fn to_be_disabled(&self) -> Result<()> {
let sel = self.sel().to_string();
let l = self.locator.clone();
let negated = self.negated;
let timeout = self.timeout;
wait_for(
move || {
let l = l.clone();
async move { l.is_disabled().await }
},
timeout,
negated,
|| format!("to_be_disabled('{sel}') timed out after {}ms", timeout.as_millis()),
)
.await
}
pub async fn to_be_editable(&self) -> Result<()> {
let sel = self.sel().to_string();
let l = self.locator.clone();
let negated = self.negated;
let timeout = self.timeout;
wait_for(
move || {
let l = l.clone();
async move { l.is_editable().await }
},
timeout,
negated,
|| format!("to_be_editable('{sel}') timed out after {}ms", timeout.as_millis()),
)
.await
}
pub async fn to_be_checked(&self) -> Result<()> {
let sel = self.sel().to_string();
let l = self.locator.clone();
let negated = self.negated;
let timeout = self.timeout;
wait_for(
move || {
let l = l.clone();
async move { l.is_checked().await }
},
timeout,
negated,
|| format!("to_be_checked('{sel}') timed out after {}ms", timeout.as_millis()),
)
.await
}
pub async fn to_be_unchecked(&self) -> Result<()> {
let sel = self.sel().to_string();
let l = self.locator.clone();
let negated = self.negated;
let timeout = self.timeout;
wait_for(
move || {
let l = l.clone();
async move {
Ok(!l.is_checked().await?)
}
},
timeout,
negated,
|| format!("to_be_unchecked('{sel}') timed out after {}ms", timeout.as_millis()),
)
.await
}
pub async fn to_have_text(&self, expected: &str) -> Result<()> {
let sel = self.sel().to_string();
let l = self.locator.clone();
let negated = self.negated;
let timeout = self.timeout;
let expected_owned = expected.to_string();
let expected_for_msg = expected_owned.clone();
wait_for(
move || {
let l = l.clone();
let expected_owned = expected_owned.clone();
async move {
let actual = l.text_content().await?;
let actual = actual.unwrap_or_default();
Ok(actual.trim() == expected_owned.trim())
}
},
timeout,
negated,
move || {
format!(
"to_have_text('{sel}', '{expected_for_msg}') timed out after {}ms",
timeout.as_millis()
)
},
)
.await
}
pub async fn to_contain_text(&self, expected: &str) -> Result<()> {
let sel = self.sel().to_string();
let l = self.locator.clone();
let negated = self.negated;
let timeout = self.timeout;
let expected_owned = expected.to_string();
let expected_for_msg = expected_owned.clone();
wait_for(
move || {
let l = l.clone();
let expected_owned = expected_owned.clone();
async move {
let actual = l.text_content().await?;
Ok(actual
.unwrap_or_default()
.trim()
.contains(expected_owned.trim()))
}
},
timeout,
negated,
move || {
format!(
"to_contain_text('{sel}', '{expected_for_msg}') timed out after {}ms",
timeout.as_millis()
)
},
)
.await
}
pub async fn to_have_inner_text(&self, expected: &str) -> Result<()> {
let sel = self.sel().to_string();
let l = self.locator.clone();
let negated = self.negated;
let timeout = self.timeout;
let expected_owned = expected.to_string();
let expected_for_msg = expected_owned.clone();
wait_for(
move || {
let l = l.clone();
let expected_owned = expected_owned.clone();
async move {
let actual = l.inner_text().await?;
Ok(actual == expected_owned)
}
},
timeout,
negated,
move || {
format!(
"to_have_inner_text('{sel}', '{expected_for_msg}') timed out after {}ms",
timeout.as_millis()
)
},
)
.await
}
pub async fn to_have_count(&self, expected: usize) -> Result<()> {
let sel = self.sel().to_string();
let l = self.locator.clone();
let negated = self.negated;
let timeout = self.timeout;
wait_for(
move || {
let l = l.clone();
async move { Ok(l.count().await? == expected) }
},
timeout,
negated,
move || {
format!(
"to_have_count('{sel}', {expected}) timed out after {}ms",
timeout.as_millis()
)
},
)
.await
}
pub async fn to_have_attribute(&self, name: &str, value: &str) -> Result<()> {
let sel = self.sel().to_string();
let l = self.locator.clone();
let negated = self.negated;
let timeout = self.timeout;
let name_owned = name.to_string();
let value_owned = value.to_string();
let name_for_msg = name.to_string();
let value_for_msg = value.to_string();
wait_for(
move || {
let l = l.clone();
let name_owned = name_owned.clone();
let value_owned = value_owned.clone();
async move {
let attr = l.get_attribute(&name_owned).await?;
Ok(attr.as_deref() == Some(value_owned.as_str()))
}
},
timeout,
negated,
move || {
format!(
"to_have_attribute('{sel}', '{name_for_msg}', '{value_for_msg}') timed out after {}ms",
timeout.as_millis()
)
},
)
.await
}
pub async fn to_have_value(&self, expected: &str) -> Result<()> {
let sel = self.sel().to_string();
let l = self.locator.clone();
let negated = self.negated;
let timeout = self.timeout;
let expected_owned = expected.to_string();
let expected_for_msg = expected.to_string();
wait_for(
move || {
let l = l.clone();
let expected_owned = expected_owned.clone();
async move { Ok(l.input_value(None).await? == expected_owned) }
},
timeout,
negated,
move || {
format!(
"to_have_value('{sel}', '{expected_for_msg}') timed out after {}ms",
timeout.as_millis()
)
},
)
.await
}
pub async fn to_be_empty(&self) -> Result<()> {
let sel = self.sel().to_string();
let l = self.locator.clone();
let negated = self.negated;
let timeout = self.timeout;
wait_for(
move || {
let l = l.clone();
async move {
let count = l.count().await?;
if count == 0 {
return Ok(true);
}
let value = l.input_value(None).await.unwrap_or_default();
Ok(value.is_empty())
}
},
timeout,
negated,
move || format!("to_be_empty('{sel}') timed out after {}ms", timeout.as_millis()),
)
.await
}
}
#[derive(Clone)]
pub struct PageAssertions {
page: Page,
timeout: Duration,
negated: bool,
}
impl PageAssertions {
pub fn new(page: Page) -> Self {
Self {
page,
timeout: DEFAULT_TIMEOUT,
negated: false,
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn set_timeout(&mut self, timeout: Duration) -> &mut Self {
self.timeout = timeout;
self
}
pub fn not(&self) -> Self {
let mut c = self.clone();
c.negated = true;
c
}
pub async fn to_have_title(&self, expected: &str) -> Result<()> {
let p = self.page.clone();
let negated = self.negated;
let timeout = self.timeout;
let expected_owned = expected.to_string();
let expected_for_msg = expected.to_string();
wait_for(
move || {
let p = p.clone();
let expected_owned = expected_owned.clone();
async move { Ok(p.title().await? == expected_owned) }
},
timeout,
negated,
move || {
format!(
"to_have_title('{expected_for_msg}') timed out after {}ms",
timeout.as_millis()
)
},
)
.await
}
pub async fn to_have_url(&self, expected: &str) -> Result<()> {
let p = self.page.clone();
let negated = self.negated;
let timeout = self.timeout;
let expected_owned = expected.to_string();
let expected_for_msg = expected.to_string();
wait_for(
move || {
let p = p.clone();
let expected_owned = expected_owned.clone();
async move { Ok(p.url().await? == expected_owned) }
},
timeout,
negated,
move || {
format!(
"to_have_url('{expected_for_msg}') timed out after {}ms",
timeout.as_millis()
)
},
)
.await
}
}