//! The app's furniture, checked against the app it belongs to. //! //! The assertion worth reading is the last one. A nav and a screen each name a //! place, and nothing but agreement between them makes a tab light: a key //! misspelled at either end is a header that quietly never marks anything, and //! no other test in this tree would notice. So the coherence check walks every //! screen the router serves and demands the nav hold the key it named. use std::sync::Arc; use quasi_http::Serves as _; use quasi_router::{Chrome, Outcome, Params, Request}; use super::super::router; use crate::state::{AppState, DESKTOP_USER_ID}; async fn state() -> Arc { let (state, _) = crate::test_utils::setup_test_state().await; let now = chrono::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 } /// Every place in the nav, both levels, as keys. fn keys(chrome: &Chrome) -> Vec<&str> { chrome .nav .iter() .flat_map(|place| { std::iter::once(place.key.as_str()) .chain(place.within.iter().map(|inner| inner.key.as_str())) }) .collect() } #[tokio::test] async fn the_nav_offers_the_tabs_the_shipped_header_offers() { // Transcribed from `navigation.js`'s TAB_GROUPS rather than invented. A nav // that offered a different set would be a second answer to what the app // contains. Search is the one place that is not in TAB_GROUPS: the shipped // header reached it from a box rather than a tab, and goingson `6b3aa22b` // chose a nav entry because it reaches every kind and the app can bind no // keys. let chrome = super::chrome(); let tabs: Vec<&str> = chrome .nav .iter() .map(|place| place.label.as_str()) .collect(); assert_eq!(tabs, ["Work", "Time", "Messages", "Search", "Settings"]); let work = &chrome.nav[0]; let inside: Vec<&str> = work.within.iter().map(|p| p.label.as_str()).collect(); assert_eq!(inside, ["Tasks", "Projects", "Problems"]); // TAB_DEFAULTS: pressing a tab opens the view it opens in the shipped app. assert_eq!(work.action, quasi_router::Action::get("/tasks")); } #[tokio::test] async fn the_graph_is_not_offered_because_nothing_serves_it() { // `task-graph` is in TAB_GROUPS and is bespoke: a hand-laid-out SVG that // stays JavaScript. A place pointing at a route that does not exist would // be a NotFound the first time it was pressed. let chrome = super::chrome(); assert!(!keys(&chrome).contains(&"task-graph")); assert!( !format!("{chrome:?}").contains("Graph"), "no label for it either" ); } #[tokio::test] async fn the_band_is_still_the_timer_modules_to_describe() { // The shell asks for the panel; it does not restate what the panel says. let chrome = super::chrome(); let panel = chrome .panel(crate::quasi::time_tracking::PANEL) .expect("the band is declared"); assert_eq!(panel.role, quasi_router::Role::Activity); } #[tokio::test] async fn a_screen_reached_from_a_row_marks_the_place_it_came_from() { // The drawer, the project dashboard and Import & Export are not tabs. Each // marks where a person got to it from, which is what TAB_GROUPS said for // the first two and what the settings sidebar says for the third. let state = state().await; let task = state .tasks .create( DESKTOP_USER_ID, goingson_core::NewTask::builder("Write the thing").build(), ) .unwrap(); for (path, expected) in [ (format!("/tasks/{}", task.id), super::TASKS), ("/data".to_owned(), super::SETTINGS), ("/board".to_owned(), super::TASKS), ] { let response = router() .handle(&state, Request::get(&path).carrying(Params::new())) .expect("the route answers"); let Outcome::Screen(screen) = &response.outcome else { panic!("{path} answers a screen"); }; assert_eq!(screen.place.as_deref(), Some(expected), "{path}"); } } #[tokio::test] async fn the_document_carries_the_nav_and_marks_where_it_is() { let state = state().await; let response = router() .handle(&state, Request::get("/problems").carrying(Params::new())) .expect("the route answers"); let Outcome::Screen(screen) = &response.outcome else { panic!("problems answers a screen"); }; let markup = quasi_webview::Webview::new() .with_shell(quasi_webview::Shell::default().with_chrome(super::chrome())) .screen(screen); assert!(markup.contains("data-chrome=\"nav\""), "{markup}"); assert!(markup.contains("Problems"), "{markup}"); // The place and the tab holding it: a header that lit the pill and not the // tab would be lying about the tab. assert_eq!( markup.matches("aria-current=\"page\"").count(), 2, "{markup}" ); } #[tokio::test] async fn every_screen_names_a_place_the_nav_actually_has() { // THE ONE THAT MATTERS. A key misspelled at either end is a tab that never // lights, and nothing else in this tree would see it: the screen still // renders, the nav still draws, and only the marking is quietly gone. // // Walked over the routes rather than over a list kept here, so a screen // added without a place is a failure rather than a line somebody forgot. let state = state().await; let chrome = super::chrome(); let known = keys(&chrome); let task = state .tasks .create( DESKTOP_USER_ID, goingson_core::NewTask::builder("Write the thing").build(), ) .unwrap(); let paths = [ "/tasks".to_owned(), "/board".to_owned(), format!("/tasks/{}", task.id), format!("/tasks/{}/edit", task.id), "/projects".to_owned(), "/problems".to_owned(), "/day".to_owned(), "/weekly-review".to_owned(), "/monthly-review".to_owned(), "/timer".to_owned(), "/events".to_owned(), "/emails".to_owned(), "/contacts".to_owned(), "/settings".to_owned(), "/data".to_owned(), ]; for path in paths { let response = router() .handle(&state, Request::get(&path).carrying(Params::new())) .expect("the route answers"); let Outcome::Screen(screen) = &response.outcome else { continue; }; let place = screen .place .as_deref() .unwrap_or_else(|| panic!("{path} names no place")); assert!( known.contains(&place), "{path} names `{place}`, which the nav does not have: {known:?}" ); } }