//! Settings, driven through the router against a real database. //! //! Same standard as the screens before it: 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 quasi_http::Serves as _; use quasi_router::Outcome; use quasi_router::{Params, Request, Response}; use super::super::router; use crate::state::AppState; async fn state() -> Arc { let (state, _) = crate::test_utils::setup_test_state().await; state } 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), 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}`") } } } /// What the settings key holds right now, read the way the command does. fn stored(state: &AppState, key: &str) -> Option { crate::commands::all_config(state) .unwrap() .get(key) .cloned() } #[tokio::test] async fn settings_opens_on_appearance_the_way_the_js_does() { let state = state().await; let page = html(get(&state, "/settings")); assert!(page.contains("Appearance")); assert!(page.contains("Theme")); // And the same screen is reachable under its own address. assert_eq!(page, html(get(&state, "/settings/appearance"))); } #[tokio::test] async fn the_sidebar_offers_the_described_sections_and_points_at_the_open_one() { let state = state().await; let page = html(get(&state, "/settings/planning")); assert!(page.contains("Appearance")); assert!(page.contains("Notifications")); assert!(page.contains("Planning & Review")); // The app's own pointer, not the user's tick. assert!(page.contains("aria-current")); // About joined them on 2026-08-22, once `AppState` held the two host facts // it needed. assert!(page.contains("About")); // Sync joined them on 2026-08-22, once somebody measured `commands/sync.rs` // per command rather than per file: its reads are local. assert!(page.contains("Sync")); // Sharing joined them on 2026-08-24, and it is the only one of the five // whose stated reason was right: its reads were remote, so there was no // local state to draw a section from at all. synckit 0.9.0 writes the group // directory down, so the reads are local now. quasicoherent `82273265`. assert!(page.contains("Sharing")); // Which leaves none of the eight absent, and the header's single-cause // explanation wrong about four of its five. assert_eq!(super::SECTIONS.len(), 8); } #[tokio::test] async fn import_and_export_is_a_sidebar_row_that_leaves_for_the_screen_it_lives_on() { // The section is described in full and lives at `/data`, so the row goes // there rather than swapping this screen's pane. See `Section::at`. let state = state().await; let page = html(get(&state, "/settings/planning")); assert!(page.contains("Import & Export"), "{page}"); assert!(page.contains("/data"), "{page}"); } #[tokio::test] async fn the_row_that_leaves_has_no_section_of_its_own_to_open() { // Without this, `/settings/data` falls through `screen`'s match and draws // Appearance under the Import & Export heading. let state = state().await; let error = router() .handle(&state, Request::get("/settings/data")) .expect_err("data is a screen, not a section here"); assert_eq!(error.class.http_status(), 404); // And it never reads as the open one, on any section. let page = html(get(&state, "/settings/planning")); let at = page.find("Import & Export").expect("the row is drawn"); let row = &page[page[..at].rfind('<').unwrap_or(0)..at]; assert!(!row.contains("aria-current"), "{row}"); } #[tokio::test] async fn a_section_that_is_not_described_is_a_not_found() { // Every section in `SECTIONS` is described now, so the case wants a name // that never will be rather than the last one to be built. It held // `/settings/sharing` until 2026-08-24. let state = state().await; let error = router() .handle(&state, Request::get("/settings/nonesuch")) .expect_err("an unknown section is not a section"); assert_eq!(error.class.http_status(), 404); } #[tokio::test] async fn a_control_writes_as_soon_as_it_changes_with_no_form_around_it() { // What finding 14612ed8 was closed for, and this screen is its first // consumer: Node::Field plus Field::writes, which is a bare control with a // route on it. A Node::Form here would describe a submit that does not // exist. let state = state().await; let page = html(get(&state, "/settings/notifications")); assert!(page.contains("Event indicator lead time")); assert!(page.contains("/settings/config/event_lead_minutes")); // No submit, because there is no form. assert!(!page.contains("type=\"submit\"")); } #[tokio::test] async fn every_control_on_the_screen_writes_through_the_one_route() { let state = state().await; for (key, value, section) in [ ("event_lead_minutes", "30", "notifications"), ("work_start_hour", "7", "planning"), ("work_end_hour", "19", "planning"), ("plan_nudges", "disabled", "planning"), ("review_nudges", "disabled", "planning"), ] { let page = html(post( &state, &format!("/settings/config/{key}"), Params::new().with("value", value), )); assert_eq!(stored(&state, key).as_deref(), Some(value)); // And the write answers with the section the setting lives in, re-read, // which the handler works out from the key rather than being told. assert!( page.contains(&format!("/settings/{section}")), "{key} should answer in {section}" ); } } #[tokio::test] async fn the_notifications_section_is_generated_from_the_registry() { // Adoption's proof: the controls come from `crate::notifs::NOTIFS` rather // than from a list in the screen, so a kind added there appears here with // nothing edited in `settings.rs`. let state = state().await; let page = html(get(&state, "/settings/notifications")); for kind in crate::notifs::NOTIFS.kinds() { assert!(page.contains(kind.title), "{} is missing", kind.id); assert!( page.contains(&quasi_notifs::config::enabled_key(kind.id)), "{} does not carry its generated key", kind.id ); } // The whole pane posts to one route, and every control names its own key. assert!(page.contains("/settings/notifications")); } #[tokio::test] async fn a_kind_that_ships_on_is_drawn_on_before_anyone_has_chosen() { // The migration fact: adopting the framework must not silently stop a // notification that fires today. A checkbox is on by presence. let state = state().await; let page = html(get(&state, "/settings/notifications")); let checkbox = page .split("to<")); } #[tokio::test] async fn a_theme_name_cannot_become_markup() { // Theme names come off disk, which is a place the app does not control: // importing a theme writes whatever the file says into the custom // directory, and the name inside it is never seen by anything that // validates. The renderer is what makes that safe, and this is the only // untrusted string on the screen. let dir = tempfile::tempdir().unwrap(); std::fs::write( dir.path().join("hostile.toml"), "[meta]\nname = \"\"\n", ) .unwrap(); let (mut state, _) = crate::test_utils::setup_test_state().await; Arc::get_mut(&mut state).expect("sole owner").theme_dirs = vec![(dir.path().to_path_buf(), true)]; let page = html(get(&state, "/settings/appearance")); assert!(page.contains("<script>")); assert!(!page.contains("