//! Portals (part 8d), end to end: what a portal shows, going through one //! and back, `gu` up to the portals that lead here, `:portal`, backlinks //! kept in step, and file names that follow titles. use super::*; impl Harness { /// The status bar's first line. fn status_line(&self) -> String { let rows = self.rows(); rows[rows.len() - 2].clone() } /// The row of the screen where `text` is. fn row_of(&self, text: &str) -> String { let (_, row) = self.find(text); self.rows()[usize::from(row)].clone() } } /// The garden on its own, with the kitchen's file renamed to `pantry.cum` /// behind its portal's back. fn garden_with_the_kitchen_moved() -> Harness { let folder = TempDir::new(); std::fs::write(folder.path().join("garden.cum"), GARDEN).unwrap(); std::fs::write(folder.path().join("pantry.cum"), KITCHEN).unwrap(); Harness::open(folder, &["garden.cum"]) } #[test] fn a_portal_shows_its_document_live() { let mut h = Harness::new(); let (col, row) = h.find("» Kitchen"); // The kitchen's color (its root's), and how many nodes it has. assert_eq!(h.color_at((col - 2, row)), ratatui::style::Color::Indexed(178), "yellow, as Kitchen is"); assert!(h.rows()[usize::from(row) + 1].contains(" 5 "), "{}", h.screen()); // A node added in the kitchen counts at once. h.type_keys("gtaPansgT"); let (_, row) = h.find("» Kitchen"); assert!(h.rows()[usize::from(row) + 1].contains(" 6 "), "{}", h.screen()); // So does a new title. h.type_keys("gt:rename CookinggT"); assert!(h.screen().contains("» Cooking"), "{}", h.screen()); } #[test] fn enter_goes_through_a_portal_and_ctrl_o_comes_back() { let mut h = Harness::new(); h.type_keys("glgjgjgj"); assert_eq!(h.cursor(), "Kitchen"); h.type_keys(""); assert_eq!(h.doc().title().as_str(), "Kitchen"); assert_eq!(h.cursor(), "Kitchen", "on its root"); assert!(h.status_line().contains("Garden plans » Kitchen"), "the way here: {}", h.status_line()); h.type_keys("gl"); assert!(h.status_line().contains("Garden plans » Kitchen › Herbs on the sill"), "{}", h.status_line()); // Ctrl-o: back to the portal, and the breadcrumb is the garden's again. h.type_keys(""); assert_eq!((h.doc().title().as_str(), h.cursor().as_str()), ("Garden plans", "Kitchen")); assert!(h.status_line().contains(" Garden plans › Kitchen"), "{}", h.status_line()); h.type_keys("gf"); assert_eq!(h.doc().title().as_str(), "Kitchen", "gf goes through too"); // The Note view says where a portal leads. h.type_keys("vn"); assert!(h.screen().contains("a portal to kitchen.cum · 5 nodes · Enter goes there"), "{}", h.screen()); } #[test] fn gu_goes_up_to_the_portals_that_lead_here() { let mut h = Harness::new(); h.type_keys("gt"); assert_eq!(h.doc().title().as_str(), "Kitchen"); // The root counts the portals that lead to its document. assert!(h.row_of("Kitchen").contains("Kitchen") && h.screen().contains("« 1"), "{}", h.screen()); h.type_keys("glgu"); assert_eq!((h.doc().title().as_str(), h.cursor().as_str()), ("Garden plans", "Kitchen"), "on the portal"); // :backlinks lists them, even just one. h.type_keys(":backlinks"); assert!(h.screen().contains("portals that lead here"), "{}", h.screen()); assert!(h.screen().contains("« Garden plans garden.cum"), "{}", h.screen()); h.type_keys(""); assert_eq!(h.cursor(), "Kitchen"); // In the Note view, the root lists them as parts (after the title, the // tags, the body and two children): Enter goes there. h.type_keys("gtggvn"); assert!(h.screen().contains("Portals here"), "{}", h.screen()); h.type_keys("jjjjj"); assert_eq!((h.doc().title().as_str(), h.cursor().as_str()), ("Garden plans", "Kitchen")); // A document no portal leads to says so. h.type_keys(":new Shopping listgu"); assert_eq!(h.message(), "no portal leads here yet: :portal in another document makes one"); } #[test] fn portal_makes_a_new_document_or_finds_one() { let mut h = Harness::new(); h.type_keys("p"); assert_eq!(h.label(), "COMMAND"); assert!(h.rows().last().unwrap().starts_with(":portal "), "{}", h.screen()); h.type_keys("Compost bins"); assert_eq!(h.message(), "made compost-bins.cum, with a portal to it: Enter goes there"); assert_eq!(h.cursor(), "Compost bins"); assert_eq!(h.parent("Compost bins"), "Garden plans", "under the cursor's node, where a adds one"); assert!(h.file("compost-bins.cum").contains("title: \"Compost bins\"")); // Saved, the portal puts a backlink in its document. h.wait(1); let made = h.file("compost-bins.cum"); assert!(made.contains("from: \"garden.cum\""), "{made}"); // One that's there already, by its file's name or its title. h.type_keys("gg:portal kitchen"); assert_eq!(h.message(), "a portal to kitchen.cum: Enter goes there"); h.type_keys("gg:portal Compost bins"); assert_eq!(h.message(), "a portal to compost-bins.cum: Enter goes there"); h.type_keys("gg:portal garden"); assert_eq!(h.message(), "a portal can't lead to its own document"); } #[test] fn backlinks_follow_the_portals_as_they_come_and_go() { let mut h = Harness::new(); // The kitchen portal goes: once the garden is saved, the kitchen // forgets it. h.type_keys("glgjgjgjdd"); h.wait(1); assert!(!h.file("kitchen.cum").contains("backlinks"), "{}", h.file("kitchen.cum")); h.type_keys("gt"); assert!(!h.screen().contains("« 1"), "{}", h.screen()); // Back again: so is the backlink, with the portal's node. h.type_keys("gTu"); h.wait(1); assert!(h.file("kitchen.cum").contains("node: 10"), "{}", h.file("kitchen.cum")); } #[test] fn a_new_title_renames_the_file_and_its_portals_follow() { let mut h = Harness::new(); h.type_keys("gt:rename Cooking"); h.wait(1); assert_eq!(h.message(), "kitchen.cum is cooking.cum now: a file's name follows its title"); assert!(!h.path("kitchen.cum").exists()); assert!(h.file("cooking.cum").contains("title: \"Cooking\"")); let garden = h.file("garden.cum"); assert!(garden.contains("path: \"cooking.cum\"") && garden.contains("title: \"Cooking\""), "{garden}"); // The portal still goes there. h.type_keys("gTglgjgjgj"); assert_eq!(h.doc().title().as_str(), "Cooking"); // A name that's taken gets a number. h.type_keys("gT:rename Cooking"); h.wait(1); assert_eq!(h.message(), "garden.cum is cooking-2.cum now: a file's name follows its title"); assert!(h.file("cooking.cum").contains("from: \"cooking-2.cum\""), "the backlink follows it too"); // A folder's default document keeps its name. h.type_keys(":new:rename Everything"); h.wait(1); assert!(h.path("ideas.cum").exists() && !h.path("everything.cum").exists()); } #[test] fn a_portal_finds_its_document_moved_by_hand() { let mut h = garden_with_the_kitchen_moved(); let (_, row) = h.find("» Kitchen"); assert!(h.rows()[usize::from(row) + 1].contains(" ? "), "not where it says: {}", h.screen()); h.type_keys("glgjgjgj"); assert_eq!(h.doc().title().as_str(), "Kitchen", "found in its folder by its uid"); assert!(h.file("garden.cum").contains("path: \"pantry.cum\""), "and the portal keeps that path"); // Gone for good: Enter says so. let mut h = garden_with_the_kitchen_moved(); std::fs::remove_file(h.path("pantry.cum")).unwrap(); h.type_keys("glgjgjgj"); assert_eq!(h.message(), "kitchen.cum isn't there anymore"); assert_eq!(h.doc().title().as_str(), "Garden plans"); } #[test] fn a_portal_pasted_into_another_folder_still_leads_there() { let mut h = Harness::new(); h.type_keys("glgjgjgjyy:e rooms/attic.cump"); let path = h.doc().portals().next().map(|(_, portal)| portal.path.as_str().to_string()); assert_eq!(path.as_deref(), Some("../kitchen.cum"), "from the attic's own folder"); h.type_keys(""); assert_eq!(h.doc().title().as_str(), "Kitchen"); } #[test] fn w_says_when_the_file_takes_a_new_name() { let mut h = Harness::new(); h.type_keys("gt:rename Pantry:w"); assert_eq!( h.message(), "saved pantry.cum · kitchen.cum is pantry.cum now: a file's name follows its title" ); }