
ThemeTest.pudu
Pudu101 lines10.4 KB
1/** @Docgen.Theme.Tests — the layout, its overrides, and site-wide files render as specified */2module PuduLangDocgen.ThemeTest34import Std.Io as Io5import Std.Test as Test6import PuduLangDocgen.Build.Site as Site7import PuduLangDocgen.Constants.Codes as Codes8import PuduLangDocgen as Docgen9import PuduLangDocgen.Markdown.Phrase as Phrase10import PuduLangDocgen.Site.Format as Format11import PuduLangDocgen.Site.Gallery as Gallery12import PuduLangDocgen.Site.Print as Print13import PuduLangDocgen.Site.Redirect as Redirect14import PuduLangDocgen.Site.Search as Search15import PuduLangDocgen.Site.Sitemap as Sitemap16import PuduLangDocgen.Site.Strings as Strings17import PuduLangDocgen.Site.View as View18import PuduLangDocgen.Theme as Theme192021fn page(meta: Array[(Str, Docgen.Meta)]) -> Docgen.Page {22 Docgen.Page{path: "guide/a.html", source: "docs/guide/a.md", kind: "article", title: "Alpha <1>", uid: "", summary: "About alpha.", body: "<h2 id=\"one\">One</h2><h2 id=\"two\">Two</h2>", headings: [Docgen.Heading{id: "one", title: "One", level: 2}, Docgen.Heading{id: "two", title: "Two", level: 2}], links: [], meta: meta}23}242526fn frame() -> View.Frame {27 let item = Docgen.TocItem{title: "Alpha", href: "guide/a.html", uid: "", expanded: false, children: []}28 let group = Docgen.TocItem{title: "Guide", href: "", uid: "", expanded: false, children: [item]}29 View.Frame{navbar: [Docgen.TocItem{..group, href: "guide/a.html", children: []}], toc: [group], trail: [group, item], previous: None, next: Some(Docgen.TocItem{..item, title: "Beta", href: "guide/b.html"}), editUrl: "https://git.example/edit", updated: "2026-09-01", features: ["math"], root: "", pdf: "guide/toc.pdf", year: "2026", tocPath: "guide/toc.json", navPath: "toc.json"}30}313233fn rendered(meta: Array[(Str, Docgen.Meta)]) -> Str {34 match Theme.load(&[]) {35 case Ok(look) => Theme.page(&look, &View.view(&page(meta), &frame()))36 case Err(_) => ""37 }38}394041fn main() -> Int {42 let html = rendered([("_appTitle", Docgen.Text("Site")), ("_lang", Docgen.Text("es")), ("_baseUrl", Docgen.Text("https://docs.example")), ("keywords", Docgen.Items([Docgen.Text("a"), Docgen.Text("b")])), ("_meta", Docgen.Fields([("owner", Docgen.Text("team"))])), ("_googleAnalyticsTagId", Docgen.Text("G-1"))])43 let landing = rendered([("_layout", Docgen.Text("landing"))])44 let chromeless = rendered([("_layout", Docgen.Text("chromeless"))])45 let footless = rendered([("_disableFooter", Docgen.Flag(true))])46 let override = match Theme.load(&[("partials/footer.html", "<footer>custom \{\{siteName\}\}</footer>"), ("partials/extra.html", "x"), ("notes.txt", "ignored")]) {47 case Ok(look) => Theme.page(&look, &View.view(&page([]), &frame()))48 case Err(_) => ""49 }50 let broken = match Theme.load(&[("layout.html", "\{\{#open\}\}")]) {51 case Ok(_) => []52 case Err(found) => found.map(|problem: Docgen.Diagnostic| problem.code)53 }54 let cover = Docgen.Page{..page([]), path: "cover.html", body: "<h1>Cover</h1>"}55 let book = Site.Book{output: "guide/toc.json", items: [Docgen.TocItem{title: "Alpha", href: "guide/a.html", uid: "", expanded: false, children: []}], meta: [("pdf", Docgen.Flag(true)), ("pdfTocPage", Docgen.Flag(true)), ("pdfCoverPage", Docgen.Text("cover.html")), ("pdfFileName", Docgen.Text("manual.pdf"))], nested: false}56 let printed = Print.documents(&[page([]), cover], &[book])57 let checks = Test.suite("Theme", &[58 Test.equals("assets", &Theme.assets().map(|artifact: Docgen.Artifact| artifact.path), &["_assets/site.css", "_assets/site.js"]),59 Test.that("stylesheet has both schemes and print", Theme.assets()[0].content.contains("[data-theme=\"dark\"]") && Theme.assets()[0].content.contains("@media print")),60 Test.that("escaped tab title", html.contains("<title>Alpha <1> | Site</title>")),61 Test.that("language", html.contains("<html lang=\"es\"")),62 Test.that("localized text", html.contains("Buscar") && html.contains("En este artículo")),63 Test.that("description and keywords", html.contains("<meta name=\"description\" content=\"About alpha.\">") && html.contains("<meta name=\"keywords\" content=\"a, b\">")),64 Test.that("social cards", html.contains("<meta property=\"og:type\" content=\"article\">") && html.contains("<meta name=\"twitter:card\" content=\"summary\">")),65 Test.that("custom meta", html.contains("<meta name=\"owner\" content=\"team\">")),66 Test.that("canonical and structured data", html.contains("<link rel=\"canonical\" href=\"https://docs.example/guide/a.html\">") && html.contains("\"@type\":\"BreadcrumbList\"")),67 Test.that("analytics", html.contains("gtag/js?id=G-1")),68 Test.that("navbar active", html.contains("aria-current=\"true\" class=\"active\">Guide</a>")),69 Test.that("toc tree", html.contains("<button type=\"button\" class=\"toc-group\" aria-expanded=\"true\">Guide</button>") && html.contains("aria-current=\"page\">Alpha</a>")),70 Test.that("breadcrumb", html.contains("<span aria-current=\"page\">Alpha <1></span>")),71 Test.that("actions", html.contains("href=\"https://git.example/edit\"") && html.contains("<time datetime=\"2026-09-01\">") && html.contains("href=\"toc.pdf\"")),72 Test.that("pager", html.contains("href=\"b.html\" rel=\"next\"") && !html.contains("rel=\"prev\"")),73 Test.that("affix", html.contains("<a href=\"#one\">One</a>")),74 Test.that("math script", html.contains("mathjax")),75 Test.that("landing drops navigation panes", !landing.contains("class=\"sidebar\"") && !landing.contains("class=\"affix\"") && landing.contains("layout-landing")),76 Test.that("chromeless drops header and footer", !chromeless.contains("class=\"site-header\"") && !chromeless.contains("class=\"site-footer\"")),77 Test.that("footer shown by default", html.contains("class=\"site-footer\"")),78 Test.that("footer switched off", !footless.contains("class=\"site-footer\"") && footless.contains("class=\"site-header\"")),79 Test.that("partial override", override.contains("<footer>custom Documentation</footer>")),80 Test.equals("invalid template", &broken, &[Codes.TEMPLATE_INVALID]),81 Test.equals("strings fall back to English", &Strings.forLanguage("xx", &[]), &Strings.forLanguage("en", &[])),82 Test.that("string override", match Strings.forLanguage("en", &[("search", Docgen.Text("Find"))]) { case Docgen.Fields(fields) => fields.contains(("search", Docgen.Text("Find"))) case _ => false }),83 Test.equals("plain text", &Search.plainText("<p>a & <b>b</b></p><script>x()</script><button>Copy</button> c"), &"a & b c"),84 Test.that("search skips hidden pages", !Search.index(&[page([("_noindex", Docgen.Flag(true))])]).contains("guide/a.html") && Search.index(&[page([])]).contains("\"title\":\"Alpha <1>\"")),85 Test.equals("sitemap", &Sitemap.render(&[page([])], &Sitemap.Options{baseUrl: "https://docs.example", priority: "0.5", changefreq: "daily", files: [("docs/guide/**", [("priority", Docgen.Text("0.9"))])]}, &[("guide/a.html", "2026-09-01")]), &"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n <url>\n <loc>https://docs.example/guide/a.html</loc>\n <lastmod>2026-09-01</lastmod>\n <changefreq>daily</changefreq>\n <priority>0.9</priority>\n </url>\n</urlset>\n"),86 Test.equals("print rebasing", &Print.rebase("<h2 id=\"a\">A</h2><a href=\"#a\">x</a><a href=\"b.html#k\">y</a><a href=\"other.html\">z</a><img src=\"../img/p.png\"><a href=\"https://e.x/\">w</a>", "guide/a.html", "guide/toc.print.html", &mapOf([("guide/a.html", 0), ("guide/b.html", 1)]), "p0-"), &"<h2 id=\"p0-a\">A</h2><a href=\"#p0-a\">x</a><a href=\"#p1-k\">y</a><a href=\"other.html\">z</a><img src=\"../img/p.png\"><a href=\"https://e.x/\">w</a>"),87 Test.that("print documents", printed.length() == 1 && printed[0].pdf == "guide/manual.pdf" && printed[0].path == "guide/manual.print.html" && printed[0].html.contains("class=\"pdftoc print-section\"") && printed[0].html.contains("<section class=\"print-cover\">") && printed[0].html.contains("id=\"page-0\"") && printed[0].footer == Print.FOOTER),88 Test.equals("tables without pdf print nothing", &Print.documents(&[page([])], &[Site.Book{output: "toc.json", items: [], meta: [], nested: false}]).length(), &0),89 Test.equals("formatted html", &Format.html("<!doctype html><html><head><title>T</title></head><body><div><p>a <b>b</b></p><pre>x\n y</pre><ul><li>one</li><li><p>two</p></li></ul><script>if (a) \{ b() \}</script></div></body></html>"), &"<!doctype html>\n<html>\n <head>\n <title>T</title>\n </head>\n <body>\n <div>\n <p>a <b>b</b></p>\n <pre>x\n y</pre>\n <ul>\n <li>one</li>\n <li>\n <p>two</p>\n </li>\n </ul>\n <script>if (a) \{ b() \}</script>\n </div>\n </body>\n</html>\n"),90 Test.that("gallery cards", match Gallery.render(&Docgen.Fields([("title", Docgen.Text("Templates")), ("items", Docgen.Items([Docgen.Fields([("name", Docgen.Text("Default")), ("type", Docgen.Text("Built in")), ("author", Docgen.Text("Team")), ("description", Docgen.Text("The **default** look.")), ("homepage", Docgen.Text("https://example.org")), ("thumbnail", Docgen.Text("javascript:x")), ("usage", Docgen.Fields([("config", Docgen.Text("\"template\": [\"default\"]"))]))])]))]), &Phrase.Scope{page: "x.html", source: "x.yml", outputs: mapOf([]), references: mapOf([]), settings: Phrase.rendering()}) {91 case Ok(found) => found.body.contains("<h2 class=\"card-title\"><a href=\"https://example.org\">Default</a></h2>") && found.body.contains("<strong>default</strong>") && found.body.contains("<dt>Configuration</dt>") && !found.body.contains("javascript") && found.headings.length() == 292 case Err(_) => false93 }),94 Test.equals("gallery needs names", &match Gallery.render(&Docgen.Fields([("title", Docgen.Text("T")), ("items", Docgen.Items([Docgen.Fields([])]))]), &Phrase.Scope{page: "x.html", source: "x.yml", outputs: mapOf([]), references: mapOf([]), settings: Phrase.rendering()}) { case Ok(_) => "accepted" case Err(reason) => reason }, &"each catalog item needs a name"),95 Test.that("redirect", Redirect.page("Old", "new.html?a=1&b=2").contains("<meta http-equiv=\"refresh\" content=\"0; url=new.html?a=1&b=2\">"))96 ])97 let ran = Test.run(&checks)98 for failure in Test.failuresOf(&ran) { let _printed = Io.writeErrorLine(failure) }99 Test.report(&ran)100}101