Files
go-nc-exapp/navigation_test.go
T
konradandCursor d4c18e63bb Load the Nextcloud theme in App navigation and name its core icons.
The shell paints the instance wallpaper by default, and ExApps pick menu icons from NextcloudIcons instead of raw paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-28 20:12:02 +02:00

204 lines
5.7 KiB
Go

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 TestAppNavigationLoadsNextcloudTheme(t *testing.T) {
body := navBody(t, "/")
for _, need := range []string{
"/apps/theming/css/default.css",
"/index.php/apps/theming/theme/default.css",
"/index.php/apps/theming/theme/dark.css",
} {
if !strings.Contains(body, need) {
t.Fatalf("missing %s in %s", need, body)
}
}
}
func TestAppNavigationThemeCanBeDisabled(t *testing.T) {
nav := sampleNavigation()
nav.DisableTheme = true
rec := httptest.NewRecorder()
nav.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
if strings.Contains(rec.Body.String(), "/apps/theming/css/default.css") {
t.Fatalf("theme stylesheet still present: %s", rec.Body.String())
}
}
func TestAppNavigationFoldControlFollowsLabel(t *testing.T) {
body := navBody(t, "/?item=domains")
name := strings.Index(body, ">Domains<")
fold := strings.Index(body, `aria-label="Fold Domains"`)
if name < 0 || fold < 0 || fold < name {
t.Fatalf("name %d fold %d", name, fold)
}
}
func TestAppNavigationRendersIconBeforeLabel(t *testing.T) {
nav := sampleNavigation()
nav.Items = func(*http.Request) []gonexapp.Item {
return []gonexapp.Item{{ID: "domains", Label: "Domains", Icon: gonexapp.NextcloudIcons.Folder}}
}
rec := httptest.NewRecorder()
nav.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/?item=domains", nil))
body := rec.Body.String()
icon := strings.Index(body, `src="/core/img/filetypes/folder.svg"`)
name := strings.Index(body, ">Domains<")
if icon < 0 || name < 0 || icon > name {
t.Fatalf("icon %d name %d", icon, name)
}
}
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)
}
}