Skip to content

Commit

Permalink
Add automatic waiting to interaction and screenshot instructions
Browse files Browse the repository at this point in the history
  • Loading branch information
bglw committed Dec 3, 2024
1 parent 2a690f4 commit 815b8a0
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 46 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

## Unreleased

* Add automatic wait-and-timeout to Toolproof actions that get elements

## v0.8.0 (December 3, 2024)

* Add instructions for clicking and hovering elements on a page
Expand Down
71 changes: 71 additions & 0 deletions toolproof/src/definitions/browser/browser_specific.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use std::path::PathBuf;

use chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotFormat;

use crate::errors::{ToolproofInputError, ToolproofStepError, ToolproofTestFailure};

pub fn chrome_image_format(
filepath: &PathBuf,
) -> Result<CaptureScreenshotFormat, ToolproofStepError> {
match filepath.extension() {
Some(ext) => {
let ext = ext.to_string_lossy().to_lowercase();
match ext.as_str() {
"png" => Ok(CaptureScreenshotFormat::Png),
"webp" => Ok(CaptureScreenshotFormat::Webp),
"jpg" | "jpeg" => Ok(CaptureScreenshotFormat::Jpeg),
_ => Err(ToolproofStepError::External(
ToolproofInputError::StepRequirementsNotMet {
reason: "Image file extension must be png, webp, jpeg, or jpg".to_string(),
},
)),
}
}
None => Err(ToolproofStepError::External(
ToolproofInputError::StepRequirementsNotMet {
reason: "Image file path must have an extension".to_string(),
},
)),
}
}

pub async fn wait_for_chrome_element_selector(
page: &chromiumoxide::Page,
selector: &str,
timeout_secs: u64,
) -> Result<chromiumoxide::element::Element, ToolproofStepError> {
let start = std::time::Instant::now();
while start.elapsed().as_secs() < timeout_secs {
if let Ok(element) = page.find_element(selector).await {
return Ok(element);
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
Err(ToolproofStepError::Assertion(
ToolproofTestFailure::Custom {
msg: format!("Element {selector} could not be found within {timeout_secs}s"),
},
))
}

pub async fn wait_for_chrome_xpath_selectors(
page: &chromiumoxide::Page,
xpath: &str,
descriptor: &str,
timeout_secs: u64,
) -> Result<Vec<chromiumoxide::element::Element>, ToolproofStepError> {
let start = std::time::Instant::now();
while start.elapsed().as_secs() < timeout_secs {
if let Ok(elements) = page.find_xpaths(xpath).await {
if !elements.is_empty() {
return Ok(elements);
}
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
Err(ToolproofStepError::Assertion(
ToolproofTestFailure::Custom {
msg: format!("Element {descriptor} could not be found within {timeout_secs}s"),
},
))
}
99 changes: 53 additions & 46 deletions toolproof/src/definitions/browser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,21 @@ use super::{SegmentArgs, ToolproofInstruction, ToolproofRetriever};
use chromiumoxide::browser::{Browser, BrowserConfig};
use pagebrowse::{PagebrowseBuilder, Pagebrowser, PagebrowserWindow};

mod browser_specific;

const HARNESS: &'static str = include_str!("./harness.js");
const INIT_SCRIPT: &'static str = include_str!("./init.js");

fn harnessed(js: String) -> String {
HARNESS.replace("// insert_toolproof_inner_js", &js)
}

/// We want selector steps to timeout before the step itself does,
/// since it provides a better error. This makes that more likely.
fn auto_selector_timeout(civ: &Civilization) -> u64 {
civ.universe.ctx.params.timeout.saturating_sub(2).max(1)
}

pub enum BrowserTester {
Pagebrowse(Arc<Pagebrowser>),
Chrome {
Expand Down Expand Up @@ -70,29 +78,6 @@ async fn try_launch_browser(mut max: usize) -> (Browser, chromiumoxide::Handler)
}
}

fn chrome_image_format(filepath: &PathBuf) -> Result<CaptureScreenshotFormat, ToolproofStepError> {
match filepath.extension() {
Some(ext) => {
let ext = ext.to_string_lossy().to_lowercase();
match ext.as_str() {
"png" => Ok(CaptureScreenshotFormat::Png),
"webp" => Ok(CaptureScreenshotFormat::Webp),
"jpg" | "jpeg" => Ok(CaptureScreenshotFormat::Jpeg),
_ => Err(ToolproofStepError::External(
ToolproofInputError::StepRequirementsNotMet {
reason: "Image file extension must be png, webp, jpeg, or jpg".to_string(),
},
)),
}
}
None => Err(ToolproofStepError::External(
ToolproofInputError::StepRequirementsNotMet {
reason: "Image file path must have an extension".to_string(),
},
)),
}
}

enum InteractionType {
Click,
Hover,
Expand Down Expand Up @@ -212,7 +197,7 @@ impl BrowserWindow {
async fn screenshot_page(&self, filepath: PathBuf) -> Result<(), ToolproofStepError> {
match self {
BrowserWindow::Chrome(page) => {
let image_format = chrome_image_format(&filepath)?;
let image_format = browser_specific::chrome_image_format(&filepath)?;

page.save_screenshot(
ScreenshotParams {
Expand Down Expand Up @@ -241,16 +226,18 @@ impl BrowserWindow {
&self,
selector: &str,
filepath: PathBuf,
timeout_secs: u64,
) -> Result<(), ToolproofStepError> {
match self {
BrowserWindow::Chrome(page) => {
let image_format = chrome_image_format(&filepath)?;
let image_format = browser_specific::chrome_image_format(&filepath)?;

let element = page.find_element(selector).await.map_err(|e| {
ToolproofStepError::Assertion(ToolproofTestFailure::Custom {
msg: format!("Element {selector} could not be screenshot: {e}"),
})
})?;
let element = browser_specific::wait_for_chrome_element_selector(
page,
selector,
timeout_secs,
)
.await?;

element
.save_screenshot(image_format, filepath)
Expand All @@ -270,6 +257,7 @@ impl BrowserWindow {
&self,
text: &str,
interaction: InteractionType,
timeout_secs: u64,
) -> Result<(), ToolproofStepError> {
match self {
BrowserWindow::Chrome(page) => {
Expand All @@ -278,17 +266,20 @@ impl BrowserWindow {
format!("//{el}[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '{text}')]")
};
let xpath = [el_xpath("a"), el_xpath("button"), el_xpath("input")].join(" | ");
let elements = page.find_xpaths(xpath).await.map_err(|e| {
ToolproofStepError::Assertion(ToolproofTestFailure::Custom {
msg: format!("Element with text '{text}' could not be clicked: {e}"),
})
})?;

let elements = browser_specific::wait_for_chrome_xpath_selectors(
page,
&xpath,
&format!("with text '{text}'"),
timeout_secs,
)
.await?;

if elements.is_empty() {
return Err(ToolproofStepError::Assertion(
ToolproofTestFailure::Custom {
msg: format!(
"Clickable element containing text '{text}' does not exist. Did you mean to use 'I click the selector'?"
"Clickable element containing text '{text}' does not exist."
),
},
));
Expand Down Expand Up @@ -355,14 +346,16 @@ impl BrowserWindow {
&self,
selector: &str,
interaction: InteractionType,
timeout_secs: u64,
) -> Result<(), ToolproofStepError> {
match self {
BrowserWindow::Chrome(page) => {
let element = page.find_element(selector).await.map_err(|e| {
ToolproofStepError::Assertion(ToolproofTestFailure::Custom {
msg: format!("Element {selector} could not be clicked: {e}"),
})
})?;
let element = browser_specific::wait_for_chrome_element_selector(
page,
selector,
timeout_secs,
)
.await?;

element.scroll_into_view().await.map_err(|e| {
ToolproofStepError::Assertion(ToolproofTestFailure::Custom {
Expand Down Expand Up @@ -644,7 +637,9 @@ mod screenshots {
));
};

window.screenshot_element(&selector, resolved_path).await
window
.screenshot_element(&selector, resolved_path, auto_selector_timeout(civ))
.await
}
}
}
Expand Down Expand Up @@ -679,7 +674,9 @@ mod interactions {
));
};

window.interact_text(&text, InteractionType::Click).await
window
.interact_text(&text, InteractionType::Click, auto_selector_timeout(civ))
.await
}
}

Expand Down Expand Up @@ -710,7 +707,9 @@ mod interactions {
));
};

window.interact_text(&text, InteractionType::Hover).await
window
.interact_text(&text, InteractionType::Hover, auto_selector_timeout(civ))
.await
}
}

Expand Down Expand Up @@ -742,7 +741,11 @@ mod interactions {
};

window
.interact_selector(&selector, InteractionType::Click)
.interact_selector(
&selector,
InteractionType::Click,
auto_selector_timeout(civ),
)
.await
}
}
Expand Down Expand Up @@ -775,7 +778,11 @@ mod interactions {
};

window
.interact_selector(&selector, InteractionType::Hover)
.interact_selector(
&selector,
InteractionType::Hover,
auto_selector_timeout(civ),
)
.await
}
}
Expand Down

0 comments on commit 815b8a0

Please sign in to comment.