The theme was supposed to need no engine work. It needed four things, which is exactly what the audit said would happen if the contract had gaps rather than the theme (ADR-0046) — a reader could not reach, from any page: another section, the tags on the page they were reading, the extras beside it, or the same page in another language. So the contract grew three fields, all additive: `.Sections` for navigation, `.Tags` with each term's listing URL, and `.ExtrasURL`, empty when a bundle has none so a theme never links a 404. Sections arrive through `Renderer.Navigation`, a callback, because sections change when content does and a copy would go stale — the nav updates on a rebuild along with everything else. One flaw only visible by looking at a rendered page: the language switcher pointed at the canonical host, because `.Alternates` went absolute for hreflang. Those are two needs, so an Alternate now carries `.URL` (absolute, for machines) and `.Path` (relative, for a link a person clicks). The theme now demonstrates every field it is given, including `.First`/`.Last`, which existed and were never rendered. Still no JavaScript, still one stylesheet. A test that asserted a page had no sequence nav was matching the inlined stylesheet rather than the markup, and now matches the element. That is the third time a loose assertion has passed for the wrong reason.
252 lines
8.0 KiB
Go
252 lines
8.0 KiB
Go
package render
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"khosra/internal/content"
|
|
)
|
|
|
|
func TestBundleRendersMarkdownIntoTheTheme(t *testing.T) {
|
|
r, err := New(nil, content.Settings{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := content.Parse("posts/hello.md", []byte("---\ntitle: Hello\n---\n\n# Heading\n\nSome *prose*.\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := r.Bundle(b, "en", []string{"en"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(out)
|
|
for _, want := range []string{
|
|
"<!doctype html>", `<html lang="en">`, "<title>Hello</title>",
|
|
"<h1>Hello</h1>", "<em>prose</em>", "<style>",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("output missing %q\n---\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBundleWithoutTitleFallsBackToKey(t *testing.T) {
|
|
r, err := New(nil, content.Settings{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := content.Parse("status/note.md", []byte("just a note\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := r.Bundle(b, "en", []string{"en"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(out), "<title>status/note</title>") {
|
|
t.Errorf("a titleless bundle must still produce a title element:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestSiteOverridesOneBlockAndInheritsTheRest(t *testing.T) {
|
|
siteFS := fstest.MapFS{
|
|
"templates/page.html": {Data: []byte(`{{define "main"}}<section class="mine">{{.Title}}</section>{{end}}`)},
|
|
}
|
|
r, err := New(siteFS, content.Settings{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := content.Parse("pages/about.md", []byte("---\ntitle: About\n---\nbody\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := r.Bundle(b, "en", []string{"en"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(out)
|
|
if !strings.Contains(got, `<section class="mine">About</section>`) {
|
|
t.Errorf("the site override should win:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "<!doctype html>") || !strings.Contains(got, `<link rel="canonical"`) {
|
|
t.Errorf("the document should still come from the embedded base:\n%s", got)
|
|
}
|
|
if strings.Contains(got, "<article>") {
|
|
t.Error("the embedded main should have been replaced, not appended")
|
|
}
|
|
}
|
|
|
|
func TestAListingOverrideDoesNotLeakIntoBundlePages(t *testing.T) {
|
|
siteFS := fstest.MapFS{
|
|
"templates/list.html": {Data: []byte(`{{define "main"}}LISTING ONLY{{end}}`)},
|
|
}
|
|
r, err := New(siteFS, content.Settings{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := content.Parse("pages/about.md", []byte("---\ntitle: About\n---\nbody\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := r.Bundle(b, "en", nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(out), "LISTING ONLY") {
|
|
t.Error("a listing override must not reach bundle pages — that is why sets are per kind")
|
|
}
|
|
}
|
|
|
|
func TestSiteStylesheetReplacesTheReferenceOne(t *testing.T) {
|
|
siteFS := fstest.MapFS{"templates/theme.css": {Data: []byte("body{color:rebeccapurple}")}}
|
|
r, err := New(siteFS, content.Settings{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, _ := content.Parse("pages/x.md", []byte("hi\n"))
|
|
out, err := r.Bundle(b, "en", nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(out), "rebeccapurple") {
|
|
t.Error("the site stylesheet should replace the reference one")
|
|
}
|
|
}
|
|
|
|
func TestADeclaredBaseMakesMachineReadableURLsAbsolute(t *testing.T) {
|
|
// A canonical link and an hreflang are read by machines that resolve neither against the page, so both
|
|
// go absolute as soon as the site says where it lives (ADR-0039).
|
|
r, err := New(nil, content.Settings{Base: "https://khosra.example", Title: "Khosra"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := content.Parse("posts/hello.md", []byte("---\ntitle: Hello\n---\nhi\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := r.Bundle(b, "en", []string{"en", "bn"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(out)
|
|
for _, want := range []string{
|
|
`rel="canonical" href="https://khosra.example/posts/hello/"`,
|
|
`hreflang="bn" href="https://khosra.example/bn/posts/hello/"`,
|
|
`property="og:url" content="https://khosra.example/posts/hello/"`,
|
|
`<title>Hello · Khosra</title>`,
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("missing %q:\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWithoutABaseEverythingStaysRelative(t *testing.T) {
|
|
r, err := New(nil, content.Settings{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, _ := content.Parse("posts/hello.md", []byte("---\ntitle: Hello\n---\nhi\n"))
|
|
out, err := r.Bundle(b, "en", []string{"en"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(out)
|
|
if !strings.Contains(got, `rel="canonical" href="/posts/hello/"`) {
|
|
t.Errorf("a site that declared no base still links to itself:\n%s", got)
|
|
}
|
|
if strings.Contains(got, "og:site_name") {
|
|
t.Error("no declared title means no site_name tag, rather than an empty one")
|
|
}
|
|
}
|
|
|
|
func TestATagListingOffersBothShapesAndLetsTheThemeChoose(t *testing.T) {
|
|
// The engine may not decide that a tag listing looks grouped (ADR-0046). It supplies the partition, because
|
|
// a template cannot group for itself, and the flat list beside it, because choosing is markup.
|
|
r, err := New(nil, content.Settings{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var bundles []content.Bundle
|
|
for _, spec := range []struct{ name, section string }{
|
|
{"posts/essay", "posts"}, {"comics/strip", "comics"}, {"posts/other", "posts"},
|
|
} {
|
|
b, err := content.Parse(spec.name+".md", []byte("---\ntitle: "+spec.name+"\n---\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b.Route = b.Key
|
|
bundles = append(bundles, b)
|
|
}
|
|
out, err := r.Tag("", "monsoon", "en", bundles, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The reference theme renders groups, so the sections appear as headings.
|
|
got := string(out)
|
|
for _, want := range []string{"<h2>posts</h2>", "<h2>comics</h2>"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("missing %q:\n%s", want, got)
|
|
}
|
|
}
|
|
// A theme preferring a flat list must have one, with each entry able to say where it came from.
|
|
if !strings.Contains(got, "posts/essay") || !strings.Contains(got, "comics/strip") {
|
|
t.Errorf("every entry should be present whichever shape is used:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestAPageCanReachTheRestOfTheSite(t *testing.T) {
|
|
// The four things a reader could not get to before the reference theme was finished (ADR-0049): another
|
|
// section, this page's tags, its extras, and the same page in another language.
|
|
fsys := fstest.MapFS{
|
|
"content/posts/one/index.md": {Data: []byte("---\ntitle: One\ntags: [Monsoon]\n---\nx\n")},
|
|
"content/posts/one/extras/note.md": {Data: []byte("a note\n")},
|
|
}
|
|
r, err := New(fsys, content.Settings{Title: "Khosra"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r.Navigation(func() []string { return []string{"posts", "comics"} })
|
|
b, err := content.Parse("posts/one/index.md", []byte("---\ntitle: One\ntags: [Monsoon]\n---\nx\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b.Path, b.Route = "content/posts/one/index.md", b.Key
|
|
out, err := r.Bundle(b, "en", []string{"en", "bn"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(out)
|
|
for _, want := range []string{
|
|
`href="/posts/"`, // navigation
|
|
`href="/comics/"`, // a section this page is not in
|
|
`rel="tag" href="/tags/monsoon/"`, // its own tags, at the term's listing
|
|
`href="/posts/one/extras/"`, // its extras, offered only because they exist
|
|
`href="/bn/posts/one/" hreflang="bn"`, // the other language, as a visible relative link
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("a reader cannot reach %s:\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAPageOffersNoExtrasLinkWhenThereAreNone(t *testing.T) {
|
|
// Guessing would give every page a link to a 404.
|
|
fsys := fstest.MapFS{"content/posts/bare/index.md": {Data: []byte("---\ntitle: Bare\n---\nx\n")}}
|
|
r, err := New(fsys, content.Settings{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, _ := content.Parse("posts/bare/index.md", []byte("---\ntitle: Bare\n---\nx\n"))
|
|
b.Path, b.Route = "content/posts/bare/index.md", b.Key
|
|
out, err := r.Bundle(b, "en", []string{"en"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(out), "/extras/") {
|
|
t.Errorf("no extras exist, so nothing should link them:\n%s", out)
|
|
}
|
|
}
|