package memoriatest // Gates for the shared chrome (estilo spec/shared-chrome, round 2; Memoria // is its §12.1 row 6). Each was run against origin/main 97b8a23 before the // change and failed there; the discussion quotes both runs. import ( "encoding/base64" "net/http" "net/url" "os" "path/filepath" "regexp" "strconv" "strings" "testing" "golang.org/x/net/html" ) var titleRe = regexp.MustCompile(`(?s)(.*?)`) func pageTitle(body string) string { m := titleRe.FindStringSubmatch(body) if m == nil { return "" } return html.UnescapeString(strings.TrimSpace(m[1])) } func storedID(t *testing.T, h http.Handler) string { t.Helper() m := regexp.MustCompile(`href="/documents/(\d+)"`).FindStringSubmatch(get(t, h, "/").Body.String()) if m == nil { t.Fatal("no stored document on the index") } return m[1] } // §3: an error is a page with the bar, for a browser; plain text for // anything else. Before: Go's "404 page not found" in monospace, and // "could not read that document". func TestHTMLErrorsRenderTheErrorPage(t *testing.T) { dbPath := tempDB(t) in, _ := signedInClient(t, dbPath) h, _ := admissionApp(t, dbPath, []string{chromeOwner}) out := newClient(t, h) cases := []struct { name, path, title string }{ {"unknown route", "/no/such/page", "Page not found · Memoria"}, {"missing document", "/documents/99999", "Document not available · Memoria"}, {"bad parameter", "/documents/not-a-number", "Document not available · Memoria"}, {"GET of a POST-only path", "/connectors/notion/sync", "Page not found · Memoria"}, } for _, tc := range cases { t.Run("signed in, "+tc.name, func(t *testing.T) { rec := in.get(tc.path) body := rec.Body.String() if rec.Code != http.StatusNotFound || !strings.HasPrefix(rec.Header().Get("Content-Type"), "text/html") { t.Fatalf("%s = %d %q, want a 404 page", tc.path, rec.Code, rec.Header().Get("Content-Type")) } if got := pageTitle(body); got != tc.title { t.Errorf("title %q, want %q", got, tc.title) } if !strings.Contains(body, "
stacks its own margins // on the callout's padding (Memoria's double-height callouts). func TestTemplatesUseOnlyCalloutTones(t *testing.T) { dir := filepath.Join("..", "memoria", "templates") entries, err := os.ReadDir(dir) if err != nil { t.Fatal(err) } bad := regexp.MustCompile(`rst-notice|rst-callout="|rst-tone="warn"|

is "{part} · … · Memoria", built by the layout, and // the first part is the page's h1. The titles mixed "—" and "·" before. func TestPageTitlesFollowTheSuitePattern(t *testing.T) { h := newApp(t) store(t, h, "Minutes, 12 September", "We agreed to move the offsite to November.") id := storedID(t, h) for path, want := range map[string]string{ "/": "Memoria", "/?q=offsite": "“offsite” · Memoria", "/add": "Add content · Memoria", "/documents/" + id: "Minutes, 12 September · Memoria", "/documents/" + id + "/delete": "Delete document · Minutes, 12 September · Memoria", "/connectors": "Sources · Memoria", "/connectors/notion": "Notion · Memoria", "/help/open-source": "Open source · Memoria", } { if got := pageTitle(get(t, h, path).Body.String()); got != want { t.Errorf("%s: title %q, want %q", path, got, want) } } so, _ := signinApp(t) for path, want := range map[string]string{ "/signin": "Sign in · Memoria", "/signin?sent=1": "Check your email · Memoria", "/signin?err=expired": "Sign in · Memoria", "/signed-out": "Signed out · Memoria", } { if got := pageTitle(get(t, so, path).Body.String()); got != want { t.Errorf("%s: title %q, want %q", path, got, want) } } } // §8.2: one account menu, the last thing in the bar, with the one Sign // out in the app; Appearance; Help ending in Open source. Before: a Help // dropdown, Sources and a Sign out button in a nav that wrapped. func TestEverySignedInPageHasOneSignOutInTheSameCorner(t *testing.T) { h := newApp(t) store(t, h, "Notes", "hosting migration notes") id := storedID(t, h) for _, path := range []string{"/", "/?q=hosting", "/add", "/documents/" + id, "/documents/" + id + "/delete", "/connectors", "/connectors/notion", "/help/open-source"} { doc := parsePage(t, get(t, h, path).Body.String()) signOuts := findAll(doc, func(n *html.Node) bool { return (n.Data == "button" || n.Data == "a") && strings.TrimSpace(textOf(n)) == "Sign out" }) if len(signOuts) != 1 { t.Errorf("%s: %d Sign outs, want exactly one", path, len(signOuts)) continue } ends := findAll(doc, hasAttr("estilo-bar-end")) if len(ends) != 1 { t.Errorf("%s: %d [estilo-bar-end]", path, len(ends)) continue } var last *html.Node for c := ends[0].FirstChild; c != nil; c = c.NextSibling { if c.Type == html.ElementNode { last = c } } if last == nil || last.Data != "details" { t.Errorf("%s: the bar does not end with the account menu", path) continue } if _, ok := attrOf(last, "estilo-account"); !ok { t.Errorf("%s: the last thing in the bar is not the account menu", path) } if !within(signOuts[0], last) { t.Errorf("%s: Sign out is not in the account menu", path) } account := textOf(last) for _, want := range []string{"Signed in as", memoriaTestOwner(), "Appearance", "Light", "Dark", "System", "Help", "Open source"} { if !strings.Contains(account, want) { t.Errorf("%s: the account menu lacks %q", path, want) } } pressed := findAll(last, func(n *html.Node) bool { v, ok := attrOf(n, "aria-pressed") return ok && (v == "true" || v == "false") }) if len(pressed) != 3 { t.Errorf("%s: %d Appearance options carry aria-pressed, want 3", path, len(pressed)) } if strings.Contains(textOf(doc), "Connect Home account") || len(findAll(doc, hasAttr("rst-dropdown"))) != 0 { t.Errorf("%s: the old header's Help dropdown or \"Connect Home account\" is still there", path) } } } func memoriaTestOwner() string { return "owner@test" } // §8.4: signed out, the corner is a plain "Sign in" back to this page; on // the sign-in pages themselves it is empty. func TestTheSignedOutCorner(t *testing.T) { h, _ := signinApp(t) body := newClient(t, h).get("/no/such/page?x=1").Body.String() doc := parsePage(t, body) signIns := findAll(doc, hasAttr("estilo-bar-signin")) if len(signIns) != 1 { t.Fatalf("%d Sign in links in the bar", len(signIns)) } if href, _ := attrOf(signIns[0], "href"); href != "/signin?return_to=%2Fno%2Fsuch%2Fpage%3Fx%3D1" { t.Errorf("Sign in goes to %q, want back to this page", href) } for _, path := range []string{"/signin", "/signin?sent=1", "/signed-out"} { b := get(t, h, path).Body.String() if !strings.Contains(b, "

`) { t.Error("the page does not carry data-theme=dark") } h, _ := admissionApp(t, tempDB(t), []string{chromeOwner}) out := newClient(t, h) if got := location(t, out.do(http.MethodPost, "/appearance", url.Values{"mode": {"light"}, "return_to": {"//evil.example/"}})); got != "/" { t.Errorf("an off-site return_to went to %q, want /", got) } } // §9.4: a document link opened signed out comes back to that document // after sign-in, through a sealed cookie; "Use a different address" keeps // it; a plain /signin forgets it. func TestASignedOutDeepLinkComesBackAfterSignIn(t *testing.T) { h, mailer := admissionApp(t, tempDB(t), []string{chromeOwner}) c := newClient(t, h) const deep = "/documents/7?x=1" signin := location(t, c.get(deep)) if signin != "/signin?return_to=%2Fdocuments%2F7%3Fx%3D1" { t.Fatalf("a signed-out deep link went to %q", signin) } c.get(signin) var ret *http.Cookie for name, ck := range c.cookies { if strings.Contains(name, "return") { ret = ck } } if ret == nil { t.Fatal("GET /signin?return_to= remembered nothing") } raw, _ := base64.RawURLEncoding.DecodeString(ret.Value) if strings.Contains(ret.Value, "documents") || strings.Contains(string(raw), "documents") || !ret.HttpOnly { t.Error("the return cookie is not sealed (the path reads straight out of it)") } sent := location(t, c.do(http.MethodPost, "/auth/begin", url.Values{"address": {chromeOwner}})) body := c.get(sent).Body.String() if !strings.Contains(body, `href="/signin?return_to=%2Fdocuments%2F7%3Fx%3D1"`) || !strings.Contains(body, "Use a different address") { t.Errorf("\"Use a different address\" does not keep the return:\n%s", body) } u, _ := url.Parse(mailer.link()) dest := location(t, c.openLink(u.RequestURI())) for i := 0; i < 3 && dest != deep && strings.HasPrefix(dest, "/auth/"); i++ { dest = location(t, c.get(dest)) } if dest != deep { t.Errorf("sign-in landed on %q, want %q", dest, deep) } // Tampered, the cookie is ignored: the landing is the index. c2 := newClient(t, h) c2.get("/signin?return_to=%2Fadd") for name, ck := range c2.cookies { if strings.Contains(name, "return") { v := []byte(ck.Value) v[len(v)/2] ^= 1 c2.cookies[name] = &http.Cookie{Name: name, Value: string(v)} } } if got := location(t, c2.get("/auth/landed")); got != "/" { t.Errorf("a tampered return cookie still steered the landing to %q", got) } // And an off-site return_to is not remembered at all. c3 := newClient(t, h) c3.get("/signin?return_to=%2F%2Fevil.example%2F") if got := location(t, c3.get("/auth/landed")); got != "/" { t.Errorf("an off-site return_to steered the landing to %q", got) } } // §9.2: already signed in, /signin skips the form to a safe return_to. And // the trap Astra found in Docs, Sheets and Slides: the admission check // runs FIRST, so a removed owner is signed out and told, once — not // bounced between /signin and a page that refuses them. func TestSigninWhenAlreadySignedInRunsTheAdmissionCheckFirst(t *testing.T) { dbPath := tempDB(t) c, _ := signedInClient(t, dbPath) if got := location(t, c.get("/signin?return_to=%2Fadd")); got != "/add" { t.Errorf("signed in, /signin?return_to=/add went to %q", got) } if got := location(t, c.get("/signin?return_to=%2F%2Fevil.example")); got != "/" { t.Errorf("signed in, an off-site return_to went to %q", got) } // The owner is removed. Same database, same session cookie. h2, _ := admissionApp(t, dbPath, []string{"someone-else@example.test"}) c.h = h2 if got := location(t, c.get("/signin")); got != "/signin?err=removed" { t.Fatalf("a removed owner at /signin went to %q, want /signin?err=removed", got) } rec := c.get("/signin?err=removed") if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "Your access has been removed") { t.Errorf("the removed page = %d, and does not say so", rec.Code) } if !strings.Contains(location(t, c.get("/")), "/signin?return_to=") { t.Error("the removed owner's session survived") } } // And the gate itself: a removed owner meets the same sentence on any // page, instead of a 404 on every page until the session lapses. func TestARemovedOwnerIsToldOnAnyPage(t *testing.T) { dbPath := tempDB(t) c, _ := signedInClient(t, dbPath) h2, _ := admissionApp(t, dbPath, []string{"someone-else@example.test"}) c.h = h2 if got := location(t, c.get("/add")); got != "/signin?err=removed" { t.Errorf("a removed owner at /add went to %q", got) } } // §9.2: Sign out lands on "You're signed out", not a bare sign-in form. func TestSignOutLandsOnTheSignedOutPage(t *testing.T) { c, _ := signedInClient(t, tempDB(t)) dest := location(t, c.do(http.MethodPost, "/signout", url.Values{})) if dest != "/signed-out" { t.Fatalf("Sign out went to %q", dest) } rec := c.get(dest) b := html.UnescapeString(rec.Body.String()) if rec.Code != http.StatusOK || !strings.Contains(b, "You’re signed out") || !strings.Contains(b, "You’ve signed out of Memoria on this device.") { t.Errorf("the signed-out page = %d:\n%s", rec.Code, b) } } // Delete asks first. The index's Delete was a one-click POST with no undo. func TestDeleteAsksFirst(t *testing.T) { h := newApp(t) store(t, h, "Keep me", "a document someone might delete by accident") id := storedID(t, h) index := get(t, h, "/").Body.String() if strings.Contains(index, `action="/documents/`+id+`/delete"`) { t.Error("the index still deletes in one click") } if !strings.Contains(index, `href="/documents/`+id+`/delete"`) { t.Error("the index has no way to ask for a delete") } confirm := get(t, h, "/documents/"+id+"/delete") b := html.UnescapeString(confirm.Body.String()) if confirm.Code != http.StatusOK || !strings.Contains(b, `method="post" action="/documents/`+id+`/delete"`) || !strings.Contains(b, "Keep me") || !strings.Contains(b, ">Cancel<") { t.Fatalf("the confirmation page = %d:\n%s", confirm.Code, b) } if storedCount(t, h) != 1 { t.Fatal("asking deleted the document") } post(t, h, "/documents/"+id+"/delete", url.Values{}) if storedCount(t, h) != 0 { t.Error("the confirmed delete did not delete") } } // Live search does not say "0 results…" while the person is still typing. // The zero state is one quiet line, marked for search.js to hold back // until a pause; a submitted search shows it at once. func TestNoZeroResultsWhileTyping(t *testing.T) { h := newApp(t) store(t, h, "Revenue", "revenue grows next year") frag := get(t, h, "/search?q=reve").Body.String() if strings.Contains(frag, "0 results") || !strings.Contains(frag, "rst-search-empty") || strings.Contains(frag, "rst-callout") { t.Errorf("the live zero state is not one marked quiet line:\n%s", frag) } js, err := os.ReadFile(filepath.Join("..", "memoria", "static", "search.js")) if err != nil { t.Fatal(err) } css, err := os.ReadFile(filepath.Join("..", "memoria", "static", "memoria.css")) if err != nil { t.Fatal(err) } if !strings.Contains(string(js), `setAttribute("rst-search-pending"`) || !strings.Contains(string(js), `removeAttribute("rst-search-pending")`) { t.Error("search.js never holds a live zero state back") } if !regexp.MustCompile(`\[rst-search-pending\] \[rst-search-empty\] \{[^}]*visibility: hidden`).Match(css) { t.Error("memoria.css does not hide a held zero state") } } // The layout: charset first, then the suite's stylesheet order (§0.5, // §12.1 item 2): tokens → theme → brand → base → menu → chrome → app. func TestTheLayoutLinksTheSharedChromeInOrder(t *testing.T) { body := get(t, newApp(t), "/").Body.String() head := body[strings.Index(body, "")+len(""):] if !strings.HasPrefix(strings.TrimSpace(head), ``) { t.Error(" is not the first thing in ") } last := -1 for _, name := range []string{"tokens", "theme", "brand/brand", "base", "menu", "chrome", "memoria"} { m := regexp.MustCompile(`href="/static/` + name + `\.[^"]*css"`).FindStringIndex(body) if m == nil { t.Errorf("the layout does not link %s.css", name) continue } if m[0] < last { t.Errorf("%s.css is linked out of the suite's order", name) } last = m[0] } } // Nineteen template sites wrote rst-cell-mut as an attribute, which styles // nothing (it is a class-only utility): muted text rendered at body colour. func TestMutedTextIsStyled(t *testing.T) { dir := filepath.Join("..", "memoria", "templates") entries, _ := os.ReadDir(dir) for _, e := range entries { raw, _ := os.ReadFile(filepath.Join(dir, e.Name())) if n := regexp.MustCompile(`<[^>]*\srst-cell-mut[\s>]`).FindAll(raw, -1); len(n) > 0 { t.Errorf("%s writes rst-cell-mut as an attribute %d times", e.Name(), len(n)) } } css, _ := os.ReadFile(filepath.Join("..", "memoria", "static", "memoria.css")) if !regexp.MustCompile(`\[rst-muted\] \{ color: var\(--rst-text-muted\); \}`).Match(css) { t.Error("memoria.css does not style [rst-muted]") } if !strings.Contains(get(t, newApp(t), "/").Body.String(), "

") { t.Error("the footer is not muted") } } // Fields fit the column: width 100% plus padding overflowed by 22px at // 1440 and off screen at 390 (content-box); password fields were the // browser's own; the search input drew a second box inside [rst-search]. func TestFormFieldsFitTheColumn(t *testing.T) { css, err := os.ReadFile(filepath.Join("..", "memoria", "static", "memoria.css")) if err != nil { t.Fatal(err) } rule := regexp.MustCompile(`(?s)\[rst-stack\] input\[type="text"\],[^{]*\{[^}]*\}`).Find(css) if rule == nil { t.Fatal("no [rst-stack] field rule") } for _, want := range []string{`input[type="password"]`, "box-sizing: border-box"} { if !strings.Contains(string(rule), want) { t.Errorf("the field rule lacks %q", want) } } if regexp.MustCompile(`\[rst-search\] input\[type="search"\] \{`).Match(css) { t.Error("memoria.css still draws its own box on the search input, inside rastrillo's") } if !strings.Contains(string(css), "::file-selector-button") { t.Error("the file chooser's button is the browser's own") } } // The Sources lists are rows, not a card of bare bulleted

  • s. func TestSourcesAreRows(t *testing.T) { b := get(t, newApp(t), "/connectors").Body.String() if strings.Contains(b, "rst-list-item") || strings.Contains(b, "
      ") { t.Error("the Sources page still puts bare list items in a card") } if !strings.Contains(b, "
        ") { t.Error("the Sources page has no rows") } } var _ = strconv.Itoa // Estilo's chrome is tested against rastrillo v0.23's tokens in estilo; // this is the same check against the tokens.css and theme.css THIS app // serves (spec §12.1 item 1): a var(--rst-*) nothing defines makes its // whole declaration invalid, and the rule vanishes with no error. func TestEstilosChromeUsesOnlyDefinedTokens(t *testing.T) { static := filepath.Join("..", "memoria", "static") defined := map[string]bool{} def := regexp.MustCompile(`(?:^|[;{,\s])(--(?:rst|estilo)-[a-zA-Z0-9_-]+)\s*:`) for _, name := range []string{"tokens.css", "theme.css", "base.css", "menu.css", "chrome.css"} { raw, err := os.ReadFile(filepath.Join(static, name)) if err != nil { t.Fatal(err) } for _, m := range def.FindAllStringSubmatch(string(raw), -1) { defined[m[1]] = true } } if !defined["--rst-accent"] || !defined["--estilo-bar-block-size"] { t.Fatal("the definition pattern found neither --rst-accent nor --estilo-bar-block-size; this gate is measuring nothing") } use := regexp.MustCompile(`var\(\s*(--(?:rst|estilo)-[a-zA-Z0-9_-]+)\s*([,)])`) seen := 0 for _, name := range []string{"base.css", "menu.css", "chrome.css"} { raw, err := os.ReadFile(filepath.Join(static, name)) if err != nil { t.Fatal(err) } for _, m := range use.FindAllStringSubmatch(string(raw), -1) { seen++ if m[2] == ")" && !defined[m[1]] { t.Errorf("%s reads var(%s), which neither this app's tokens nor estilo define", name, m[1]) } } } if seen < 20 { t.Fatalf("only %d token references in estilo's chrome; the walk is broken", seen) } } // polish audit r3 B7 and C5: the Title hint read as bold label text, and // /add put a section rule between "← Back to search" and its h1, where the // document page uses the breadcrumb nav. func TestTheAddPageReadsLikeTheDocumentPage(t *testing.T) { b := get(t, newApp(t), "/add").Body.String() if !strings.Contains(b, ``) { t.Error("the Title hint is not a quiet \"(optional)\"") } if !strings.Contains(b, ``) { t.Error("/add's back link is not the breadcrumb the document page uses") } if regexp.MustCompile(`(?s)
        \s*

        Add content

        `).MatchString(b) { t.Error("a section rule still sits between the back link and the h1") } css, _ := os.ReadFile(filepath.Join("..", "memoria", "static", "memoria.css")) if !regexp.MustCompile(`\[rst-hint\] \{[^}]*font-weight: 400`).Match(css) { t.Error("the hint inherits the label's weight") } }