tests/navbar.rs 6.3 KiB 193 lines Raw
1 //! Navbar one-bar (m6, f0rge issues #18 + #27 + #28):2 //! single `<header>` (logo-with-text • nav only), scrollable nav strip3 //! (own `nav-scroll` class: `overflow-x:auto` + nowrap, snap off, visible4 //! focus), logo img alt text, `aria-current` on the active tab.5 //!6 //! Builds a small fixture repo with the real binary and string-asserts on7 //! the emitted HTML + `static/style.css` (no rendering engine needed).8 9 use std::sync::atomic::{AtomicU64, Ordering};10 11 static SEQ: AtomicU64 = AtomicU64::new(0);12 13 use std::path::{Path, PathBuf};14 use std::process::Command;15 16 fn bin() -> &'static str {17     env!("CARGO_BIN_EXE_hu13")18 }19 20 fn manifest() -> PathBuf {21     PathBuf::from(env!("CARGO_MANIFEST_DIR"))22 }23 24 fn fresh(prefix: &str) -> PathBuf {25     let dir = std::env::temp_dir().join(format!(26         "hu13-navbar-{prefix}-{}-{}",27         std::process::id(),28         std::time::SystemTime::now()29             .duration_since(std::time::UNIX_EPOCH)30             .map(|d| d.as_nanos())31             .unwrap_or(0)32             .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))33     ));34     std::fs::create_dir_all(&dir).unwrap();35     dir36 }37 38 fn git(repo: &Path, args: &[&str]) {39     let st = Command::new("git")40         .arg("-C")41         .arg(repo)42         .args(args)43         .status()44         .expect("spawn git");45     assert!(st.success(), "git {args:?} failed");46 }47 48 fn fixture_repo() -> PathBuf {49     let dir = fresh("repo");50     git(&dir, &["init", "-b", "main"]);51     git(&dir, &["config", "user.name", "hu13-test"]);52     git(&dir, &["config", "user.email", "[email protected]"]);53     git(&dir, &["config", "commit.gpgsign", "false"]);54     std::fs::write(dir.join("README.md"), "# navbar fixture\n\nhello\n").unwrap();55     std::fs::write(dir.join("hello.txt"), "hello\n").unwrap();56     git(&dir, &["add", "-A"]);57     git(&dir, &["commit", "-m", "init"]);58     dir59 }60 61 fn build_fixture() -> PathBuf {62     let repo = fixture_repo();63     let out = fresh("out");64     let st = Command::new(bin())65         .arg("build")66         .arg(&repo)67         .arg("-o")68         .arg(&out)69         .status()70         .expect("spawn hu13 build");71     assert!(st.success(), "fixture build exits 0");72     out73 }74 75 fn css() -> String {76     std::fs::read_to_string(manifest().join("static").join("style.css")).expect("read style.css")77 }78 79 #[test]80 fn one_header_bar_with_logo_sep_and_scroll_nav() {81     let out = build_fixture();82     for page in ["index.html", "commits.html", "issues/index.html"] {83         let html = std::fs::read_to_string(out.join(page)).expect(page);84         // One header bar on every page; the nav lives inside it.85         assert_eq!(86             html.matches("<header").count(),87             1,88             "{page} has a single <header>"89         );90         assert!(91             html.contains("<nav"),92             "{page} keeps a <nav> inside the header"93         );94         // Minimal lockup (#27 + #28): logo-with-text art • nav only.95         assert!(96             html.contains("<img class=\"site-logo\" src=\"/logo.svg\""),97             "{page} shows the logo lockup img"98         );99         assert!(100             html.contains("alt=\"hu13 logo\""),101             "{page} logo carries alt text"102         );103         assert!(104             !html.contains("hu13-word"),105             "{page} drops the hu13 word node (letters live in logo.svg)"106         );107         assert!(108             !html.contains("<span class=\"brand\">"),109             "{page} drops the repo brand node"110         );111         let header = html.split("</header>").next().expect("header close");112         assert_eq!(113             header.matches("•").count(),114             1,115             "{page} header is logo • nav (exactly one sep)"116         );117         // Scrollable nav strip under its own class.118         assert!(119             html.contains("nav-scroll"),120             "{page} nav has the scroll class"121         );122     }123     // Repo name still lives outside the header (#28): title, og:title,124     // code-page repo-head, and feed title.125     let index = std::fs::read_to_string(out.join("index.html")).expect("index");126     assert!(index.contains("<title>"), "title kept");127     assert!(128         index.contains("<meta property=\"og:title\""),129         "og:title kept"130     );131     assert!(132         index.contains("<div class=\"repo-head\"><h1>"),133         "code-page repo-head title kept"134     );135     let feed = std::fs::read_to_string(out.join("feed.atom")).expect("feed");136     assert!(feed.contains("<title>"), "feed title kept");137 }138 139 #[test]140 fn active_tab_marked_with_aria_current() {141     let out = build_fixture();142     let index = std::fs::read_to_string(out.join("index.html")).expect("index");143     assert!(144         index.contains("<a href=\"/\" aria-current=\"page\">"),145         "code tab active on index"146     );147     let commits = std::fs::read_to_string(out.join("commits.html")).expect("commits");148     assert!(149         commits.contains("<a href=\"/commits.html\" aria-current=\"page\">"),150         "commits tab active on commits.html"151     );152     let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");153     assert!(154         issues.contains("<a href=\"/issues/index.html\" aria-current=\"page\">"),155         "issues tab active on issues/index.html"156     );157     // Exactly one active tab per page.158     for (name, html) in [159         ("index", &index),160         ("commits", &commits),161         ("issues", &issues),162     ] {163         assert_eq!(164             html.matches("aria-current=\"page\"").count(),165             1,166             "{name} marks exactly one active tab"167         );168     }169 }170 171 #[test]172 fn nav_scroll_css_scrolls_without_snap_and_shows_state() {173     let css = css();174     // Own class (appended, earlier rules untouched).175     assert!(css.contains(".nav-scroll"), "scroll class styled");176     // Scrollable strip: horizontal scroll + no wrapping.177     assert!(css.contains("overflow-x:auto"), "nav scrolls horizontally");178     assert!(css.contains("white-space:nowrap"), "nav links never wrap");179     // Scroll snap explicitly off (the strip is a plain scroller).180     assert!(181         css.contains("scroll-snap-type:none"),182         "snap off so the strip rests anywhere"183     );184     // Position stays visible: focus ring + active accent underline.185     assert!(186         css.contains(":focus-visible"),187         "keyboard focus stays visible"188     );189     assert!(190         css.contains("aria-current"),191         "active tab styled via aria-current"192     );193 }