Files
bookhoard/templates/docs_render_test.go
T
John O'Keefe 0d613ba0e3 fix(docs): folder-index pages expand the sidebar and highlight — same rule for every page
The filter manual is the docs tree's first FOLDER-index page: it serves
at /docs/user/filters while its nav entry carries /index
(/docs/user/filters/index). The sidebar's two checks — group expansion
(containsURL) and active-link styling (docsNavClass) — compared URLs
EXACTLY, so the folder URL matched neither: no submenu expansion, no
highlight (2026-11-01 user report: "the left sidebar should open to
the submenu and highlight the page").

Fix is structural, not per-page: both checks normalize folder-index
URLs (a trailing /index is the same page as its directory), so EVERY
folder-index page — current and future — expands and highlights like
a file page. No per-page registration exists anywhere in the docs
system to redo: the sidebar derives from the docs tree, and this was
the one URL shape its equality rule missed.

Pins: navigation (section + group expand for the folder URL, group
present) and docsNavClass (folder URL highlights the Overview item,
normalization symmetric, file pages keep their highlight, no
cross-highlighting).
2026-11-01 16:34:25 -05:00

167 lines
5.7 KiB
Go

package templates
import (
"context"
"strings"
"testing"
)
// The docs layout is a standalone documentation site: it must NOT embed the
// app chrome (its sidebar assumed a signed-in user) and must never contain a
// login form — the old layout posted credentials via htmx that was never
// loaded on this page, degrading to a native GET that put the username and
// password into the URL. These render tests pin that fix.
func TestDocsLayoutRendersStandalone(t *testing.T) {
nav := Navigation{
Sections: []NavSection{
{
Title: "Getting Started",
Icon: "book-open",
Items: []NavItem{{Title: "Overview", URL: "/docs"}},
},
{
Title: "User Guide",
Icon: "book",
Collapsed: true,
Items: []NavItem{{Title: "Dashboard", URL: "/docs/user/dashboard"}},
Groups: []NavGroup{
{
Title: "Devices",
Items: []NavItem{{Title: "Kobo Setup", URL: "/docs/user/devices/kobo-setup"}},
Collapsed: true,
},
},
},
},
}
doc := Document{
Title: "Test Doc",
Content: "<p>Hello docs</p>",
TOC: []TOCItem{{Level: 2, Title: "Section", Anchor: "section"}},
Breadcrumb: []BreadcrumbItem{
{Title: "Docs", URL: "/docs"},
{Title: "User Guide"},
},
SourceFile: "user/test.md",
}
var b strings.Builder
if err := DocsLayout(nav, doc, User{Theme: "tokyo-night"}, "/docs/user/test").Render(context.Background(), &b); err != nil {
t.Fatalf("DocsLayout: %v", err)
}
out := b.String()
for _, banned := range []string{
"app-sidebar", // app chrome
`hx-post="/api/auth/login"`, // no inline login form (the URL-leak bug)
`name="password"`, // credentials never render on docs pages
"prose-invert", // theme-aware prose, not hardcoded dark
"highlight-dark.min.css", // highlighting is server-side now
"cdn.tailwindcss.com", // no CDN dependencies
"lunr", // dead client-side search is gone
} {
if strings.Contains(out, banned) {
t.Errorf("docs layout must not contain %q", banned)
}
}
for _, want := range []string{
`href="/docs"`, // sidebar + logo links
`data-collapsed="true"`, // collapsed section state
`data-collapsed="false"`, // expanded section state
"Open App", // topbar action
"theme-select", // theme picker
"docs-search-results", // search dropdown target
"Hello docs", // content rendered
"page-docs",
} {
if !strings.Contains(out, want) {
t.Errorf("docs layout missing %q", want)
}
}
// Anonymous visitor: a sign-in LINK, never a form.
b.Reset()
if err := DocsLayout(nav, doc, User{Theme: "tokyo-night"}, "/docs/user/test").Render(context.Background(), &b); err != nil {
t.Fatalf("DocsLayout anonymous: %v", err)
}
if !strings.Contains(b.String(), `href="/login"`) {
t.Error("anonymous docs layout should link to /login")
}
}
func TestDocsTopbarSignedIn(t *testing.T) {
var b strings.Builder
if err := DocsTopbar(User{ID: "u1", Username: "reader", Theme: "tokyo-night"}).Render(context.Background(), &b); err != nil {
t.Fatalf("DocsTopbar: %v", err)
}
out := b.String()
if strings.Contains(out, `href="/login"`) {
t.Error("signed-in topbar must not offer /login")
}
if !strings.Contains(out, "reader") {
t.Error("signed-in topbar should surface the username")
}
}
func TestDocsSearchResultsPartial(t *testing.T) {
var b strings.Builder
results := []SearchResultItem{
{Title: "Kobo Setup", Category: "User Guide", URL: "/docs/user/devices/kobo-setup"},
}
if err := DocsSearchResults(results).Render(context.Background(), &b); err != nil {
t.Fatalf("DocsSearchResults: %v", err)
}
out := b.String()
for _, want := range []string{"Kobo Setup", "User Guide", `/docs/user/devices/kobo-setup`} {
if !strings.Contains(out, want) {
t.Errorf("search results missing %q", want)
}
}
// Empty state renders, not blank markup.
b.Reset()
if err := DocsSearchResults(nil).Render(context.Background(), &b); err != nil {
t.Fatalf("DocsSearchResults empty: %v", err)
}
if !strings.Contains(b.String(), "No matching pages.") {
t.Error("search results empty state missing")
}
}
func TestDocsNotFoundRenders(t *testing.T) {
var b strings.Builder
if err := DocsNotFound("Document Not Found", "no such page").Render(context.Background(), &b); err != nil {
t.Fatalf("DocsNotFound: %v", err)
}
out := b.String()
for _, want := range []string{"Document Not Found", "no such page", `href="/docs"`} {
if !strings.Contains(out, want) {
t.Errorf("404 page missing %q", want)
}
}
if strings.Contains(out, "cdn.tailwindcss.com") {
t.Error("404 page must not pull the tailwind CDN")
}
}
// docsNavClass folder-index normalization (2026-11-01): folder-index
// pages serve at their directory URL while nav entries carry /index —
// the Overview item must highlight for the folder URL, file pages keep
// their own highlight, and cross-highlighting stays impossible.
func TestDocsNavClassFolderIndex(t *testing.T) {
active := docsNavClass("/docs/user/filters", "/docs/user/filters/index", "pl-9")
if !strings.Contains(active, "bg-brand/15") {
t.Errorf("folder-index item must highlight as active, got %q", active)
}
if !strings.Contains(docsNavClass("/docs/user/filters/index", "/docs/user/filters", "pl-9"), "bg-brand/15") {
t.Errorf("normalization is symmetric (either URL order highlights)")
}
if !strings.Contains(docsNavClass("/docs/user/filters/cookbook", "/docs/user/filters/cookbook", "pl-9"), "bg-brand/15") {
t.Error("file-page highlight regression")
}
if strings.Contains(docsNavClass("/docs/user/filters/cookbook", "/docs/user/filters/index", "pl-9"), "bg-brand/15") {
t.Error("cookbook URL wrongly highlights the Overview item")
}
}