go-help/templates_test.go
Snider 944cad006b feat(help): Phase 2 — HTTP server, rendering, static site generator, CLI ingestion
Add complete HTTP server and rendering layer for the help catalog:

- render.go: Markdown-to-HTML via goldmark (GFM, typographer, raw HTML)
- server.go: HTTP server with 6 routes (HTML index/topic/search + JSON API)
- templates.go: Embedded HTML templates with dark theme (bg #0d1117)
- templates/: base, index, topic, search, 404 page templates
- generate.go: Static site generator with client-side JS search
- ingest.go: CLI help text parser (Usage/Flags/Examples/Commands sections)

320 tests passing, 95.5% coverage, race-clean, vet-clean.

Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 08:50:10 +00:00

167 lines
4.6 KiB
Go

// SPDX-Licence-Identifier: EUPL-1.2
package help
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseTemplates_Good(t *testing.T) {
pages := []string{"index.html", "topic.html", "search.html", "404.html"}
for _, page := range pages {
t.Run(page, func(t *testing.T) {
tmpl, err := parseTemplates(page)
require.NoError(t, err, "template %s should parse without error", page)
assert.NotNil(t, tmpl)
})
}
}
func TestRenderPage_Good_Index(t *testing.T) {
topics := []*Topic{
{ID: "getting-started", Title: "Getting Started", Tags: []string{"intro"}, Content: "Welcome."},
{ID: "config", Title: "Configuration", Tags: []string{"setup"}, Content: "Config options."},
}
data := indexData{
Topics: topics,
Groups: groupTopicsByTag(topics),
}
var buf bytes.Buffer
err := renderPage(&buf, "index.html", data)
require.NoError(t, err)
html := buf.String()
assert.Contains(t, html, "Getting Started")
assert.Contains(t, html, "Configuration")
assert.Contains(t, html, "2 topics")
assert.Contains(t, html, "core help")
}
func TestRenderPage_Good_Topic(t *testing.T) {
topic := &Topic{
ID: "getting-started",
Title: "Getting Started",
Content: "# Getting Started\n\nWelcome to the **guide**.\n",
Tags: []string{"intro"},
Sections: []Section{
{ID: "overview", Title: "Overview", Level: 2},
},
Related: []string{"config"},
}
data := topicData{Topic: topic}
var buf bytes.Buffer
err := renderPage(&buf, "topic.html", data)
require.NoError(t, err)
html := buf.String()
assert.Contains(t, html, "Getting Started")
assert.Contains(t, html, "<strong>guide</strong>")
assert.Contains(t, html, "Overview")
assert.Contains(t, html, "config")
}
func TestRenderPage_Good_Search(t *testing.T) {
data := searchData{
Query: "install",
Results: []*SearchResult{
{
Topic: &Topic{ID: "install", Title: "Installation", Tags: []string{"setup"}},
Score: 12.5,
Snippet: "How to **install** the tool.",
},
},
}
var buf bytes.Buffer
err := renderPage(&buf, "search.html", data)
require.NoError(t, err)
html := buf.String()
assert.Contains(t, html, "install")
assert.Contains(t, html, "Installation")
assert.Contains(t, html, "1 result")
assert.Contains(t, html, "12.5")
}
func TestRenderPage_Good_404(t *testing.T) {
var buf bytes.Buffer
err := renderPage(&buf, "404.html", nil)
require.NoError(t, err)
html := buf.String()
assert.Contains(t, html, "not found")
assert.Contains(t, html, "404")
}
func TestGroupTopicsByTag_Good(t *testing.T) {
topics := []*Topic{
{ID: "a", Title: "Alpha", Tags: []string{"setup"}, Order: 2},
{ID: "b", Title: "Beta", Tags: []string{"setup"}, Order: 1},
{ID: "c", Title: "Gamma", Tags: []string{"advanced"}},
{ID: "d", Title: "Delta"}, // no tags -> "other"
}
groups := groupTopicsByTag(topics)
require.Len(t, groups, 3)
// Groups should be sorted alphabetically by tag
assert.Equal(t, "advanced", groups[0].Tag)
assert.Equal(t, "other", groups[1].Tag)
assert.Equal(t, "setup", groups[2].Tag)
// Within "setup", topics should be sorted by Order then Title
setupGroup := groups[2]
require.Len(t, setupGroup.Topics, 2)
assert.Equal(t, "Beta", setupGroup.Topics[0].Title) // Order 1
assert.Equal(t, "Alpha", setupGroup.Topics[1].Title) // Order 2
}
func TestTemplateFuncs_Good(t *testing.T) {
fns := templateFuncs()
t.Run("truncate short string", func(t *testing.T) {
fn := fns["truncate"].(func(string, int) string)
assert.Equal(t, "hello", fn("hello", 10))
})
t.Run("truncate long string", func(t *testing.T) {
fn := fns["truncate"].(func(string, int) string)
result := fn("hello world this is long", 11)
assert.Equal(t, "hello world...", result)
})
t.Run("truncate strips headings", func(t *testing.T) {
fn := fns["truncate"].(func(string, int) string)
result := fn("# Title\n\nSome content here.", 100)
assert.Equal(t, "Some content here.", result)
assert.NotContains(t, result, "#")
})
t.Run("pluralise singular", func(t *testing.T) {
fn := fns["pluralise"].(func(int, string, string) string)
assert.Equal(t, "topic", fn(1, "topic", "topics"))
})
t.Run("pluralise plural", func(t *testing.T) {
fn := fns["pluralise"].(func(int, string, string) string)
assert.Equal(t, "topics", fn(0, "topic", "topics"))
assert.Equal(t, "topics", fn(5, "topic", "topics"))
})
t.Run("multiply", func(t *testing.T) {
fn := fns["multiply"].(func(int, int) int)
assert.Equal(t, 24, fn(4, 6))
})
t.Run("sub", func(t *testing.T) {
fn := fns["sub"].(func(int, int) int)
assert.Equal(t, 2, fn(5, 3))
})
}