//! The task overview, driven through the router against a real database. //! //! Same standard as the other two screens: no Tauri runtime and no window, the //! description asserted, and the markup only where the markup is the point. //! Every workaround this port had to take is asserted here rather than left to //! be noticed, so closing a finding is a test that has to change. use std::sync::Arc; use goingson_core::{NewTask, Priority}; use quasi_http::Serves as _; use quasi_router::Outcome; use quasi_router::{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 = 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 } fn add(state: &AppState, title: &str) -> goingson_core::Task { state .tasks .create( DESKTOP_USER_ID, NewTask::builder(title) .title(title) .priority(Priority::High) .build(), ) .unwrap() } fn get(state: &AppState, path: &str) -> Response { router() .handle(state, Request::get(path)) .expect("the route answers") } fn post(state: &AppState, path: &str, params: Params) -> Response { router() .handle(state, Request::post(path).sending(params)) .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), // Deliberately not a wildcard. See the same helper in the dashboard // tests: a redirect has no body, and a fallback returning empty markup // would read as a screen that rendered nothing. Outcome::Goto(action) => panic!("expected content, got a redirect to {action:?}"), // Same reasoning one step along: an overlay is a screen, but it is not // the screen this route was asked for. Rendering it here would let a // route that answered with the command palette pass as the page. 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}`") } } } #[tokio::test] async fn the_overview_carries_the_task_and_its_badges() { let state = state().await; let task = add(&state, "Write the port"); let page = html(get(&state, &format!("/tasks/{}", task.id))); assert!(page.contains("Write the port")); // Status and priority are tokens, so they keep their tone. `H` is the short // form `Priority::as_str` gives, which is what the JS chip shows too. assert!(page.contains("Pending")); assert!(page.contains(">H<")); assert!(page.contains("data-tone=\"danger\"")); } #[tokio::test] async fn a_missing_task_is_a_not_found_rather_than_a_panic() { let state = state().await; let error = router() .handle( &state, Request::get(format!("/tasks/{}", uuid::Uuid::nil())), ) .expect_err("no such task"); assert_eq!(error.class.http_status(), 404); } #[tokio::test] async fn a_task_title_cannot_become_markup() { let state = state().await; let task = add(&state, ""); let page = html(get(&state, &format!("/tasks/{}", task.id))); assert!(!page.contains("") .build(), ) .unwrap(); let page = html(get(&state, &format!("/tasks/{}", task.id))); assert!(page.contains("bold")); // The member carries source, so what a user typed is still the renderer's // to decide about. This is the same guarantee `Node::Text` gives, kept. // The body of the script rather than the tag: the document shell carries // htmx's own `"); let blocked = add(&state, "Ordinary"); state .tasks .add_dependency(DESKTOP_USER_ID, blocked.id, blocker.id) .unwrap(); let page = html(get(&state, &format!("/tasks/{}", blocked.id))); assert!(!page.contains("