test(e2e): add homepage scan tests and fix stale element in checkin test
Add 3 homepage scan e2e tests covering all roaster/roast match permutations (new+new, existing+new, existing+existing). Fix StaleElementReference in checkin test by using JS clicks for Datastar-managed DOM elements.
This commit is contained in:
parent
264ef9c1fb
commit
3d61f71f1d
2 changed files with 250 additions and 15 deletions
|
|
@ -192,28 +192,36 @@ async fn checkin_with_new_cafe_and_scanned_roast() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Click the first result — shows the review form
|
// Click the first result — use JS to avoid StaleElementReference from Datastar updates
|
||||||
let result_btn = session
|
session
|
||||||
.driver
|
.driver
|
||||||
.find(By::Css("#nearby-results button"))
|
.execute(
|
||||||
|
"document.querySelector('#nearby-results button').click()",
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
result_btn.click().await.unwrap();
|
|
||||||
|
|
||||||
// Wait for the review form, then click "Next" to advance to step 2
|
// Wait for the review form, then click "Next" to advance to step 2
|
||||||
wait_for_text(&session.driver, "body", "Confirm cafe details")
|
wait_for_text(&session.driver, "body", "Confirm cafe details")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let buttons = session.driver.find_all(By::Css("button")).await.unwrap();
|
// Use JS to find+click atomically — avoids StaleElementReference from DOM updates
|
||||||
for button in buttons {
|
session
|
||||||
if button.is_displayed().await.unwrap_or(false) {
|
.driver
|
||||||
let text = button.text().await.unwrap_or_default();
|
.execute(
|
||||||
if text.contains("Next") {
|
r#"
|
||||||
button.click().await.unwrap();
|
for (const btn of document.querySelectorAll('button')) {
|
||||||
break;
|
if (btn.offsetParent !== null && btn.textContent.includes('Next')) {
|
||||||
|
btn.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
"#,
|
||||||
}
|
vec![],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Step 2: Scan a coffee bag via text prompt
|
// Step 2: Scan a coffee bag via text prompt
|
||||||
let prompt_input = wait_for_visible(&session.driver, "#checkin-scan-form input[name='prompt']")
|
let prompt_input = wait_for_visible(&session.driver, "#checkin-scan-form input[name='prompt']")
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ use wiremock::{Mock, ResponseTemplate};
|
||||||
|
|
||||||
use crate::helpers::auth::authenticate_browser;
|
use crate::helpers::auth::authenticate_browser;
|
||||||
use crate::helpers::browser::BrowserSession;
|
use crate::helpers::browser::BrowserSession;
|
||||||
use crate::helpers::server_helpers::spawn_app_with_openrouter_mock;
|
use crate::helpers::server_helpers::{
|
||||||
use crate::helpers::wait::{wait_for_url_contains, wait_for_visible};
|
create_default_roast, create_default_roaster, spawn_app_with_openrouter_mock,
|
||||||
|
};
|
||||||
|
use crate::helpers::wait::{wait_for_text, wait_for_url_contains, wait_for_visible};
|
||||||
|
|
||||||
fn mock_openrouter_response(json_content: &str) -> ResponseTemplate {
|
fn mock_openrouter_response(json_content: &str) -> ResponseTemplate {
|
||||||
let body = serde_json::json!({
|
let body = serde_json::json!({
|
||||||
|
|
@ -109,3 +111,228 @@ async fn extract_roaster_via_text_prompt_populates_form() {
|
||||||
|
|
||||||
session.quit().await;
|
session.quit().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mock_bag_scan_response(json_content: &str) -> ResponseTemplate {
|
||||||
|
mock_openrouter_response(json_content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trigger the homepage scan by setting the hidden image input and submitting the form.
|
||||||
|
/// The `brew-photo-capture` web component is image-only; we bypass it via JS.
|
||||||
|
async fn trigger_homepage_scan(driver: &WebDriver) {
|
||||||
|
driver
|
||||||
|
.execute(
|
||||||
|
r#"
|
||||||
|
document.getElementById('scan-image').value = 'data:image/png;base64,iVBOR';
|
||||||
|
document.getElementById('scan-extract-form').requestSubmit();
|
||||||
|
"#,
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find and click a visible button whose text contains the given substring.
|
||||||
|
async fn click_button_with_text(driver: &WebDriver, text: &str) {
|
||||||
|
let buttons = driver.find_all(By::Css("button")).await.unwrap();
|
||||||
|
for button in buttons {
|
||||||
|
if button.is_displayed().await.unwrap_or(false) {
|
||||||
|
let btn_text = button.text().await.unwrap_or_default();
|
||||||
|
if btn_text.contains(text) {
|
||||||
|
button.click().await.unwrap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!("No visible button containing '{text}' found");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn homepage_scan_new_roaster_and_new_roast() {
|
||||||
|
let app = spawn_app_with_openrouter_mock().await;
|
||||||
|
let mock_server = app.mock_server.as_ref().unwrap();
|
||||||
|
|
||||||
|
Mock::given(method("POST"))
|
||||||
|
.and(path("/api/v1/chat/completions"))
|
||||||
|
.respond_with(mock_bag_scan_response(
|
||||||
|
r#"{"roaster": {"name": "Koppi", "country": "SE", "city": "Helsingborg"}, "roast": {"name": "Finca Vista", "origin": "Colombia", "region": "Huila", "producer": "Luis Anibal", "process": "Washed", "tasting_notes": ["Caramel", "Red Apple"]}}"#,
|
||||||
|
))
|
||||||
|
.mount(mock_server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let session = BrowserSession::new(&app.address).await.unwrap();
|
||||||
|
authenticate_browser(&session, &app).await.unwrap();
|
||||||
|
|
||||||
|
session.goto("/").await.unwrap();
|
||||||
|
wait_for_visible(&session.driver, "brew-photo-capture")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
trigger_homepage_scan(&session.driver).await;
|
||||||
|
|
||||||
|
// Wait for extraction to complete — the result form appears
|
||||||
|
// Neither roaster nor roast matched → editable form with "Save Roaster & Roast" button
|
||||||
|
wait_for_text(&session.driver, "body", "Save Roaster")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Verify extracted fields are populated in form inputs (not visible body text)
|
||||||
|
let roaster_input = session
|
||||||
|
.driver
|
||||||
|
.find(By::Css("[data-bind\\:_roaster-name]"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let roaster_val = roaster_input.value().await.unwrap().unwrap_or_default();
|
||||||
|
assert_eq!(roaster_val, "Koppi", "Roaster name input should be filled");
|
||||||
|
|
||||||
|
let roast_input = session
|
||||||
|
.driver
|
||||||
|
.find(By::Css("[data-bind\\:_roast-name]"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let roast_val = roast_input.value().await.unwrap().unwrap_or_default();
|
||||||
|
assert_eq!(
|
||||||
|
roast_val, "Finca Vista",
|
||||||
|
"Roast name input should be filled"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Submit — creates roaster + roast + bag, then reloads
|
||||||
|
click_button_with_text(&session.driver, "Save Roaster").await;
|
||||||
|
|
||||||
|
// After reload, the new bag should appear in Open Bags
|
||||||
|
wait_for_text(&session.driver, "#open-bags-section", "Finca Vista")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
session.quit().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn homepage_scan_existing_roaster_new_roast() {
|
||||||
|
let app = spawn_app_with_openrouter_mock().await;
|
||||||
|
let mock_server = app.mock_server.as_ref().unwrap();
|
||||||
|
|
||||||
|
// Pre-create the roaster so the extraction matches it
|
||||||
|
let _roaster = create_default_roaster(&app).await;
|
||||||
|
|
||||||
|
// AI returns data matching the existing roaster name but a new roast
|
||||||
|
Mock::given(method("POST"))
|
||||||
|
.and(path("/api/v1/chat/completions"))
|
||||||
|
.respond_with(mock_bag_scan_response(
|
||||||
|
r#"{"roaster": {"name": "Test Roasters", "country": "UK"}, "roast": {"name": "Gesha Village", "origin": "Ethiopia", "region": "Bench Maji", "producer": "Gesha Village Estate", "process": "Natural", "tasting_notes": ["Jasmine", "Peach"]}}"#,
|
||||||
|
))
|
||||||
|
.mount(mock_server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let session = BrowserSession::new(&app.address).await.unwrap();
|
||||||
|
authenticate_browser(&session, &app).await.unwrap();
|
||||||
|
|
||||||
|
session.goto("/").await.unwrap();
|
||||||
|
wait_for_visible(&session.driver, "brew-photo-capture")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
trigger_homepage_scan(&session.driver).await;
|
||||||
|
|
||||||
|
// Wait for extraction — roaster matched, roast not → "Save Roast" button
|
||||||
|
wait_for_text(&session.driver, "body", "Save Roast")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Roaster should show as a card (matched) — visible in body text
|
||||||
|
let body_text = session
|
||||||
|
.driver
|
||||||
|
.find(By::Css("body"))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
body_text.contains("Test Roasters"),
|
||||||
|
"Should show matched roaster name in card"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Roast name is in an editable input (not matched)
|
||||||
|
let roast_input = session
|
||||||
|
.driver
|
||||||
|
.find(By::Css("[data-bind\\:_roast-name]"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let roast_val = roast_input.value().await.unwrap().unwrap_or_default();
|
||||||
|
assert_eq!(
|
||||||
|
roast_val, "Gesha Village",
|
||||||
|
"Roast name input should be filled"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Submit — creates roast + bag under existing roaster, then reloads
|
||||||
|
click_button_with_text(&session.driver, "Save Roast").await;
|
||||||
|
|
||||||
|
// After reload, the new bag should appear in Open Bags
|
||||||
|
wait_for_text(&session.driver, "#open-bags-section", "Gesha Village")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
session.quit().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn homepage_scan_existing_roaster_and_existing_roast() {
|
||||||
|
let app = spawn_app_with_openrouter_mock().await;
|
||||||
|
let mock_server = app.mock_server.as_ref().unwrap();
|
||||||
|
|
||||||
|
// Pre-create both roaster and roast so the extraction matches both
|
||||||
|
let roaster = create_default_roaster(&app).await;
|
||||||
|
let _roast = create_default_roast(&app, roaster.id).await;
|
||||||
|
|
||||||
|
// AI returns data matching both existing entities
|
||||||
|
Mock::given(method("POST"))
|
||||||
|
.and(path("/api/v1/chat/completions"))
|
||||||
|
.respond_with(mock_bag_scan_response(
|
||||||
|
r#"{"roaster": {"name": "Test Roasters", "country": "UK"}, "roast": {"name": "Test Roast", "origin": "Ethiopia", "region": "Yirgacheffe", "producer": "Coop", "process": "Washed", "tasting_notes": ["Blueberry"]}}"#,
|
||||||
|
))
|
||||||
|
.mount(mock_server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let session = BrowserSession::new(&app.address).await.unwrap();
|
||||||
|
authenticate_browser(&session, &app).await.unwrap();
|
||||||
|
|
||||||
|
session.goto("/").await.unwrap();
|
||||||
|
wait_for_visible(&session.driver, "brew-photo-capture")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
trigger_homepage_scan(&session.driver).await;
|
||||||
|
|
||||||
|
// Wait for extraction — both matched → "Open Bag" button (open_bag defaults to true)
|
||||||
|
wait_for_text(&session.driver, "body", "Open Bag")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Both roaster and roast should show as cards (matched)
|
||||||
|
let body_text = session
|
||||||
|
.driver
|
||||||
|
.find(By::Css("body"))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
body_text.contains("Test Roasters"),
|
||||||
|
"Should show matched roaster"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
body_text.contains("Test Roast"),
|
||||||
|
"Should show matched roast"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Submit — creates bag only for existing roast, then reloads
|
||||||
|
click_button_with_text(&session.driver, "Open Bag").await;
|
||||||
|
|
||||||
|
// After reload, the bag should appear in Open Bags
|
||||||
|
wait_for_text(&session.driver, "#open-bags-section", "Test Roast")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
session.quit().await;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue