Simplification per spec: the menu tracks state through the URL alone. Land on /docs: fully closed, headers only (an explicit home rule now overrides the contains-URL rule, since the root overview doc lives in Getting Started and would otherwise auto-open it). Land on any doc page: its containing section + group render open at first paint, current link bold — no JS, no jump, arrows consistent. Navigate elsewhere: old closes, new opens. Deleted: docs_nav cookie const/parse, ApplyNavOverrides, cookie write + collect/persist helpers in web/src/docs.ts, data-key attributes in the sidebar templ, and the first-section-open fallback (docs home used to force-open the first section — spec'd as closed). toggleCollapsible remains for in-page peeking; purely ephemeral. The [data-collapsed=true] CSS from the previous round stays — it is what makes the server-rendered state actually hide sections at paint (the original bug was that this CSS never existed). Tests: TestBuildNavigationNothingCurrent now asserts zero sections open on unmatched paths; TestBuildNavigation asserts Getting Started closed on a device page (no site-wide pin). make test green; 146 vitests; live verified: home 0 expanded, doc pages exactly 2 (section+group).
203 lines
6.7 KiB
Go
203 lines
6.7 KiB
Go
package docs
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"bookhoard/templates"
|
|
)
|
|
|
|
func TestPlaceDoc(t *testing.T) {
|
|
tests := []struct {
|
|
path string
|
|
section string
|
|
group string
|
|
pinned bool
|
|
}{
|
|
{"index.md", "Getting Started", "", true},
|
|
{"user/user-guide.md", "User Guide", "", true},
|
|
{"user/dashboard.md", "User Guide", "", false},
|
|
{"user/admin-guide.md", "User Guide", "Administration", false},
|
|
{"user/devices/kobo-setup.md", "User Guide", "Devices", false},
|
|
{"developer/api/api-reference.md", "API Reference", "", true},
|
|
{"developer/api/dashboard.md", "API Reference", "", false},
|
|
{"developer/api/authentication/login.md", "API Reference", "Authentication", false},
|
|
{"developer/api/saved-filters/index.md", "API Reference", "Saved Filters", true},
|
|
{"developer/testing.md", "Developer", "", false},
|
|
{"developer/development.md", "Developer", "", true},
|
|
{"developer/scanner/audio-metadata.md", "Developer", "Scanner", false},
|
|
{"contract_pins.md", "Developer", "Reference", false},
|
|
{"FRONTEND_INTEGRATION.md", "Developer", "Reference", false},
|
|
{"development/calibre-opf-implementation.md", "Developer", "Reference", false},
|
|
{"operations/operations.md", "Operations", "", true},
|
|
{"operations/troubleshooting.md", "Operations", "", false},
|
|
{"contributing/contributing.md", "Contributing", "", true},
|
|
{"contributing/development.md", "Contributing", "", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := placeDoc(tt.path)
|
|
if got.section != tt.section || got.group != tt.group {
|
|
t.Errorf("placeDoc(%q) = (%q, %q), want (%q, %q)",
|
|
tt.path, got.section, got.group, tt.section, tt.group)
|
|
}
|
|
if (got.pin < 0) != tt.pinned {
|
|
t.Errorf("placeDoc(%q) pin = %d, pinned want %v", tt.path, got.pin, tt.pinned)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDocTitle(t *testing.T) {
|
|
tests := map[string]string{
|
|
"index.md": "Overview",
|
|
"user/user-guide.md": "User Guide",
|
|
"user/devices/koreader-setup.md": "KOReader Setup",
|
|
"developer/development.md": "Developer Portal",
|
|
"developer/api/api-reference.md": "Overview",
|
|
"developer/api/users/update_profile.md": "Update Profile",
|
|
"developer/api/book-matching/link_book.md": "Link Book",
|
|
"operations/troubleshooting.md": "Troubleshooting",
|
|
"contract_pins.md": "Contract Pins",
|
|
}
|
|
for path, want := range tests {
|
|
if got := docTitle(path); got != want {
|
|
t.Errorf("docTitle(%q) = %q, want %q", path, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDocURL(t *testing.T) {
|
|
if got := docURL("index.md"); got != "/docs" {
|
|
t.Errorf("docURL(index.md) = %q, want /docs", got)
|
|
}
|
|
if got := docURL("user/sync-guide.md"); got != "/docs/user/sync-guide" {
|
|
t.Errorf("docURL = %q, want /docs/user/sync-guide", got)
|
|
}
|
|
}
|
|
|
|
// buildTestDocs creates a representative docs tree in a temp dir and returns
|
|
// a handler over it.
|
|
func buildTestDocs(t *testing.T) *DocsHandler {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
files := []string{
|
|
"index.md",
|
|
"user/user-guide.md",
|
|
"user/dashboard.md",
|
|
"user/admin-guide.md",
|
|
"user/devices/kobo-setup.md",
|
|
"user/devices/koreader-setup.md",
|
|
"developer/api/api-reference.md",
|
|
"developer/api/authentication/login.md",
|
|
"developer/api/authentication/overview.md",
|
|
"developer/api/saved-filters/index.md",
|
|
"developer/testing.md",
|
|
"operations/troubleshooting.md",
|
|
"contributing/development.md",
|
|
"contract_pins.md",
|
|
}
|
|
for _, f := range files {
|
|
path := filepath.Join(dir, f)
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte("# Test\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return &DocsHandler{docsFS: os.DirFS(dir), docsPath: dir}
|
|
}
|
|
|
|
func TestBuildNavigation(t *testing.T) {
|
|
h := buildTestDocs(t)
|
|
|
|
// Current page: a device setup guide → User Guide section and its
|
|
// Devices group must render expanded; everything else collapsed.
|
|
nav := h.BuildNavigation("/docs/user/devices/kobo-setup")
|
|
if len(nav.Sections) == 0 {
|
|
t.Fatal("no sections built")
|
|
}
|
|
|
|
wantOrder := []string{"Getting Started", "User Guide", "API Reference", "Developer", "Operations", "Contributing"}
|
|
for i, want := range wantOrder {
|
|
if nav.Sections[i].Title != want {
|
|
t.Fatalf("section %d = %q, want %q", i, nav.Sections[i].Title, want)
|
|
}
|
|
}
|
|
|
|
getting := nav.Sections[0]
|
|
if !getting.Collapsed {
|
|
t.Error("Getting Started should be collapsed: only sections containing the current page open")
|
|
}
|
|
if len(getting.Items) != 1 || getting.Items[0].Title != "Overview" || getting.Items[0].URL != "/docs" {
|
|
t.Errorf("Getting Started items = %+v, want single Overview → /docs", getting.Items)
|
|
}
|
|
|
|
userGuide := nav.Sections[1]
|
|
if userGuide.Collapsed {
|
|
t.Error("User Guide should be expanded: it contains the current page")
|
|
}
|
|
if len(userGuide.Items) != 2 || userGuide.Items[0].Title != "User Guide" {
|
|
t.Errorf("User Guide direct items = %+v, want User Guide pinned first", userGuide.Items)
|
|
}
|
|
var devices *templates.NavGroup
|
|
for i := range userGuide.Groups {
|
|
if userGuide.Groups[i].Title == "Devices" {
|
|
devices = &userGuide.Groups[i]
|
|
}
|
|
}
|
|
if devices == nil {
|
|
t.Fatal("User Guide has no Devices group")
|
|
}
|
|
if devices.Collapsed {
|
|
t.Error("Devices group should be expanded: it contains the current page")
|
|
}
|
|
if len(devices.Items) != 2 || devices.Items[0].Title != "Kobo Setup" {
|
|
t.Errorf("Devices items = %+v, want Kobo Setup + KOReader Setup", devices.Items)
|
|
}
|
|
|
|
// Everything else starts collapsed.
|
|
for _, s := range nav.Sections[2:] {
|
|
if !s.Collapsed {
|
|
t.Errorf("section %q should be collapsed (current page is elsewhere)", s.Title)
|
|
}
|
|
}
|
|
|
|
// API Reference: subdirectory groups with the index page as Overview.
|
|
api := nav.Sections[2]
|
|
if len(api.Groups) != 2 {
|
|
t.Fatalf("API Reference groups = %d, want 2 (Authentication, Saved Filters)", len(api.Groups))
|
|
}
|
|
if api.Groups[0].Title != "Authentication" || len(api.Groups[0].Items) != 2 {
|
|
t.Errorf("Authentication group = %+v", api.Groups[0])
|
|
}
|
|
saved := api.Groups[1]
|
|
if saved.Title != "Saved Filters" || len(saved.Items) != 1 || saved.Items[0].Title != "Overview" {
|
|
t.Errorf("Saved Filters group = %+v, want single Overview item", saved)
|
|
}
|
|
if !saved.Collapsed {
|
|
t.Error("Saved Filters group should be collapsed on an unrelated page")
|
|
}
|
|
|
|
// Stray root files land in Developer → Reference.
|
|
dev := nav.Sections[3]
|
|
if len(dev.Groups) != 1 || dev.Groups[0].Title != "Reference" {
|
|
t.Errorf("Developer groups = %+v, want Reference", dev.Groups)
|
|
}
|
|
}
|
|
|
|
func TestBuildNavigationNothingCurrent(t *testing.T) {
|
|
h := buildTestDocs(t)
|
|
nav := h.BuildNavigation("/docs/nonexistent")
|
|
open := 0
|
|
for _, s := range nav.Sections {
|
|
if !s.Collapsed {
|
|
open++
|
|
}
|
|
}
|
|
if open != 0 {
|
|
t.Errorf("expected zero sections open when nothing matches (docs home / 404 renders fully closed), got %d", open)
|
|
}
|
|
}
|