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:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user