brewlog/tests/e2e/timeline_tests.rs
Jon Seager 0e3847ade9
feat: sync timeline events with entity edits
Add background timeline rebuild task (mirroring stats cache pattern)
that refreshes denormalized timeline event snapshots when entities are
updated. Includes cascade logic so editing a roaster refreshes timeline
events for its roasts, bags, brews, and cups.

- Add update_by_entity/delete_by_entity/delete_all to TimelineEventRepository
- Add TimelineInvalidator with debounced background rebuild task
- Add invalidate() calls to all 7 entity update handlers
- Add delete_by_entity cleanup to define_delete_handler! macro
- Add gear_id filter to BrewFilter for cascade traversal
- Add `brewlog timeline rebuild` CLI command for full rebuild
- Add 5 integration tests for timeline sync behavior
2026-02-15 11:11:07 +00:00

74 lines
2.5 KiB
Rust

use thirtyfour::prelude::*;
use crate::helpers::auth::authenticate_browser;
use crate::helpers::browser::BrowserSession;
use crate::helpers::forms::{fill_input, submit_visible_form};
use crate::helpers::server_helpers::{
create_default_roast, create_default_roaster, spawn_app_with_timeline_sync,
};
use crate::helpers::wait::{wait_for_url_not_contains, wait_for_visible};
#[tokio::test]
async fn editing_roaster_updates_timeline_events_in_browser() {
let app = spawn_app_with_timeline_sync().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();
// Verify original roaster name appears in the timeline
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();
assert!(
text.contains("Test Roasters"),
"Expected original roaster name 'Test Roasters' in timeline, got: {text}"
);
// Navigate to the roaster edit page and change the name
session
.goto(&format!("/roasters/{}/edit", roaster.id))
.await
.unwrap();
wait_for_visible(&session.driver, "input[name='name']")
.await
.unwrap();
fill_input(&session.driver, "name", "E2E Renamed Roasters")
.await
.unwrap();
submit_visible_form(&session.driver).await.unwrap();
// Wait for redirect back to the detail page
wait_for_url_not_contains(&session.driver, "/edit")
.await
.unwrap();
// Wait for the background timeline rebuild task to process
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
// Go back to the timeline and verify both events reflect the new name
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();
assert!(
text.contains("E2E Renamed Roasters"),
"Expected updated roaster name 'E2E Renamed Roasters' in timeline after edit, got: {text}"
);
assert!(
!text.contains("Test Roasters"),
"Expected original name 'Test Roasters' to no longer appear in timeline, got: {text}"
);
session.quit().await;
}