brewlog/tests/e2e/data_tests.rs
Jon Seager 7c1fbb99b8
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.
2026-02-10 11:09:30 +00:00

246 lines
7.1 KiB
Rust

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;
}