Add an optional App navigation shell for ExApp pages.

ExApps that mount it share one Files-style tree; an ExApp that never mounts it keeps its own page.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-28 19:31:36 +02:00
co-authored by Cursor
parent 9e2005cfb3
commit 7a915aee7b
5 changed files with 377 additions and 3 deletions
+156
View File
@@ -0,0 +1,156 @@
package gonexapp_test
import (
"html"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"gitea.neitzel.de/konrad/go-nc-exapp"
)
func sampleNavigation() gonexapp.AppNavigation {
items := []gonexapp.Item{
{
ID: "domains", Label: "Domains",
Children: []gonexapp.Item{{ID: "example.com", Label: "example.com"}},
},
{ID: "keys", Label: "Keys"},
}
return gonexapp.AppNavigation{
Items: func(*http.Request) []gonexapp.Item { return items },
Page: func(_ *http.Request, id string) (string, bool) {
switch id {
case "domains":
return "<p>domains-page</p>", true
case "keys":
return "<p>keys-page</p>", true
case "example.com":
return "<p>zone-page</p>", true
default:
return "", false
}
},
Header: func(*http.Request) string { return `<p id="visit">folder-line</p>` },
DefaultID: "domains",
MissingMessage: "that item is not in this Visit",
}
}
func navBody(t *testing.T, rawURL string) string {
t.Helper()
rec := httptest.NewRecorder()
sampleNavigation().Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, rawURL, nil))
if rec.Code != http.StatusOK {
t.Fatalf("GET %s: got %d", rawURL, rec.Code)
}
return rec.Body.String()
}
func navAddress(t *testing.T, body string) url.Values {
t.Helper()
const key = `data-address="`
_, after, ok := strings.Cut(body, key)
if !ok {
t.Fatalf("no data-address in %s", body)
}
rest := after
end := strings.Index(rest, `"`)
if end < 0 {
t.Fatalf("unclosed data-address")
}
raw, err := url.QueryUnescape(html.UnescapeString(rest[:end]))
if err != nil {
t.Fatal(err)
}
q, err := url.ParseQuery(strings.TrimPrefix(raw, "?"))
if err != nil {
t.Fatal(err)
}
return q
}
func TestAppNavigationShowsSelectedPage(t *testing.T) {
body := navBody(t, "/?item=keys")
if !strings.Contains(body, "<p>keys-page</p>") {
t.Fatalf("got %s", body)
}
if !strings.Contains(body, `data-selected="keys"`) {
t.Fatalf("got %s", body)
}
if navAddress(t, body).Get("item") != "keys" {
t.Fatalf("address: %s", navAddress(t, body).Encode())
}
}
func TestAppNavigationDefaultsWhenItemMissingFromAddress(t *testing.T) {
body := navBody(t, "/")
if !strings.Contains(body, "<p>domains-page</p>") {
t.Fatalf("got %s", body)
}
if navAddress(t, body).Get("item") != "domains" {
t.Fatalf("address item = %q", navAddress(t, body).Get("item"))
}
if strings.Contains(body, "that item is not in this Visit") {
t.Fatalf("default open should not show the missing-item message: %s", body)
}
}
func TestAppNavigationMissingItemSelectsDefault(t *testing.T) {
body := navBody(t, "/?folder=share&item=gone")
if !strings.Contains(body, "<p>domains-page</p>") {
t.Fatalf("got %s", body)
}
if !strings.Contains(body, "that item is not in this Visit") {
t.Fatalf("got %s", body)
}
q := navAddress(t, body)
if q.Get("item") != "domains" || q.Get("folder") != "share" {
t.Fatalf("address = %s", q.Encode())
}
}
func TestAppNavigationPageRefusalSelectsDefault(t *testing.T) {
nav := sampleNavigation()
inner := nav.Page
nav.Page = func(r *http.Request, id string) (string, bool) {
if id == "keys" {
return "", false
}
return inner(r, id)
}
rec := httptest.NewRecorder()
nav.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/?folder=share&item=keys", nil))
if rec.Code != http.StatusOK {
t.Fatalf("got %d", rec.Code)
}
body := rec.Body.String()
if !strings.Contains(body, "<p>domains-page</p>") || !strings.Contains(body, "that item is not in this Visit") {
t.Fatalf("got %s", body)
}
q := navAddress(t, body)
if q.Get("item") != "domains" || q.Get("folder") != "share" {
t.Fatalf("address = %s", q.Encode())
}
}
func TestAppNavigationChildExpandsParents(t *testing.T) {
body := navBody(t, "/?item=example.com")
if !strings.Contains(body, "<p>zone-page</p>") {
t.Fatalf("got %s", body)
}
if !strings.Contains(body, `data-id="domains" data-expanded="true"`) {
t.Fatalf("got %s", body)
}
}
func TestAppNavigationHeaderPrecedesTree(t *testing.T) {
body := navBody(t, "/?item=domains")
header := strings.Index(body, "folder-line")
tree := strings.Index(body, ">Domains<")
if header < 0 || tree < 0 || header > tree {
t.Fatalf("header %d tree %d in %s", header, tree, body)
}
}