package gallery import ( "html/template" "regexp" "strings" "testing" "amadan.net/rastrillo/fiesta" ) // The demo application and the modal route as the render writes them: // four documents in the sidebar shell Fiesta ships, the modal at an // address of its own, the Overview framing the demo, and nothing else // in the tree laying a modal over the page it is on. The drives in // demo_browser_test.go take over where the engine has to answer. // demoFiles is the demo's four documents, the index first, keyed by // the view each shows. var demoFiles = map[string]string{ "index": "demo.html", "dashboard": "demo-dashboard.html", "requests": "demo-requests.html", "request": "demo-request.html", } // demoScripts are the scripts a demo document loads, in order: frame.js // first, as every document the gallery frames, then the four the spec // names, which are the layout's own minus the ones the demo has no // control for. var demoScripts = []string{"frame.js", "js/fiesta.js", "js/light.js", "js/anchor.js", "js/shell.js"} // TestTheDemoIsFourDocumentsInTheShippedShell: in every theme and // locale the demo application is four documents, each the sidebar // shell the layout examples ship (the index on demo.html, a content // page on the other three), with the brand Harbour going to the // demo's own index, a rail of two links with the current one marked // for the view, the way up to the index at the row just left, Ada // Lovelace at the foot, the language menu going to the same document // in each of the twelve locales with this one marked, the view's own // section and no other, a way back to the design system, the scripts // the spec names and no others, and no link out of the gallery's own: // every href in the body is the gallery's or a fragment. func TestTheDemoIsFourDocumentsInTheShippedShell(t *testing.T) { const mount = "/x" files := render(t, mount) s := newTestSite(t, fiesta.FS(), mount) attr := func(n node, name string) string { v, _ := n.attr(name); return v } index := parse(t, files["day/en/frames/layout-sidebar/default.html"]) page := parse(t, files["day/en/frames/layout-sidebar-page/default.html"]) read := 0 for _, dir := range dirs() { theme, locale, _ := strings.Cut(strings.TrimSuffix(dir, "/"), "/") for view, file := range demoFiles { name := dir + file body, ok := files[name] if !ok { t.Errorf("the render wrote no %s", name) continue } read++ doc := parse(t, body) // The shell, and its skeleton the layout's own. wantShell, layout := "page", page if view == "index" { wantShell, layout = "index", index } if m := shellRoot.FindSubmatch(body); m == nil || string(m[1]) != wantShell { t.Errorf("%s does not open the sidebar shell as %q", name, wantShell) } if got, want := shellSkeleton(doc), shellSkeleton(layout); !sameKeys(got, want) { t.Errorf("%s: the shell's skeleton %v is not the layout's %v", name, keysOf(got), keysOf(want)) } // The brand, both copies. brands := doc.find(byAttr("rst-shell-brand", "")) if len(brands) == 0 { t.Errorf("%s has no brand", name) } for _, b := range brands { if b.text() != "Harbour" || attr(b, "href") != s.href(theme, locale, "demo.html") || !hasAttr(b, "data-gallery-nav") { t.Errorf("%s: the brand reads %q and goes to %q, want Harbour to the demo's index", name, b.text(), attr(b, "href")) } } // The rail: two links, the current one the view's. navs := doc.find(func(n node) bool { return n.n.Data == "a" && within(n, func(p node) bool { return hasAttr(p, "rst-shell-nav") }) }) current := "nav-requests" if view == "index" || view == "dashboard" { current = "nav-dashboard" } if len(navs) != 2 { t.Errorf("%s: the rail has %d links, want Dashboard and Requests", name, len(navs)) } else { for i, want := range []struct{ id, file string }{{"nav-dashboard", "demo-dashboard.html"}, {"nav-requests", "demo-requests.html"}} { a := navs[i] if attr(a, "id") != want.id || attr(a, "href") != s.href(theme, locale, want.file) || !hasAttr(a, "data-gallery-nav") { t.Errorf("%s: rail link %d is id=%q href=%q, want id=%q href=%q", name, i, attr(a, "id"), attr(a, "href"), want.id, s.href(theme, locale, want.file)) } if mark := attr(a, "aria-current") == "page"; mark != (want.id == current) { t.Errorf("%s: %s marked current %v, want %v", name, want.id, mark, want.id == current) } } } // The way up: the index itself on the index, and the row // just left on a content page. up := doc.find(func(n node) bool { return n.n.Data == "a" && attr(n, "rel") == "up" && within(n, func(p node) bool { return hasAttr(p, "rst-shell-back") }) }) wantUp := s.href(theme, locale, "demo.html") if view != "index" { wantUp += "#" + current } if len(up) != 1 || attr(up[0], "href") != wantUp || !hasAttr(up[0], "data-gallery-nav") { t.Errorf("%s: %d ways up, want one to %s", name, len(up), wantUp) } // The person at the foot. for _, p := range doc.find(byAttr("rst-person-name", "")) { if p.text() != "Ada Lovelace" { t.Errorf("%s: the profile names %q, want Ada Lovelace", name, p.text()) } } if strings.Contains(string(body), "Grace Hopper") || strings.Contains(string(body), "Party planner") { t.Errorf("%s still carries the layout's own person or brand", name) } // The language menu: the same document in every locale. langs := doc.find(func(n node) bool { return n.n.Data == "a" && within(n, func(p node) bool { return hasAttr(p, "rst-locale") }) }) if len(langs) != len(Locales) { t.Errorf("%s: %d language links, want %d", name, len(langs), len(Locales)) } for _, a := range langs { lang := attr(a, "lang") if attr(a, "href") != s.href(theme, lang, file) || !hasAttr(a, "data-gallery-nav") { t.Errorf("%s: the %s link goes to %q, want %q", name, lang, attr(a, "href"), s.href(theme, lang, file)) } if mark := attr(a, "aria-current") == "true"; mark != (lang == locale) { t.Errorf("%s: the %s link marked current %v", name, lang, mark) } } // The view, alone. wantView := "view-" + view if view == "index" { wantView = "view-dashboard" } views := doc.find(byClass("app-view")) if len(views) != 1 || attr(views[0], "id") != wantView { var ids []string for _, v := range views { ids = append(ids, attr(v, "id")) } t.Errorf("%s shows the views %v, want exactly %s", name, ids, wantView) } // The way back to the design system. back := s.say(locale, "gallery.back") found := false for _, a := range bodyLinks(t, doc) { if a.text() == back && attr(a, "href") == s.href(theme, locale, "index.html") && hasAttr(a, "data-gallery-nav") { found = true } } if !found { t.Errorf("%s has no %q link to the Overview", name, back) } // The scripts. var scripts []string for _, sc := range doc.find(func(n node) bool { return n.n.Data == "script" }) { scripts = append(scripts, attr(sc, "src")) } var want []string for _, sc := range demoScripts { want = append(want, s.asset(sc)) } if strings.Join(scripts, " ") != strings.Join(want, " ") { t.Errorf("%s loads %v, want %v", name, scripts, want) } // Nothing leaves the gallery. for _, a := range bodyLinks(t, doc) { h := attr(a, "href") if !strings.HasPrefix(h, "#") && !(hasAttr(a, "data-gallery-nav") && strings.HasPrefix(h, mount+"/")) { t.Errorf("%s: %q goes to %q, neither the gallery's nor a fragment", name, a.text(), h) } } } } if read != len(dirs())*len(demoFiles) { t.Fatalf("read %d demo documents, want %d", read, len(dirs())*len(demoFiles)) } } // sameKeys says whether two sets hold the same keys. func sameKeys(a, b map[string]bool) bool { if len(a) != len(b) { return false } for k := range a { if !b[k] { return false } } return true } // keysOf is a set's keys, for a message. func keysOf(m map[string]bool) []string { var out []string for k := range m { out = append(out, k) } return out } // liveOverlay is a modal laid out live: the overlay or the backdrop as // an element, never as escaped source in a code block. var liveOverlay = regexp.MustCompile(`
`) { t.Errorf("%smodal.html does not show the overlay as source", dir) } link := regexp.MustCompile(``) if !link.MatchString(page) { t.Errorf("%smodal.html does not link %s in a new tab", dir, self) } name := dir + "demo-modal.html" body, ok := files[name] if !ok { t.Errorf("the render wrote no %s", name) continue } routes++ doc := parse(t, body) if n := len(liveOverlay.FindAll(body, -1)); n != 2 { t.Errorf("%s lays out %d of the backdrop and the overlay, want both", name, n) } backdrop, ok := doc.first(byAttr("rst-backdrop", "")) if !ok || !hasAttr(backdrop, "inert") { t.Errorf("%s: the backdrop is missing or not inert", name) } else { if h, ok := backdrop.first(func(n node) bool { return n.n.Data == "h1" }); !ok || h.text() != s.say(locale, "gallery.modal.settings") { t.Errorf("%s: the page under the modal has no heading %q", name, s.say(locale, "gallery.modal.settings")) } if !strings.Contains(backdrop.text(), s.say(locale, "gallery.modal.own")) { t.Errorf("%s: the page under the modal does not say %q", name, s.say(locale, "gallery.modal.own")) } } panel, ok := doc.first(byAttr("rst-modal-panel", "")) if !ok { t.Errorf("%s has no panel", name) continue } tabs := panel.find(func(n node) bool { return n.n.Data == "a" && within(n, func(p node) bool { return p.n.Data == "nav" }) }) if len(tabs) != 3 { t.Errorf("%s: the panel has %d tabs, want 3", name, len(tabs)) } for i, a := range tabs { if attr(a, "href") != self || !hasAttr(a, "data-gallery-nav") { t.Errorf("%s: tab %q goes to %q, want the route itself", name, a.text(), attr(a, "href")) } if mark := attr(a, "aria-current") == "page"; mark != (i == 0) { t.Errorf("%s: tab %q marked current %v", name, a.text(), mark) } } closers := panel.find(byAttr("rst-modal-close", "")) if len(closers) != 1 || attr(closers[0], "href") != overview || attr(closers[0], "aria-label") != s.say(locale, "gallery.modal.close") { t.Errorf("%s: Close does not go to the Overview under its name", name) } back := false for _, a := range panel.find(func(n node) bool { return n.n.Data == "a" }) { if a.text() == s.say(locale, "gallery.back") && attr(a, "href") == overview && hasAttr(a, "data-gallery-nav") { back = true } } if !back { t.Errorf("%s: the panel has no %q button to the Overview", name, s.say(locale, "gallery.back")) } for _, id := range []string{"gallery.modal.body", "gallery.modal.nojs", "gallery.modal.inapp"} { if !strings.Contains(panel.text(), squash(s.say(locale, id))) { t.Errorf("%s: the panel does not say %q", name, s.say(locale, id)) } } } if routes != len(dirs()) { t.Fatalf("read %d modal routes, want %d", routes, len(dirs())) } } // TestTheOverviewFramesTheDemo: every Overview frames the demo // application between its intro and its routes: the heading and its // lead, one widget at the page width with Desktop and Mobile and no // HTML tab (an application is not a snippet to paste), at the demo's // heights, framing its own directory's demo.html, and the link that // opens it in a new tab and says so. func TestTheOverviewFramesTheDemo(t *testing.T) { const mount = "/x" files := render(t, mount) s := newTestSite(t, fiesta.FS(), mount) for _, dir := range dirs() { theme, locale, _ := strings.Cut(strings.TrimSuffix(dir, "/"), "/") name := dir + "index.html" page := string(files[name]) section := regexp.MustCompile(`(?s)
(.*?)
`).FindStringSubmatch(page) if section == nil { t.Errorf("%s frames no demo", name) continue } intro, demo, next := strings.Index(page, `class="ds-intro"`), strings.Index(page, `class="ds-demo"`), strings.Index(page, `class="ds-routes"`) if !(intro >= 0 && intro < demo && demo < next) { t.Errorf("%s: the demo is not between the intro and the routes", name) } body := section[1] for _, id := range []string{"gallery.demo.heading", "gallery.demo.lead"} { if !strings.Contains(body, template.HTMLEscapeString(s.say(locale, id))) { t.Errorf("%s: the demo section does not say %q", name, s.say(locale, id)) } } widgets := widgetsOf(body) if len(widgets) != 1 { t.Errorf("%s: %d widgets in the demo section, want 1", name, len(widgets)) continue } w := widgets[0] if _, ok := previewHeights["demo"]; !ok { t.Fatal("heights.go has no measured height for the demo") } if !strings.Contains(body, `
` + template.HTMLEscapeString(s.say(locale, "gallery.demo.open")) + ` (` + template.HTMLEscapeString(s.say(locale, "gallery.newtab")) + `)` if !strings.Contains(body, open) { t.Errorf("%s: no link opens the demo in a new tab saying so", name) } } }