//! The day view, driven through the router against a real database. //! //! Same property as its siblings: no Tauri runtime and no window. What is new //! here is that the assertions are about *placement*, which is the thing the //! vocabulary could not carry until `makeover-layout` 0.24.0. use std::sync::Arc; use chrono::{Duration, Local, NaiveDate, TimeZone, Utc}; use goingson_core::NewEvent; use quasi_http::Serves as _; use quasi_router::{Outcome, Params, Request, Response}; use super::super::router; use crate::state::{AppState, DESKTOP_USER_ID}; /// State with the desktop user in place, which is who the handlers read as. async fn state() -> Arc { let (state, _) = crate::test_utils::setup_test_state().await; let now = Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(); state .db .conn() .unwrap() .execute( "INSERT OR IGNORE INTO users (id, email, password_hash, display_name, created_at) \ VALUES (?, ?, ?, ?, ?)", rusqlite::params![ DESKTOP_USER_ID.to_string(), "desktop@localhost", "x", "Desktop User", &now, ], ) .unwrap(); state } /// The day every test in this file draws. /// /// Fixed rather than "today", because a test whose fixtures are relative to the /// clock passes at 09:00 and fails at 23:50 when the local day rolls over /// between building the event and reading the screen. const DAY: &str = "2026-03-04"; fn day() -> NaiveDate { NaiveDate::parse_from_str(DAY, "%Y-%m-%d").unwrap() } /// An event on the test day, at a local wall-clock time. /// /// Local rather than UTC because the screen's whole job is placing things on a /// *local* day, and a fixture written in UTC would silently test a different /// hour on any machine not on UTC. fn event(state: &AppState, title: &str, from: (u32, u32), minutes: i64) { let start = Local .from_local_datetime(&day().and_hms_opt(from.0, from.1, 0).unwrap()) .unwrap() .with_timezone(&Utc); state .events .create( DESKTOP_USER_ID, NewEvent::builder(title, start) .end_time(start + Duration::minutes(minutes)) .build(), ) .unwrap(); } fn get(state: &AppState, path: &str) -> Response { router() .handle(state, Request::get(path).carrying(Params::new())) .expect("the route answers") } fn html(response: Response) -> String { match response.outcome { Outcome::Screen(screen) => quasi_webview::Webview::new().screen(&screen), Outcome::Fragment { node, .. } => quasi_webview::Webview::new().fragment(&node), Outcome::Goto(action) => panic!("expected content, got a redirect to {action:?}"), Outcome::Over(_) => panic!("expected content, got a screen drawn over it"), Outcome::Anchored { .. } => { panic!("expected content, got a screen drawn at a point on it") } Outcome::Suggestions { field, .. } => { panic!("expected content, got a suggestion list for `{field}`") } Outcome::File { name, .. } => panic!("expected content, got the file `{name}`"), Outcome::Locate(_) => panic!("expected content, got a place on a map"), // `cb62a9dc`. Work that runs somewhere else and a region that says so: // not content, and not a place either. Outcome::Started { region, .. } => { panic!("expected content, got work started in `{region}`") } } } fn screen(state: &AppState) -> String { html(get(state, &format!("/day/{DAY}"))) } #[tokio::test] async fn an_item_is_placed_at_the_hour_it_starts() { let state = state().await; event(&state, "Standup", (9, 0), 60); let markup = screen(&state); // 09:00 on a midnight-to-midnight axis is 540/1440 = 37.5%, and an hour is // 60/1440 = 4.1667%. This is the assertion the whole vocabulary change // exists for: before Track there was no way to say either number. assert!(markup.contains("--track-at:37.5000%"), "{markup}"); assert!(markup.contains("--track-for:4.1667%"), "{markup}"); assert!(markup.contains("Standup"), "{markup}"); } #[tokio::test] async fn two_things_at_once_take_lanes_and_say_they_clash() { let state = state().await; event(&state, "Standup", (9, 0), 60); event(&state, "Interview", (9, 30), 60); let markup = screen(&state); // The geometry: two lanes, so neither is drawn on top of the other. assert!(markup.contains("--track-lane:0"), "{markup}"); assert!(markup.contains("--track-lane:1"), "{markup}"); assert!(markup.contains("--track-lanes:2"), "{markup}"); // And the judgment, which is a different thing from the geometry and comes // from the backend's own `detect_conflicts` rather than from the overlap. assert!(markup.contains("clashes"), "{markup}"); } #[tokio::test] async fn back_to_back_items_are_not_a_clash() { let state = state().await; event(&state, "First", (9, 0), 60); event(&state, "Second", (10, 0), 60); let markup = screen(&state); // The off-by-one this guards is the classic day-view bug: every appointment // on the hour reading as a conflict with the one before it. assert!(markup.contains("--track-lanes:1"), "{markup}"); assert!(!markup.contains("clashes"), "{markup}"); } #[tokio::test] async fn the_axis_carries_an_hourly_ruler() { let state = state().await; let markup = screen(&state); // 96 quarter-hour slots, labelled every fourth. Matches // `day-planning-render.js:56`'s `for (let hour = 0; hour < 24; hour++)`. assert!(markup.contains(">00:00<"), "{markup}"); assert!(markup.contains(">09:00<"), "{markup}"); assert!(markup.contains(">23:00<"), "{markup}"); assert!(!markup.contains(">09:15<"), "labelled hourly, not per slot"); } #[tokio::test] async fn the_interesting_hour_travels_as_a_moment() { let state = state().await; let markup = screen(&state); // What replaces `day-planning-render.js:321`'s `const targetHour = 9`. The // app says which hour matters; the host decides how to get there. assert!(markup.contains("data-focus=\"540\""), "{markup}"); } #[tokio::test] async fn an_events_title_cannot_become_markup() { let state = state().await; event(&state, "", (9, 0), 60); let markup = screen(&state); // The property this whole port exists for. Being placed on an axis is not a // way around the renderer's escaping. assert!(!markup.contains("