package gonexapp_test import ( "html" "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/chromedp/chromedp" "github.com/chromedp/chromedp/kb" "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 "

domains-page

", true case "keys": return "

keys-page

", true case "example.com": return "

zone-page

", true default: return "", false } }, Header: func(*http.Request) string { return `

folder-line

` }, 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, "

keys-page

") { 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, "

domains-page

") { 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, "

domains-page

") { 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, "

domains-page

") || !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, "

zone-page

") { 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) } } func TestAppNavigationOffersNarrowScreenToggle(t *testing.T) { body := navBody(t, "/") for _, need := range []string{ ``, `id="nav-toggle"`, `aria-controls="app-navigation"`, `aria-expanded="false"`, `aria-label="Open navigation"`, `id="nav-backdrop"`, `id="app-navigation"`, `max-width: 1024px`, } { if !strings.Contains(body, need) { t.Errorf("missing %s", need) } } } // navLayoutJS reports how App navigation is laid out: "wide", "closed", or "open". const navLayoutJS = `(() => { const nav = document.querySelector("nav.app-navigation"); const btn = document.getElementById("nav-toggle"); const narrow = window.matchMedia("(max-width: 1024px)").matches; const box = nav.getBoundingClientRect(); const navShown = getComputedStyle(nav).display !== "none" && box.width > 0 && box.left < window.innerWidth && box.right > 0; const btnShown = getComputedStyle(btn).display !== "none"; const expanded = btn.getAttribute("aria-expanded") === "true"; if (!narrow && navShown && !btnShown && !expanded) return "wide"; if (narrow && !navShown && btnShown && !expanded) return "closed"; if (narrow && navShown && btnShown && expanded) return "open"; return "narrow=" + narrow + " nav=" + navShown + " btn=" + btnShown + " expanded=" + expanded + " width=" + window.innerWidth; })()` func TestAppNavigationWideScreenKeepsTheTreeVisible(t *testing.T) { drive(t, "/nav", chromedp.EmulateViewport(1280, 800), waitJS(navLayoutJS, "wide", false), ) } func TestAppNavigationNarrowScreenOpensTheTreeOnDemand(t *testing.T) { drive(t, "/nav", chromedp.EmulateViewport(390, 800), waitJS(navLayoutJS, "closed", false), chromedp.Click("#nav-toggle", chromedp.ByQuery), waitJS(navLayoutJS, "open", false), chromedp.Click("#nav-toggle", chromedp.ByQuery), waitJS(navLayoutJS, "closed", false), chromedp.Click("#nav-toggle", chromedp.ByQuery), waitJS(navLayoutJS, "open", false), chromedp.MouseClickXY(370, 400), waitJS(navLayoutJS, "closed", false), chromedp.Click("#nav-toggle", chromedp.ByQuery), waitJS(navLayoutJS, "open", false), chromedp.KeyEvent(kb.Escape), waitJS(navLayoutJS, "closed", false), chromedp.Click("#nav-toggle", chromedp.ByQuery), waitJS(navLayoutJS, "open", false), chromedp.Click(`a.app-navigation-entry-link[href*="item=keys"]`, chromedp.ByQuery), waitJS(`document.querySelector("main").textContent`, "keys-page", true), waitJS(navLayoutJS, "closed", false), ) }