brewlog/tests/e2e/helpers/forms.rs
Jon Seager 73976cbe51
feat(test): add e2e browser test helpers
BrowserSession wraps headless Chrome via thirtyfour. Auth helper injects
session cookies to bypass WebAuthn. Wait helpers handle Datastar's async
DOM updates (visibility, text, URL). Form helpers find visible elements
to avoid hidden duplicates on tabbed pages. Chromedriver is auto-spawned
on first use via a dedicated parked thread with PR_SET_PDEATHSIG so the
kernel kills it when the test binary exits.
2026-02-10 10:44:47 +00:00

95 lines
3.4 KiB
Rust

use std::time::Duration;
use thirtyfour::error::no_such_element;
use thirtyfour::prelude::*;
/// Find the first visible element matching a CSS selector.
/// The `/add` page has duplicate `name` fields across tabbed forms (roaster, roast, etc.)
/// hidden via Datastar `data-show`. This ensures we interact with the active form's fields.
async fn find_visible(driver: &WebDriver, css: &str) -> WebDriverResult<WebElement> {
let elements = driver.find_all(By::Css(css)).await?;
for el in elements {
if el.is_displayed().await.unwrap_or(false) {
return Ok(el);
}
}
Err(no_such_element(format!(
"No visible element found for selector: {css}"
)))
}
/// Fill a text input identified by its `name` attribute.
/// Finds the first *visible* match to avoid hidden duplicate fields on tabbed pages.
pub async fn fill_input(driver: &WebDriver, name: &str, value: &str) -> WebDriverResult<()> {
let input = find_visible(driver, &format!("input[name='{name}']")).await?;
input.clear().await?;
input.send_keys(value).await?;
Ok(())
}
/// Fill a textarea identified by its `name` attribute.
/// Finds the first *visible* match to avoid hidden duplicate fields on tabbed pages.
pub async fn fill_textarea(driver: &WebDriver, name: &str, value: &str) -> WebDriverResult<()> {
let textarea = find_visible(driver, &format!("textarea[name='{name}']")).await?;
textarea.clear().await?;
textarea.send_keys(value).await?;
Ok(())
}
/// Select an option from a native `<select>` by name and value.
/// Finds the first *visible* select to avoid hidden duplicates on tabbed pages.
pub async fn select_option(driver: &WebDriver, name: &str, value: &str) -> WebDriverResult<()> {
let select = find_visible(driver, &format!("select[name='{name}']")).await?;
let option = select
.find(By::Css(&format!("option[value='{value}']")))
.await?;
option.click().await?;
Ok(())
}
/// Interact with a `<searchable-select>` web component:
/// 1. Find the search input (role=combobox) inside the component
/// 2. Type to filter options
/// 3. Click the first visible option button
pub async fn select_searchable(
driver: &WebDriver,
name: &str,
search_text: &str,
) -> WebDriverResult<()> {
let component = driver
.find(By::Css(&format!("searchable-select[name='{name}']")))
.await?;
let search_input = component.find(By::Css("input[role='combobox']")).await?;
search_input.send_keys(search_text).await?;
// Wait for the dropdown to appear and options to filter
tokio::time::sleep(Duration::from_millis(300)).await;
// Click the first visible option
let options = component.find_all(By::Css("button[role='option']")).await?;
for option in options {
if option.is_displayed().await.unwrap_or(false) {
option.click().await?;
return Ok(());
}
}
Err(no_such_element(format!(
"No visible option found in searchable-select[name='{name}'] after typing '{search_text}'"
)))
}
/// Click the visible submit button on the page.
pub async fn submit_visible_form(driver: &WebDriver) -> WebDriverResult<()> {
let buttons = driver.find_all(By::Css("button[type='submit']")).await?;
for button in buttons {
if button.is_displayed().await.unwrap_or(false) {
button.click().await?;
return Ok(());
}
}
Err(no_such_element(
"No visible submit button found".to_string(),
))
}