a73x

internal/site/render_test.go

Ref:   Size: 6.3 KiB   History

package site

import (
	"strings"
	"testing"
)

var testTargets = map[string]string{
	"README.md":     "/docs/",
	"ssh-access.md": "/docs/ssh-access/",
	"../ROADMAP.md": "/docs/roadmap/",
	"shape.html":    "/docs/shape.html",
}

func TestRenderRewritesInternalLinks(t *testing.T) {
	got, _, err := render([]byte("see [ssh](ssh-access.md) and [index](README.md)"), testTargets)
	if err != nil {
		t.Fatal(err)
	}
	html := string(got)
	for _, want := range []string{`href="/docs/ssh-access/"`, `href="/docs/"`} {
		if !strings.Contains(html, want) {
			t.Errorf("output missing %s:\n%s", want, html)
		}
	}
}

func TestRenderPreservesFragments(t *testing.T) {
	got, _, err := render([]byte("[a](ssh-access.md#gate)"), testTargets)
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(string(got), `href="/docs/ssh-access/#gate"`) {
		t.Errorf("fragment lost:\n%s", got)
	}
}

func TestRenderLeavesExternalAndAbsoluteAlone(t *testing.T) {
	src := "[x](https://eitri.sh) [y](/dl/) [z](#local)"
	got, _, err := render([]byte(src), testTargets)
	if err != nil {
		t.Fatal(err)
	}
	html := string(got)
	for _, want := range []string{`href="https://eitri.sh"`, `href="/dl/"`, `href="#local"`} {
		if !strings.Contains(html, want) {
			t.Errorf("output missing %s:\n%s", want, html)
		}
	}
}

func TestRenderFailsOnUnknownInternalLink(t *testing.T) {
	_, _, err := render([]byte("[gone](no-such-doc.md)"), testTargets)
	if err == nil || !strings.Contains(err.Error(), "no-such-doc.md") {
		t.Fatalf("want broken-link error naming the target, got %v", err)
	}
}

func TestRenderTables(t *testing.T) {
	got, _, err := render([]byte("| a | b |\n|---|---|\n| 1 | 2 |"), testTargets)
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(string(got), "<table>") {
		t.Errorf("GFM tables not rendered:\n%s", got)
	}
}

func TestRenderRewritesParentRelativeLinks(t *testing.T) {
	got, _, err := render([]byte("[r](../ROADMAP.md)"), testTargets)
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(string(got), `href="/docs/roadmap/"`) {
		t.Errorf("parent-relative link not rewritten:\n%s", got)
	}
}

func TestRenderNormalizesDotSlashPrefix(t *testing.T) {
	got, _, err := render([]byte("[s](./ssh-access.md)"), testTargets)
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(string(got), `href="/docs/ssh-access/"`) {
		t.Errorf("./-prefixed link not rewritten:\n%s", got)
	}
}

func TestRenderRewritesImageDestinations(t *testing.T) {
	got, _, err := render([]byte("![d](shape.html)"), testTargets)
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(string(got), `src="/docs/shape.html"`) {
		t.Errorf("image destination not rewritten:\n%s", got)
	}
}

func TestSummaryTakesTheOpeningParagraph(t *testing.T) {
	md := "# quickstart\n\nZero to a first VM on one host, then a second.\n\nA later paragraph.\n"
	if got, want := summary([]byte(md)), "Zero to a first VM on one host, then a second."; got != want {
		t.Errorf("summary = %q, want %q", got, want)
	}
}

func TestSummaryStripsMarkupAndJoinsLines(t *testing.T) {
	md := "# t\n\n*The ease* of a [cloud provider](https://x),\nbacked by `what` you own\n"
	if got, want := summary([]byte(md)), "The ease of a cloud provider, backed by what you own"; got != want {
		t.Errorf("summary = %q, want %q", got, want)
	}
}

func TestSummarySkipsNonProseOpeners(t *testing.T) {
	md := "# t\n\n- a list item\n\n```\ncode\n```\n\nThe real opening line.\n"
	if got, want := summary([]byte(md)), "The real opening line."; got != want {
		t.Errorf("summary = %q, want %q", got, want)
	}
}

// A description longer than the crawlers show is cut at a word boundary, so
// no preview ends mid-word.
func TestSummaryClampsAtAWordBoundary(t *testing.T) {
	long := strings.Repeat("alpha bravo charlie delta echo ", 20)
	got := summary([]byte("# t\n\n" + long))
	if len([]rune(got)) > summaryLimit+1 {
		t.Errorf("summary is %d runes, want at most %d plus an ellipsis", len([]rune(got)), summaryLimit)
	}
	if !strings.HasSuffix(got, "…") {
		t.Errorf("clamped summary should end in an ellipsis: %q", got)
	}
	if strings.Contains(got, "  ") || strings.HasSuffix(strings.TrimSuffix(got, "…"), " ") {
		t.Errorf("clamped summary has ragged whitespace: %q", got)
	}
}

func TestSummaryOfProselessDocIsEmpty(t *testing.T) {
	if got := summary([]byte("# just a heading\n")); got != "" {
		t.Errorf("summary = %q, want empty", got)
	}
}

// A long, many-sectioned page gets a contents list in front of its first
// section — inside the content, because the page's own title is in there too.
func TestWithTOCListsSectionsOfALongPage(t *testing.T) {
	var md strings.Builder
	md.WriteString("# page\n\nopening prose\n")
	for _, name := range []string{"Join a Linux host", "Join a Mac", "Sharp edges", "Related"} {
		md.WriteString("\n## " + name + "\n\n" + strings.Repeat("filler prose. ", 200) + "\n")
	}
	html, secs, err := render([]byte(md.String()), testTargets)
	if err != nil {
		t.Fatal(err)
	}
	if len(secs) != 4 {
		t.Fatalf("found %d sections, want 4: %v", len(secs), secs)
	}
	out := string(withTOC(html, secs))
	toc := strings.Index(out, `<nav class="toc">`)
	if toc < 0 {
		t.Fatalf("no table of contents:\n%s", out[:400])
	}
	if h2 := strings.Index(out, "<h2"); toc > h2 {
		t.Errorf("contents list sits after the first section (%d > %d)", toc, h2)
	}
	if h1 := strings.Index(out, "<h1"); h1 > toc {
		t.Error("contents list sits above the page title")
	}
	for _, want := range []string{`href="#join-a-linux-host"`, `href="#join-a-mac"`, "Sharp edges"} {
		if !strings.Contains(out, want) {
			t.Errorf("contents list missing %s:\n%s", want, out[toc:toc+400])
		}
	}
}

func TestWithTOCLeavesShortPagesAlone(t *testing.T) {
	md := "# page\n\n## one\n\nprose\n\n## two\n\nprose\n\n## three\n\nprose\n\n## four\n\nprose\n"
	html, secs, err := render([]byte(md), testTargets)
	if err != nil {
		t.Fatal(err)
	}
	if got := string(withTOC(html, secs)); strings.Contains(got, "toc") {
		t.Errorf("short page got a contents list:\n%s", got)
	}
}

// Sections are what the reader can jump to, so a heading with markup in it
// still has to produce plain link text.
func TestSectionsFlattenHeadingMarkup(t *testing.T) {
	_, secs, err := render([]byte("# p\n\n## The `eitri` *client*\n\nprose\n"), testTargets)
	if err != nil {
		t.Fatal(err)
	}
	if len(secs) != 1 || secs[0].Title != "The eitri client" {
		t.Errorf("sections = %v, want one titled \"The eitri client\"", secs)
	}
}