test: add e2e tests for data page, detail pages, bag close, and navigation
Cover tab switching, search, pagination, and sorting on the data list page. Verify detail page rendering for roasters, roasts, bags, gear, and cafes. Test the close-bag flow from the detail page. Check that home, stats, and timeline pages load correctly with data.
This commit is contained in:
parent
034630ade0
commit
7c1fbb99b8
5 changed files with 525 additions and 0 deletions
52
tests/e2e/bag_tests.rs
Normal file
52
tests/e2e/bag_tests.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
use thirtyfour::prelude::*;
|
||||
|
||||
use crate::helpers::auth::authenticate_browser;
|
||||
use crate::helpers::browser::BrowserSession;
|
||||
use crate::helpers::server_helpers::{
|
||||
create_default_bag, create_default_roast, create_default_roaster, spawn_app_with_auth,
|
||||
};
|
||||
use crate::helpers::wait::wait_for_visible;
|
||||
|
||||
#[tokio::test]
|
||||
async fn close_bag_from_detail_page() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let roast = create_default_roast(&app, roaster.id).await;
|
||||
let bag = create_default_bag(&app, roast.id).await;
|
||||
|
||||
let session = BrowserSession::new(&app.address).await.unwrap();
|
||||
authenticate_browser(&session, &app).await.unwrap();
|
||||
|
||||
session.goto(&format!("/bags/{}", bag.id)).await.unwrap();
|
||||
|
||||
// Find and click the "Close Bag" button
|
||||
wait_for_visible(&session.driver, "button").await.unwrap();
|
||||
let buttons = session.driver.find_all(By::Css("button")).await.unwrap();
|
||||
for button in buttons {
|
||||
let text = button.text().await.unwrap_or_default();
|
||||
if text.contains("Close Bag") {
|
||||
button.click().await.unwrap();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Accept the confirm() dialog
|
||||
session.driver.accept_alert().await.unwrap();
|
||||
|
||||
// After closing, the "Close Bag" button should disappear.
|
||||
// Wait for the page to update — the @put returns a redirect or refreshes.
|
||||
// The button text "Close Bag" should no longer be present.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
|
||||
let body = session.driver.find(By::Css("body")).await.unwrap();
|
||||
let text = body.text().await.unwrap();
|
||||
assert!(
|
||||
!text.contains("Close Bag"),
|
||||
"Close Bag button should be gone after closing"
|
||||
);
|
||||
|
||||
// Bag should still show on the page (just marked as closed)
|
||||
assert!(text.contains("Test Roast"), "Should still show roast name");
|
||||
|
||||
session.quit().await;
|
||||
}
|
||||
246
tests/e2e/data_tests.rs
Normal file
246
tests/e2e/data_tests.rs
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use thirtyfour::prelude::*;
|
||||
|
||||
use crate::helpers::auth::authenticate_browser;
|
||||
use crate::helpers::browser::BrowserSession;
|
||||
use crate::helpers::server_helpers::{
|
||||
create_default_bag, create_default_gear, create_default_roast, create_default_roaster,
|
||||
create_roaster_with_name, spawn_app_with_auth,
|
||||
};
|
||||
use crate::helpers::wait::{wait_for_text, wait_for_visible};
|
||||
|
||||
#[tokio::test]
|
||||
async fn tab_switching_loads_correct_entity_list() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let roast = create_default_roast(&app, roaster.id).await;
|
||||
let _bag = create_default_bag(&app, roast.id).await;
|
||||
let _grinder = create_default_gear(&app, "grinder", "Timemore", "C3").await;
|
||||
|
||||
let session = BrowserSession::new(&app.address).await.unwrap();
|
||||
authenticate_browser(&session, &app).await.unwrap();
|
||||
|
||||
// Default tab is brews — but we have no brews, so check empty state
|
||||
session.goto("/data").await.unwrap();
|
||||
wait_for_visible(&session.driver, "#data-content")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Click "Roasters" tab (desktop)
|
||||
let tabs = session
|
||||
.driver
|
||||
.find_all(By::Css("nav[role='tablist'] button[role='tab']"))
|
||||
.await
|
||||
.unwrap();
|
||||
for tab in &tabs {
|
||||
let text = tab.text().await.unwrap_or_default();
|
||||
if text.contains("Roasters") {
|
||||
tab.click().await.unwrap();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for roaster list to load
|
||||
wait_for_text(&session.driver, "#data-content", "Test Roasters")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Click "Gear" tab
|
||||
let tabs = session
|
||||
.driver
|
||||
.find_all(By::Css("nav[role='tablist'] button[role='tab']"))
|
||||
.await
|
||||
.unwrap();
|
||||
for tab in &tabs {
|
||||
let text = tab.text().await.unwrap_or_default();
|
||||
if text.contains("Gear") {
|
||||
tab.click().await.unwrap();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
wait_for_text(&session.driver, "#data-content", "Timemore")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
session.quit().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_filters_list() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
create_roaster_with_name(&app, "Acme Coffee").await;
|
||||
create_roaster_with_name(&app, "Beta Beans").await;
|
||||
|
||||
let session = BrowserSession::new(&app.address).await.unwrap();
|
||||
authenticate_browser(&session, &app).await.unwrap();
|
||||
|
||||
session.goto("/data?type=roasters").await.unwrap();
|
||||
wait_for_visible(&session.driver, "#roaster-list")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Both roasters should be visible initially
|
||||
let body = session.driver.find(By::Css("#data-content")).await.unwrap();
|
||||
let text = body.text().await.unwrap();
|
||||
assert!(text.contains("Acme Coffee"), "Should show Acme Coffee");
|
||||
assert!(text.contains("Beta Beans"), "Should show Beta Beans");
|
||||
|
||||
// Type in search field — the data page search is on the outer page (input[type='search'])
|
||||
let search = session
|
||||
.driver
|
||||
.find(By::Css("input[type='search']"))
|
||||
.await
|
||||
.unwrap();
|
||||
search.send_keys("Acme").await.unwrap();
|
||||
|
||||
// Wait for debounce (300ms) + fragment load
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
// Wait for the list to update with filtered results
|
||||
wait_for_text(&session.driver, "#data-content", "Acme Coffee")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Beta Beans should be filtered out
|
||||
let text = session
|
||||
.driver
|
||||
.find(By::Css("#data-content"))
|
||||
.await
|
||||
.unwrap()
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
!text.contains("Beta Beans"),
|
||||
"Beta Beans should be filtered out"
|
||||
);
|
||||
|
||||
session.quit().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pagination_next_and_prev() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
|
||||
// Create 11 roasters — default page size is 10
|
||||
for i in 0..11 {
|
||||
create_roaster_with_name(&app, &format!("Roaster {i:02}")).await;
|
||||
}
|
||||
|
||||
let session = BrowserSession::new(&app.address).await.unwrap();
|
||||
authenticate_browser(&session, &app).await.unwrap();
|
||||
|
||||
session.goto("/data?type=roasters").await.unwrap();
|
||||
wait_for_visible(&session.driver, ".pagination-controls")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Should show "Page 1 of 2"
|
||||
let pagination = session
|
||||
.driver
|
||||
.find(By::Css(".pagination-controls"))
|
||||
.await
|
||||
.unwrap();
|
||||
let pagination_text = pagination.text().await.unwrap();
|
||||
assert!(
|
||||
pagination_text.contains("Page 1 of 2"),
|
||||
"Should show page 1 of 2, got: {pagination_text}"
|
||||
);
|
||||
|
||||
// Click Next
|
||||
let next_btn = pagination
|
||||
.find(By::XPath(".//button[contains(., 'Next')]"))
|
||||
.await
|
||||
.unwrap();
|
||||
next_btn.click().await.unwrap();
|
||||
|
||||
// Wait for page 2
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
let pagination = session
|
||||
.driver
|
||||
.find(By::Css(".pagination-controls"))
|
||||
.await
|
||||
.unwrap();
|
||||
wait_for_text(&session.driver, ".pagination-controls", "Page 2 of 2")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Click Prev
|
||||
let prev_btn = pagination
|
||||
.find(By::XPath(".//button[contains(., 'Prev')]"))
|
||||
.await
|
||||
.unwrap();
|
||||
prev_btn.click().await.unwrap();
|
||||
|
||||
wait_for_text(&session.driver, ".pagination-controls", "Page 1 of 2")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
session.quit().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sort_by_column_reverses_order() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
create_roaster_with_name(&app, "Alpha Roasters").await;
|
||||
create_roaster_with_name(&app, "Zulu Coffee").await;
|
||||
|
||||
let session = BrowserSession::new(&app.address).await.unwrap();
|
||||
authenticate_browser(&session, &app).await.unwrap();
|
||||
|
||||
session.goto("/data?type=roasters").await.unwrap();
|
||||
wait_for_visible(&session.driver, "#roaster-list")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Click "Name" sort header to sort ascending
|
||||
let name_header = session
|
||||
.driver
|
||||
.find(By::XPath("//thead//button[contains(., 'Name')]"))
|
||||
.await
|
||||
.unwrap();
|
||||
name_header.click().await.unwrap();
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
// Get row order after first click (ascending)
|
||||
let rows = session
|
||||
.driver
|
||||
.find_all(By::Css("#roaster-list tbody tr"))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut first_order = Vec::new();
|
||||
for row in &rows {
|
||||
first_order.push(row.text().await.unwrap_or_default());
|
||||
}
|
||||
|
||||
// Click again to reverse
|
||||
let name_header = session
|
||||
.driver
|
||||
.find(By::XPath("//thead//button[contains(., 'Name')]"))
|
||||
.await
|
||||
.unwrap();
|
||||
name_header.click().await.unwrap();
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
let rows = session
|
||||
.driver
|
||||
.find_all(By::Css("#roaster-list tbody tr"))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut second_order = Vec::new();
|
||||
for row in &rows {
|
||||
second_order.push(row.text().await.unwrap_or_default());
|
||||
}
|
||||
|
||||
// Orders should be different (reversed)
|
||||
assert_ne!(
|
||||
first_order, second_order,
|
||||
"Clicking sort header twice should reverse order"
|
||||
);
|
||||
|
||||
session.quit().await;
|
||||
}
|
||||
136
tests/e2e/detail_tests.rs
Normal file
136
tests/e2e/detail_tests.rs
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
use thirtyfour::prelude::*;
|
||||
|
||||
use crate::helpers::auth::authenticate_browser;
|
||||
use crate::helpers::browser::BrowserSession;
|
||||
use crate::helpers::server_helpers::{
|
||||
create_default_bag, create_default_cafe, create_default_gear, create_default_roast,
|
||||
create_default_roaster, create_roaster_with_payload, spawn_app_with_auth,
|
||||
};
|
||||
use crate::helpers::wait::wait_for_visible;
|
||||
|
||||
use brewlog::domain::coffee::roasters::NewRoaster;
|
||||
|
||||
#[tokio::test]
|
||||
async fn roaster_detail_shows_all_fields() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let roaster = create_roaster_with_payload(
|
||||
&app,
|
||||
NewRoaster {
|
||||
name: "Detail Roasters".to_string(),
|
||||
country: "JP".to_string(),
|
||||
city: Some("Tokyo".to_string()),
|
||||
homepage: Some("https://example.com".to_string()),
|
||||
created_at: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let session = BrowserSession::new(&app.address).await.unwrap();
|
||||
authenticate_browser(&session, &app).await.unwrap();
|
||||
|
||||
session
|
||||
.goto(&format!("/roasters/{}", roaster.slug))
|
||||
.await
|
||||
.unwrap();
|
||||
wait_for_visible(&session.driver, "h1").await.unwrap();
|
||||
|
||||
let body = session.driver.find(By::Css("body")).await.unwrap();
|
||||
let text = body.text().await.unwrap();
|
||||
assert!(text.contains("Detail Roasters"), "Should show roaster name");
|
||||
assert!(text.contains("JP"), "Should show country code");
|
||||
assert!(text.contains("Tokyo"), "Should show city");
|
||||
assert!(text.contains("Visit Website"), "Should show homepage link");
|
||||
|
||||
session.quit().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn roast_detail_shows_coffee_info() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let roast = create_default_roast(&app, roaster.id).await;
|
||||
|
||||
let session = BrowserSession::new(&app.address).await.unwrap();
|
||||
authenticate_browser(&session, &app).await.unwrap();
|
||||
|
||||
session
|
||||
.goto(&format!("/roasters/{}/roasts/{}", roaster.slug, roast.slug))
|
||||
.await
|
||||
.unwrap();
|
||||
wait_for_visible(&session.driver, "h1").await.unwrap();
|
||||
|
||||
let body = session.driver.find(By::Css("body")).await.unwrap();
|
||||
let text = body.text().await.unwrap();
|
||||
assert!(text.contains("Test Roast"), "Should show roast name");
|
||||
assert!(text.contains("Ethiopia"), "Should show origin");
|
||||
assert!(text.contains("Yirgacheffe"), "Should show region");
|
||||
assert!(text.contains("Washed"), "Should show process");
|
||||
assert!(text.contains("Blueberry"), "Should show tasting notes");
|
||||
|
||||
session.quit().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bag_detail_shows_status_and_close_button() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let roast = create_default_roast(&app, roaster.id).await;
|
||||
let bag = create_default_bag(&app, roast.id).await;
|
||||
|
||||
let session = BrowserSession::new(&app.address).await.unwrap();
|
||||
authenticate_browser(&session, &app).await.unwrap();
|
||||
|
||||
session.goto(&format!("/bags/{}", bag.id)).await.unwrap();
|
||||
wait_for_visible(&session.driver, "h1").await.unwrap();
|
||||
|
||||
let body = session.driver.find(By::Css("body")).await.unwrap();
|
||||
let text = body.text().await.unwrap();
|
||||
assert!(text.contains("Test Roast"), "Should show roast name");
|
||||
assert!(text.contains("250"), "Should show bag amount");
|
||||
|
||||
// Close Bag button should be visible (bag is open)
|
||||
assert!(text.contains("Close Bag"), "Should show Close Bag button");
|
||||
|
||||
session.quit().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gear_detail_shows_fields() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let gear = create_default_gear(&app, "grinder", "Comandante", "C40 MK4").await;
|
||||
|
||||
let session = BrowserSession::new(&app.address).await.unwrap();
|
||||
authenticate_browser(&session, &app).await.unwrap();
|
||||
|
||||
session.goto(&format!("/gear/{}", gear.id)).await.unwrap();
|
||||
wait_for_visible(&session.driver, "h1").await.unwrap();
|
||||
|
||||
let body = session.driver.find(By::Css("body")).await.unwrap();
|
||||
let text = body.text().await.unwrap();
|
||||
assert!(text.contains("Comandante"), "Should show make");
|
||||
assert!(text.contains("C40 MK4"), "Should show model");
|
||||
|
||||
session.quit().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cafe_detail_shows_location() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let cafe = create_default_cafe(&app).await;
|
||||
|
||||
let session = BrowserSession::new(&app.address).await.unwrap();
|
||||
authenticate_browser(&session, &app).await.unwrap();
|
||||
|
||||
session
|
||||
.goto(&format!("/cafes/{}", cafe.slug))
|
||||
.await
|
||||
.unwrap();
|
||||
wait_for_visible(&session.driver, "h1").await.unwrap();
|
||||
|
||||
let body = session.driver.find(By::Css("body")).await.unwrap();
|
||||
let text = body.text().await.unwrap();
|
||||
assert!(text.contains("Blue Bottle"), "Should show cafe name");
|
||||
assert!(text.contains("San Francisco"), "Should show city");
|
||||
|
||||
session.quit().await;
|
||||
}
|
||||
|
|
@ -2,8 +2,12 @@
|
|||
|
||||
mod helpers;
|
||||
|
||||
mod bag_tests;
|
||||
mod brew_tests;
|
||||
mod checkin_tests;
|
||||
mod data_tests;
|
||||
mod delete_tests;
|
||||
mod detail_tests;
|
||||
mod navigation_tests;
|
||||
mod roaster_tests;
|
||||
mod scan_tests;
|
||||
|
|
|
|||
87
tests/e2e/navigation_tests.rs
Normal file
87
tests/e2e/navigation_tests.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
use thirtyfour::prelude::*;
|
||||
|
||||
use crate::helpers::auth::authenticate_browser;
|
||||
use crate::helpers::browser::BrowserSession;
|
||||
use crate::helpers::server_helpers::{
|
||||
create_default_bag, create_default_roast, create_default_roaster, spawn_app_with_auth,
|
||||
};
|
||||
use crate::helpers::wait::wait_for_visible;
|
||||
|
||||
#[tokio::test]
|
||||
async fn home_page_shows_quick_actions_and_data() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let roast = create_default_roast(&app, roaster.id).await;
|
||||
let _bag = create_default_bag(&app, roast.id).await;
|
||||
|
||||
let session = BrowserSession::new(&app.address).await.unwrap();
|
||||
authenticate_browser(&session, &app).await.unwrap();
|
||||
|
||||
session.goto("/").await.unwrap();
|
||||
wait_for_visible(&session.driver, "body").await.unwrap();
|
||||
|
||||
let body = session.driver.find(By::Css("body")).await.unwrap();
|
||||
let text = body.text().await.unwrap();
|
||||
|
||||
// Quick action links should be present for authenticated users
|
||||
assert!(text.contains("Check In"), "Should show Check In action");
|
||||
assert!(text.contains("Add"), "Should show Add action");
|
||||
|
||||
// Open bag should appear
|
||||
assert!(text.contains("Test Roast"), "Should show open bag's roast");
|
||||
|
||||
session.quit().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stats_page_renders_with_data() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let _roast = create_default_roast(&app, roaster.id).await;
|
||||
|
||||
// Stats are computed by a background task with debouncing.
|
||||
// Give it time to populate.
|
||||
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
|
||||
|
||||
let session = BrowserSession::new(&app.address).await.unwrap();
|
||||
authenticate_browser(&session, &app).await.unwrap();
|
||||
|
||||
session.goto("/stats").await.unwrap();
|
||||
wait_for_visible(&session.driver, "h1").await.unwrap();
|
||||
|
||||
let body = session.driver.find(By::Css("body")).await.unwrap();
|
||||
let text = body.text().await.unwrap();
|
||||
|
||||
assert!(text.contains("Stats"), "Should show Stats heading");
|
||||
// With a roast created, we should see origin stats
|
||||
assert!(
|
||||
text.contains("Origins") || text.contains("Top Origin"),
|
||||
"Should show stat cards"
|
||||
);
|
||||
|
||||
session.quit().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn timeline_page_loads_events() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let _roaster = create_default_roaster(&app).await;
|
||||
|
||||
let session = BrowserSession::new(&app.address).await.unwrap();
|
||||
authenticate_browser(&session, &app).await.unwrap();
|
||||
|
||||
session.goto("/timeline").await.unwrap();
|
||||
wait_for_visible(&session.driver, "#timeline-events")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let body = session.driver.find(By::Css("body")).await.unwrap();
|
||||
let text = body.text().await.unwrap();
|
||||
// Creating a roaster generates a timeline event
|
||||
assert!(
|
||||
text.contains("Test Roasters"),
|
||||
"Should show roaster name in timeline"
|
||||
);
|
||||
|
||||
session.quit().await;
|
||||
}
|
||||
Loading…
Reference in a new issue