diff --git a/tests/code_cols.rs b/tests/code_cols.rsnew file mode 100644index 0000000..a21ebf3--- /dev/null+++ b/tests/code_cols.rs@@ -0,0 +1,160 @@1+//! Code tab two columns (m6, f0rge issue #45):2+//! files + README share the existing `.cols` grid, side by side only at3+//! >=1100px (files 7/12, README 5/12), stacked below.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::path::{Path, PathBuf};9+use std::process::Command;10+11+fn bin() -> &'static str {12+ env!("CARGO_BIN_EXE_hu13")13+}14+15+fn manifest() -> PathBuf {16+ PathBuf::from(env!("CARGO_MANIFEST_DIR"))17+}18+19+fn fresh(prefix: &str) -> PathBuf {20+ let dir = std::env::temp_dir().join(format!(21+ "hu13-codecols-{prefix}-{}-{}",22+ std::process::id(),23+ std::time::SystemTime::now()24+ .duration_since(std::time::UNIX_EPOCH)25+ .map(|d| d.as_nanos())26+ .unwrap_or(0)27+ ));28+ std::fs::create_dir_all(&dir).unwrap();29+ dir30+}31+32+fn git(repo: &Path, args: &[&str]) {33+ let st = Command::new("git")34+ .arg("-C")35+ .arg(repo)36+ .args(args)37+ .status()38+ .expect("spawn git");39+ assert!(st.success(), "git {args:?} failed");40+}41+42+fn fixture_repo() -> PathBuf {43+ let dir = fresh("repo");44+ git(&dir, &["init", "-b", "main"]);45+ git(&dir, &["config", "user.name", "hu13-test"]);46+ git(&dir, &["config", "user.email", "[email protected]"]);47+ git(&dir, &["config", "commit.gpgsign", "false"]);48+ std::fs::write(dir.join("README.md"), "# cols fixture\n\nhello readme\n").unwrap();49+ std::fs::write(dir.join("hello.txt"), "hello\n").unwrap();50+ git(&dir, &["add", "-A"]);51+ git(&dir, &["commit", "-m", "init"]);52+ dir53+}54+55+fn build_fixture() -> PathBuf {56+ let repo = fixture_repo();57+ let out = fresh("out");58+ let st = Command::new(bin())59+ .arg("build")60+ .arg(&repo)61+ .arg("-o")62+ .arg(&out)63+ .status()64+ .expect("spawn hu13 build");65+ assert!(st.success(), "fixture build exits 0");66+ out67+}68+69+fn css() -> String {70+ std::fs::read_to_string(manifest().join("static").join("style.css")).expect("read style.css")71+}72+73+#[test]74+fn code_tab_wraps_files_and_readme_in_cols_code() {75+ let out = build_fixture();76+ let index = std::fs::read_to_string(out.join("index.html")).expect("index");77+ // Wrapper reuses the existing .cols pattern with the code variant.78+ assert!(79+ index.contains("<div class=\"cols cols-code\">"),80+ "code tab wraps files + README in .cols.cols-code"81+ );82+ // Two sections in order: files (table) left, README right.83+ let files_pos = index84+ .find("<section class=\"code-files\">")85+ .expect("files section");86+ let readme_pos = index87+ .find("<section class=\"code-readme\">")88+ .expect("readme section");89+ assert!(90+ files_pos < readme_pos,91+ "files section comes before README section"92+ );93+ let files_block = &index[files_pos..readme_pos];94+ assert!(95+ files_block.contains("<h2>Files</h2>"),96+ "files section keeps its heading"97+ );98+ assert!(99+ files_block.contains("file-table"),100+ "file table lives in the files (left) column"101+ );102+ let readme_block = &index[readme_pos..];103+ assert!(104+ readme_block.contains("<h2>README</h2>"),105+ "README section keeps its heading"106+ );107+ assert!(108+ readme_block.contains("cols fixture"),109+ "README body renders in the right column"110+ );111+}112+113+#[test]114+fn code_cols_breakpoint_is_1100px_stacked_below() {115+ let css = css();116+ // Mobile-first base: .cols is a single stacked column.117+ let base = css.split("@media (min-width").next().expect("base css");118+ assert!(119+ base.contains(".cols{display:grid;grid-template-columns:minmax(0,1fr)"),120+ "base .cols stays stacked single-column"121+ );122+ // The code variant stays stacked through the 768px breakpoint123+ // (overrides the generic .cols side-by-side there).124+ assert!(125+ css.contains("@media (min-width:768px)"),126+ "768px breakpoint kept"127+ );128+ assert!(129+ css.contains("@media (min-width:1100px)"),130+ "1100px breakpoint present"131+ );132+ let stacked_rule = ".cols-code{grid-template-columns:minmax(0,1fr)}";133+ assert!(134+ css.contains(stacked_rule),135+ "cols-code stays stacked below 1100px, got no {stacked_rule:?}"136+ );137+ // Desktop: files 7/12 + README 5/12 inside the 1100px block.138+ let wide_rule = ".cols-code{grid-template-columns:minmax(0,7fr) minmax(0,5fr)}";139+ assert!(140+ css.contains(wide_rule),141+ "cols-code is 7/12 + 5/12 at >=1100px, got no {wide_rule:?}"142+ );143+ let block_1100 = css144+ .split("@media (min-width:1100px)")145+ .nth(1)146+ .expect("1100px block");147+ let first_chunk = block_1100.split("@media").next().expect("1100 chunk");148+ assert!(149+ first_chunk.contains(".cols-code"),150+ "7/5 rule lives in the 1100px block"151+ );152+ // Cascade order: the stacked 768px override comes before the 1100px153+ // two-column rule, so desktop wins and 768px–1099px stays stacked.154+ let stacked_pos = css.find(stacked_rule).expect("stacked rule pos");155+ let wide_pos = css.find(wide_rule).expect("wide rule pos");156+ assert!(157+ stacked_pos < wide_pos,158+ "stacked override precedes the 1100px two-column rule"159+ );160+}