1 //! Code tab stack (f0rge issue #120):2 //! files and README share one stacked `.cols` column at every width, in3 //! DOM order Files then README. No desktop sidebar split.4 //!5 //! Builds a small fixture repo with the real binary and string-asserts on6 //! the emitted HTML + `static/style.css` (no rendering engine needed).7 8 use std::sync::atomic::{AtomicU64, Ordering};9 10 static SEQ: AtomicU64 = AtomicU64::new(0);11 12 use std::path::{Path, PathBuf};13 use std::process::Command;14 15 fn bin() -> &'static str {16 env!("CARGO_BIN_EXE_hu13")17 }18 19 fn manifest() -> PathBuf {20 PathBuf::from(env!("CARGO_MANIFEST_DIR"))21 }22 23 fn fresh(prefix: &str) -> PathBuf {24 let dir = std::env::temp_dir().join(format!(25 "hu13-codecols-{prefix}-{}-{}",26 std::process::id(),27 std::time::SystemTime::now()28 .duration_since(std::time::UNIX_EPOCH)29 .map(|d| d.as_nanos())30 .unwrap_or(0)31 .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))32 ));33 std::fs::create_dir_all(&dir).unwrap();34 dir35 }36 37 fn git(repo: &Path, args: &[&str]) {38 let st = Command::new("git")39 .arg("-C")40 .arg(repo)41 .args(args)42 .status()43 .expect("spawn git");44 assert!(st.success(), "git {args:?} failed");45 }46 47 fn fixture_repo() -> PathBuf {48 let dir = fresh("repo");49 git(&dir, &["init", "-b", "main"]);50 git(&dir, &["config", "user.name", "hu13-test"]);51 git(&dir, &["config", "user.email", "[email protected]"]);52 git(&dir, &["config", "commit.gpgsign", "false"]);53 std::fs::write(dir.join("README.md"), "# cols fixture\n\nhello readme\n").unwrap();54 std::fs::write(dir.join("hello.txt"), "hello\n").unwrap();55 git(&dir, &["add", "-A"]);56 git(&dir, &["commit", "-m", "init"]);57 dir58 }59 60 fn build_fixture() -> PathBuf {61 let repo = fixture_repo();62 let out = fresh("out");63 let st = Command::new(bin())64 .arg("build")65 .arg(&repo)66 .arg("-o")67 .arg(&out)68 .status()69 .expect("spawn hu13 build");70 assert!(st.success(), "fixture build exits 0");71 out72 }73 74 fn css() -> String {75 std::fs::read_to_string(manifest().join("static").join("style.css")).expect("read style.css")76 }77 78 #[test]79 fn code_tab_wraps_files_and_readme_in_stacked_column() {80 let out = build_fixture();81 let index = std::fs::read_to_string(out.join("index.html")).expect("index");82 // Wrapper is one plain stacked grid; no code-specific column variant.83 assert!(84 index.contains("<div class=\"cols\">"),85 "code tab wraps files + README in .cols"86 );87 assert!(88 !index.contains("cols-code"),89 "no dead side-by-side class in the markup"90 );91 // Two sections in order: files first, then README.92 let files_pos = index93 .find("<section class=\"code-files\">")94 .expect("files section");95 let readme_pos = index96 .find("<section class=\"code-readme\">")97 .expect("readme section");98 assert!(99 files_pos < readme_pos,100 "files section comes before README section"101 );102 let files_block = &index[files_pos..readme_pos];103 assert!(104 files_block.contains("<h2>Files</h2>"),105 "files section keeps its heading"106 );107 assert!(108 files_block.contains("file-table"),109 "file table lives in the files section"110 );111 let readme_block = &index[readme_pos..];112 assert!(113 readme_block.contains("<h2>README</h2>"),114 "README section keeps its heading"115 );116 assert!(117 readme_block.contains("cols fixture"),118 "README body renders after the file table"119 );120 }121 122 #[test]123 fn code_tab_is_always_stacked() {124 let css = css();125 // Mobile-first base: .cols is a single stacked column.126 let base = css.split("@media (min-width").next().expect("base css");127 assert!(128 base.contains(".cols{display:grid;grid-template-columns:minmax(0,1fr)"),129 "base .cols stays stacked single-column"130 );131 // No code-specific columns and no wide-screen split anywhere.132 assert!(!css.contains(".cols-code"), "no code column variant");133 assert!(134 !css.contains("minmax(0,1.25fr)"),135 "generic .cols no longer goes two-column"136 );137 assert!(138 !css.contains("minmax(0,7fr)") && !css.contains("minmax(0,5fr)"),139 "no desktop 7/5 files+README split"140 );141 assert!(142 css.contains("@media (min-width:1100px)") && css.contains(".wrap{max-width:72rem"),143 "wide layout keeps the readable content width"144 );145 }146 147 #[test]148 fn code_page_uses_one_vertical_rhythm() {149 let css = css();150 // f0rge #122: explicit code-page rhythm, not browser heading margins.151 // 32px between major blocks, 12px heading -> content, 8px bar -> legend.152 for rule in [153 ".cols{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s6)",154 ".repo-head{margin-top:var(--s6);margin-bottom:var(--s6);gap:var(--s3)}",155 ".repo-head h1{margin:0}",156 ".repo-head .clonebox{margin:0}",157 ".code-files>h2,.code-readme>h2{margin:0 0 var(--s3)}",158 ".code-files>.table-scroll{margin:0}",159 ".langs{margin:var(--s6) 0 0}",160 ".langs>h2{margin:0 0 var(--s3)}",161 ".lang-bar{margin:0}",162 ".lang-legend{margin:var(--s2) 0 0}",163 ".code-readme .markdown>:first-child{margin-top:0}",164 ] {165 assert!(css.contains(rule), "code-page rhythm rule missing: {rule}");166 }167 }