use std::fs;
use std::path::PathBuf;
use std::process::Command;
#[test]
fn builds_fixture_site() {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let workspace = manifest.parent().unwrap().parent().unwrap(); let fixture = workspace.join("fixtures/site-basic");
let tmp = std::env::temp_dir().join(format!("docgen_build_cli_test_{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(tmp.join("docs/guide")).unwrap();
fs::copy(fixture.join("docs/index.md"), tmp.join("docs/index.md")).unwrap();
fs::copy(
fixture.join("docs/guide/intro.md"),
tmp.join("docs/guide/intro.md"),
)
.unwrap();
fs::copy(fixture.join("docs/markup.md"), tmp.join("docs/markup.md")).unwrap();
let status = Command::new(env!("CARGO_BIN_EXE_docgen"))
.arg("build")
.arg(&tmp)
.status()
.unwrap();
assert!(status.success());
let home = fs::read_to_string(tmp.join("dist/index/index.html")).unwrap();
assert!(home.contains("<title>Home</title>"));
assert!(home.contains("<strong>basic</strong>"));
assert!(home.contains(r#"href="/guide/intro""#));
assert!(home.contains(">guide<"));
let intro = fs::read_to_string(tmp.join("dist/guide/intro/index.html")).unwrap();
assert!(intro.contains("<title>Introduction</title>"));
assert!(intro.contains(r#"href="/index""#));
assert!(intro.contains(r#"href="/guide/intro""#));
assert!(intro.contains(">guide<"));
assert!(home.contains(
r#"<a class="docgen-wikilink" href="/guide/intro" data-wikilink-title="Intro guide" data-wikilink-path="/guide/intro">Intro guide</a>"#
));
assert!(intro.contains(r#"href="/index""#));
assert!(intro.contains("docgen-wikilink--broken"));
assert!(intro.contains(r#"<pre class="docgen-code">"#));
assert!(home.contains("Referenced by"));
assert!(home.contains(r#"class="docgen-rail__backlink" href="/guide/intro""#));
assert!(!home.contains("docgen-backlinks"));
let idx = fs::read_to_string(tmp.join("dist/search-index.json")).unwrap();
assert!(idx.contains(r#""slug":"index""#));
assert!(idx.contains(r#""slug":"guide/intro""#));
assert!(idx.contains(r#""title":"Home""#));
assert!(!idx.contains("[["));
let entries: serde_json::Value = serde_json::from_str(&idx).unwrap();
let markup = entries
.as_array()
.unwrap()
.iter()
.find(|e| e["slug"] == "markup")
.expect("markup entry present");
let text = markup["text"].as_str().unwrap();
assert!(text.contains("Compare a < b"), "got: {text}");
assert!(text.contains("inline"));
assert!(!text.contains("<em>"));
assert!(!text.contains("</em>"));
assert!(text.contains("missing-page"));
assert!(!text.contains("[["));
let js = fs::read_to_string(tmp.join("dist/search.js")).unwrap();
assert!(js.contains("search-index.json"));
assert!(tmp.join("dist/docgen.css").exists());
assert!(tmp.join("dist/bootstrap.js").is_file());
assert!(tmp.join("dist/vendor/alpine/alpine.min.js").is_file());
assert!(home.contains("data-docgen-search"));
assert!(home.contains(r#"src="/search.js""#));
assert!(home.contains(r#"src="/bootstrap.js""#));
assert!(home.contains(r#"src="/vendor/alpine/alpine.min.js""#));
assert!(!tmp.join("dist/vendor/mermaid/mermaid.min.js").exists());
assert!(!tmp.join("dist/islands/mermaid.js").exists());
assert!(!home.contains("islands/mermaid.js"));
assert!(tmp.join("dist/graph/index.html").is_file());
assert!(tmp.join("dist/islands/graph.js").is_file());
assert!(home.contains("docgen-home-graph"));
assert!(home.contains(r#"id="docgen-graph-data""#));
assert!(home.contains(r#"src="/islands/graph.js""#));
assert!(!home.contains("docgen-sidebar__graph"));
let intro = fs::read_to_string(tmp.join("dist/guide/intro/index.html")).unwrap();
assert!(!intro.contains("docgen-home-graph"));
assert!(!intro.contains(r#"src="/islands/graph.js""#));
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn builds_graph_page_with_island_and_nav_link() {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace = manifest.parent().unwrap().parent().unwrap();
let fixture = workspace.join("fixtures/site-basic");
let tmp = std::env::temp_dir().join(format!(
"docgen_build_cli_graph_test_{}",
std::process::id()
));
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(tmp.join("docs/guide")).unwrap();
fs::copy(fixture.join("docs/index.md"), tmp.join("docs/index.md")).unwrap();
fs::copy(
fixture.join("docs/guide/intro.md"),
tmp.join("docs/guide/intro.md"),
)
.unwrap();
let status = Command::new(env!("CARGO_BIN_EXE_docgen"))
.arg("build")
.arg(&tmp)
.status()
.unwrap();
assert!(status.success());
let graph = fs::read_to_string(tmp.join("dist/graph/index.html")).unwrap();
assert!(graph.contains("<title>Graph</title>"));
assert!(graph.contains(r#"x-data="docgenGraph""#));
assert!(graph.contains(r#"id="docgen-graph-data""#));
assert!(graph.contains(r#"src="/islands/graph.js""#));
let start = graph.find("docgen-graph-data").unwrap();
let open = graph[start..].find('>').unwrap() + start + 1;
let close = graph[open..].find("</script>").unwrap() + open;
let json = &graph[open..close];
let data: serde_json::Value = serde_json::from_str(json).unwrap();
let nodes = data["nodes"].as_array().unwrap();
assert_eq!(nodes.len(), 2);
assert!(nodes.iter().any(|n| n["slug"] == "index"));
assert!(nodes.iter().any(|n| n["slug"] == "guide/intro"));
let edges = data["edges"].as_array().unwrap();
let links_index_intro = |e: &serde_json::Value| {
(e["from"] == "index" && e["to"] == "guide/intro")
|| (e["from"] == "guide/intro" && e["to"] == "index")
};
assert_eq!(edges.iter().filter(|e| links_index_intro(e)).count(), 1);
for n in nodes {
let (x, y) = (n["x"].as_f64().unwrap(), n["y"].as_f64().unwrap());
assert!(x.is_finite() && y.is_finite());
assert!((74.0..=1420.0 - 74.0).contains(&x));
assert!((74.0..=760.0 - 74.0).contains(&y));
}
assert!(tmp.join("dist/islands/graph.js").is_file());
let island = fs::read_to_string(tmp.join("dist/islands/graph.js")).unwrap();
assert!(!island.to_lowercase().contains("d3"));
let home = fs::read_to_string(tmp.join("dist/index/index.html")).unwrap();
assert!(home.contains(r#"id="docgen-graph-data""#));
assert!(home.contains(r#"src="/islands/graph.js""#));
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn builds_mermaid_page_with_lazy_island() {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace = manifest.parent().unwrap().parent().unwrap();
let fixture = workspace.join("fixtures/site-basic");
let tmp = std::env::temp_dir().join(format!(
"docgen_build_cli_mermaid_test_{}",
std::process::id()
));
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(tmp.join("docs")).unwrap();
fs::copy(fixture.join("docs/diagram.md"), tmp.join("docs/diagram.md")).unwrap();
fs::copy(fixture.join("docs/index.md"), tmp.join("docs/index.md")).unwrap();
let status = Command::new(env!("CARGO_BIN_EXE_docgen"))
.arg("build")
.arg(&tmp)
.status()
.unwrap();
assert!(status.success());
let diag = fs::read_to_string(tmp.join("dist/diagram/index.html")).unwrap();
assert!(diag.contains("docgen-mermaid"), "no container: {diag}");
assert!(diag.contains(r#"x-data="docgenMermaid""#));
assert!(diag.contains("graph TD")); assert!(diag.contains(r#"src="/islands/mermaid.js""#));
assert!(tmp.join("dist/vendor/mermaid/mermaid.min.js").is_file());
assert!(tmp.join("dist/islands/mermaid.js").is_file());
let home = fs::read_to_string(tmp.join("dist/index/index.html")).unwrap();
assert!(!home.contains("islands/mermaid.js"));
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn builds_math_page_with_build_time_katex() {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace = manifest.parent().unwrap().parent().unwrap();
let fixture = workspace.join("fixtures/site-basic");
let tmp =
std::env::temp_dir().join(format!("docgen_build_cli_math_test_{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(tmp.join("docs")).unwrap();
fs::copy(fixture.join("docs/math.md"), tmp.join("docs/math.md")).unwrap();
let status = Command::new(env!("CARGO_BIN_EXE_docgen"))
.arg("build")
.arg(&tmp)
.status()
.unwrap();
assert!(status.success());
let math = fs::read_to_string(tmp.join("dist/math/index.html")).unwrap();
assert!(math.contains("class=\"katex\""), "no katex html: {math}");
assert!(math.contains("katex-display"), "no display math: {math}");
assert!(!math.contains("$E=mc^2$"));
assert!(!math.contains("$$\\sum"));
assert!(math.contains("application/x-tex"));
assert!(math.contains(r#"href="/vendor/katex/katex.min.css""#));
assert!(tmp.join("dist/vendor/katex/katex.min.css").is_file());
assert!(tmp
.join("dist/vendor/katex/fonts/KaTeX_Main-Regular.woff2")
.is_file());
let fonts = fs::read_dir(tmp.join("dist/vendor/katex/fonts"))
.unwrap()
.filter(|e| {
e.as_ref()
.unwrap()
.path()
.extension()
.map(|x| x == "woff2")
.unwrap_or(false)
})
.count();
assert_eq!(fonts, 16, "expected 16 katex fonts");
assert!(!tmp.join("dist/vendor/katex/katex.min.js").exists());
assert!(!tmp.join("dist/vendor/katex/auto-render.min.js").exists());
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn broken_math_degrades_without_failing_build() {
let tmp = std::env::temp_dir().join(format!(
"docgen_build_cli_badmath_test_{}",
std::process::id()
));
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(tmp.join("docs")).unwrap();
fs::write(
tmp.join("docs/bad.md"),
"# Bad\n\nProse before. Broken inline math: $<script>\\frac{$ and prose after.\n",
)
.unwrap();
let status = Command::new(env!("CARGO_BIN_EXE_docgen"))
.arg("build")
.arg(&tmp)
.status()
.unwrap();
assert!(status.success(), "build must not fail on broken math");
let page = fs::read_to_string(tmp.join("dist/bad/index.html")).unwrap();
assert!(
page.contains("docgen-math-error"),
"no math-error fallback: {page}"
);
assert!(
page.contains("<script>"),
"metachars not escaped: {page}"
);
assert!(
!page.contains("<script>\\frac"),
"raw script tag leaked: {page}"
);
assert!(page.contains("Prose before."));
assert!(page.contains("and prose after."));
let _ = fs::remove_dir_all(&tmp);
}