search: make the navigation form work from every page (AUD-151)
The nav form carried hx-get="/search" hx-target="#search-results", but #search-results exists only on /search. On every other page htmx found no target and silently discarded the response: a search box that looks like it works and does nothing. - SearchForm is now a plain GET with no htmx attributes (navigation and the landing page), so it navigates to /search?q=… from any page and with JavaScript disabled. - SearchFormLive keeps the in-place swap on the search page, where the target does exist. For HX-Request the server now returns the block's *contents* (SearchResultsBody), so the fragment no longer nests a second element with id="search-results" inside the live region. - Empty query shows a prompt instead of an empty list. A zero-match query says "Nothing found" and offers the six areas plus the evidence index. - The result count is role="status" inside the existing aria-live container, so a screen reader reads "4 results for …" rather than a run of titles. Tests (internal/server, internal/site): - the nav form carries no htmx attribute on nine pages, and no built page carries an hx-target whose element is missing (walks all of dist/); - search submitted from /, /edge/ and /platform/ lands on a page with results with no HX-Request header, i.e. the JavaScript-disabled path; - empty query, zero match and the announced count; - a content change followed by a build is visible in dist/search.db. That is the deploy path: deploy/deploy.sh runs `make dist` before it stages a release, and search.Build removes and rewrites the index every build. DoD: search from home, /edge and /platform lands on results; works with JavaScript disabled; zero-match query shows a message; content change followed by a deploy is reflected in search; no htmx request targets a missing element. (cherry picked from commit a56c795768e1e9b10204aae04c3612d7a96775a7)
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"gitea.lab.audasmedia.com.au/sam/ai-resume/internal/content"
|
||||
"gitea.lab.audasmedia.com.au/sam/ai-resume/internal/search"
|
||||
)
|
||||
|
||||
// newSite loads the repository's own content. If this fails, every other test
|
||||
@@ -172,3 +173,93 @@ func TestBuildWritesEveryPublishedPage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// copyTree copies a directory tree. The freshness test needs its own content
|
||||
// tree so it can change a page without touching the repository.
|
||||
func copyTree(src, dst string) error {
|
||||
return filepath.WalkDir(src, func(p string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(src, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target := filepath.Join(dst, rel)
|
||||
if d.IsDir() {
|
||||
return os.MkdirAll(target, 0o755)
|
||||
}
|
||||
raw, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(target, raw, 0o644)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSearchIndexFollowsContent is the AUD-151 freshness line: a content change
|
||||
// followed by a build is reflected in search. The deploy path is exactly this —
|
||||
// deploy/deploy.sh runs `make dist` before it stages a release, and the build
|
||||
// rewrites dist/search.db from content/ every time (search.Build removes the old
|
||||
// file), so search can never serve an index from a previous content tree.
|
||||
func TestSearchIndexFollowsContent(t *testing.T) {
|
||||
root, err := filepath.Abs("../..")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir := t.TempDir()
|
||||
contentDir := filepath.Join(dir, "content")
|
||||
if err := copyTree(filepath.Join(root, "content"), contentDir); err != nil {
|
||||
t.Fatalf("copy content: %v", err)
|
||||
}
|
||||
out := filepath.Join(dir, "dist")
|
||||
|
||||
build := func() {
|
||||
t.Helper()
|
||||
s, err := New(Config{Root: root, ContentDir: contentDir, OutDir: out})
|
||||
if err != nil {
|
||||
t.Fatalf("load site: %v", err)
|
||||
}
|
||||
if err := s.Build(); err != nil {
|
||||
t.Fatalf("build: %v", err)
|
||||
}
|
||||
}
|
||||
query := func(q string) []search.Result {
|
||||
t.Helper()
|
||||
idx, err := search.Open(filepath.Join(out, IndexFile))
|
||||
if err != nil {
|
||||
t.Fatalf("open index: %v", err)
|
||||
}
|
||||
defer idx.Close()
|
||||
results, err := idx.Query(q, 20)
|
||||
if err != nil {
|
||||
t.Fatalf("query %q: %v", q, err)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
const token = "kumquatprotocol"
|
||||
build()
|
||||
if got := query(token); len(got) != 0 {
|
||||
t.Fatalf("token is already indexed in %d results; the test proves nothing", len(got))
|
||||
}
|
||||
|
||||
page := filepath.Join(contentDir, "edge", "edge-fleet.md")
|
||||
raw, err := os.ReadFile(page)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
added := string(raw) + "\n\nA " + token + " harness keeps the nodes in step.\n"
|
||||
if err := os.WriteFile(page, []byte(added), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
build()
|
||||
results := query(token)
|
||||
if len(results) == 0 {
|
||||
t.Fatal("content change is not in the rebuilt index")
|
||||
}
|
||||
if results[0].URL != "/edge/edge-fleet/" {
|
||||
t.Errorf("token found at %s, want /edge/edge-fleet/", results[0].URL)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user