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>
167 lines
4.6 KiB
Go
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))
|
|
})
|
|
}
|