oficina / home Public

Test: Home's pages on the shared chrome (red)

shared_chrome_test.go states round 2's Home row (estilo spec
shared-chrome §12.1 row 8) and the r3 audit's Home findings, and fails on
this tree:

- TestHomePagesWearTheSharedBar: every page but the front page sits under
  estilo's bar, branded Oficina. On the directory a signed-in person has
  one account menu (Passkeys and Recovery codes on id.*, Appearance, Help
  > Open source, exactly one Sign out); signed out, the corner is Sign
  in. id.* pages are the door with a brand-only bar. The sky only where
  Calma puts it: the door, the team home header, the front page.
- TestFrontPageKeepsItsOwnDesign: no app bar; the title is "Oficina".
- TestAppearanceSetsTheTheme: /appearance sets the theme.
- TestErrorPagesSplitByAccept: estilo's 404 page on both origins for a
  browser, plain text otherwise (it was text/plain for everyone).
- TestStartingAgainKeepsTheDestination: r3 C1.
- TestHopKeepsTheSkyPlaceholder: r3 C2.
- TestColdLoadPreloadsTheFontAndMarks: r3 C4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev pushed by paul@keymail.dev 535d41c08773e7100df2d9062ef9d58a6acf8e0f parent b2f937b
1 file changed, +302 −0
  • internal/home/shared_chrome_test.go +302 −0
diff --git a/internal/home/shared_chrome_test.go b/internal/home/shared_chrome_test.go
new file mode 100644
index 0000000..644f54f
--- /dev/null
+++ b/internal/home/shared_chrome_test.go
@@ -0,0 +1,302 @@
+package home
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "regexp"
+ "strings"
+ "testing"
+
+ "amadan.net/oficina/home/internal/home/sealtest"
+)
+
+// The shared chrome (estilo spec shared-chrome, §12.1 row 8, Home): every
+// page Home draws for a person, other than the public front page, sits
+// under estilo's 52px bar with the brand "Oficina". On the directory, a
+// signed-in person has one account menu (who, Passkeys and Recovery codes
+// on id.*, Appearance, Help → Open source, one Sign out); signed out, the
+// corner is "Sign in". id.* pages are the door: the bar carries the brand
+// only, linking back to the directory.
+
+// chromeGet is a GET with an Accept header (empty sends none).
+func chromeGet(h http.Handler, target, accept string, cookies ...*http.Cookie) *httptest.ResponseRecorder {
+ r := httptest.NewRequest(http.MethodGet, target, nil)
+ if accept != "" {
+ r.Header.Set("Accept", accept)
+ }
+ for _, c := range cookies {
+ if c != nil {
+ r.AddCookie(c)
+ }
+ }
+ rec := httptest.NewRecorder()
+ h.ServeHTTP(rec, r)
+ return rec
+}
+
+var pageTitle = regexp.MustCompile(`<title>([^<]*)</title>`)
+
+func titleOf(body string) string {
+ m := pageTitle.FindStringSubmatch(body)
+ if m == nil {
+ return ""
+ }
+ return m[1]
+}
+
+const acceptHTML = "text/html,application/xhtml+xml"
+
+func TestHomePagesWearTheSharedBar(t *testing.T) {
+ f := newMountedChooser(t)
+ directory := f.p.directory.Router()
+ credential := f.p.credential.Router()
+ person := f.session(t, chooserPerson)
+ idSession := signInFully(t, f.p.credential, sealtest.CanaryAddress)
+
+ type want struct {
+ title string
+ account bool // the account menu, and with it exactly one Sign out
+ signIn bool // the signed-out corner's "Sign in"
+ sky bool
+ brand string // the brand link's href
+ }
+ cases := []struct {
+ name string
+ h http.Handler
+ target string
+ cookie *http.Cookie
+ want want
+ }{
+ {"directory", directory, directoryOrigin + "/", person, want{"Oficina", true, false, true, "/"}},
+ {"team chooser", directory, directoryOrigin + docsEntryPath, person, want{"No teams available · Oficina", true, false, false, "/"}},
+ {"open source, signed in", directory, directoryOrigin + "/help/open-source", person, want{"Open source · Oficina", true, false, false, "/"}},
+ {"open source, signed out", directory, directoryOrigin + "/help/open-source", nil, want{"Open source · Oficina", false, true, false, "/"}},
+ {"signed out", directory, directoryOrigin + "/signed-out", nil, want{"Signed out · Oficina", false, false, true, "/"}},
+ {"id.* sign in", credential, Origin + "/signin", nil, want{"Sign in · Oficina", false, false, true, directoryOrigin + "/"}},
+ {"id.* check your email", credential, Origin + "/signin/sent", nil, want{"Check your email · Oficina", false, false, true, directoryOrigin + "/"}},
+ {"id.* recovery codes", credential, Origin + "/passkey/codes", idSession, want{"Recovery codes · Oficina", false, false, true, directoryOrigin + "/"}},
+ {"id.* add a passkey", credential, Origin + "/passkey/enrol", idSession, want{"Add a passkey · Oficina", false, false, true, directoryOrigin + "/"}},
+ {"id.* open source", credential, Origin + "/help/open-source", nil, want{"Open source · Oficina", false, false, false, directoryOrigin + "/"}},
+ {"id.* sign out", credential, Origin + "/signout", nil, want{"Finish signing out · Oficina", false, false, true, directoryOrigin + "/"}},
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ rec := chromeGet(c.h, c.target, acceptHTML, c.cookie)
+ body := rec.Body.String()
+ if rec.Code != http.StatusOK {
+ t.Fatalf("status %d", rec.Code)
+ }
+ if got := titleOf(body); got != c.want.title {
+ t.Errorf("title %q, want %q", got, c.want.title)
+ }
+ if strings.Count(body, "<header estilo-bar") != 1 {
+ t.Errorf("want exactly one estilo-bar")
+ }
+ if !strings.Contains(body, `<a estilo-bar-brand href="`+c.want.brand+`">`) || !strings.Contains(body, `<span estilo-bar-name>Oficina</span>`) {
+ t.Errorf("the brand is not \"Oficina\" linking to %s", c.want.brand)
+ }
+ if strings.Contains(body, "rst-dropdown") || strings.Contains(body, "home-hub-head><h1 estilo-brand") {
+ t.Errorf("the old lone Help menu or hub header is still drawn")
+ }
+ if got := strings.Contains(body, "<details estilo-account"); got != c.want.account {
+ t.Errorf("account menu: %v, want %v", got, c.want.account)
+ }
+ signOuts := strings.Count(body, ">Sign out<")
+ if c.want.account {
+ if signOuts != 1 {
+ t.Errorf("%d Sign out buttons, want exactly one", signOuts)
+ }
+ for _, item := range []string{
+ `href="` + Origin + `/passkey/enrol"`,
+ `href="` + Origin + `/passkey/codes"`,
+ `action="/appearance"`,
+ `href="/help/open-source"`,
+ `<form method="post" action="/signout">`,
+ } {
+ if !strings.Contains(body, item) {
+ t.Errorf("the account menu lacks %s", item)
+ }
+ }
+ } else if c.name != "id.* sign out" && signOuts != 0 {
+ t.Errorf("%d Sign out buttons with no account menu", signOuts)
+ }
+ if got := strings.Contains(body, "estilo-bar-signin"); got != c.want.signIn {
+ t.Errorf("Sign in corner: %v, want %v", got, c.want.signIn)
+ }
+ if got := strings.Contains(body, "<div estilo-sky "); got != c.want.sky {
+ t.Errorf("sky: %v, want %v (sign-in, team home header and front page only)", got, c.want.sky)
+ }
+ })
+ }
+}
+
+// The front page keeps its own design: no app bar. Its title is the hub's
+// name alone (spec §10), not "Oficina — open your new office suite".
+func TestFrontPageKeepsItsOwnDesign(t *testing.T) {
+ f := newMountedChooser(t)
+ body := chromeGet(f.p.directory.Router(), directoryOrigin+"/", acceptHTML).Body.String()
+ if strings.Contains(body, "estilo-bar") {
+ t.Error("the public front page has the app bar")
+ }
+ if got := titleOf(body); got != "Oficina" {
+ t.Errorf("front page title %q, want \"Oficina\"", got)
+ }
+}
+
+// Appearance: a signed-in person's choice, kept per host (spec §8.3).
+func TestAppearanceSetsTheTheme(t *testing.T) {
+ f := newMountedChooser(t)
+ person := f.session(t, chooserPerson)
+ rec := f.request("POST", "/appearance", directoryOrigin, url.Values{"mode": {"dark"}, "return_to": {"/"}}.Encode(), person)
+ if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/" {
+ t.Fatalf("POST /appearance: %d %q", rec.Code, rec.Header().Get("Location"))
+ }
+ var mode *http.Cookie
+ for _, c := range rec.Result().Cookies() {
+ if c.Name == "estilo_appearance" {
+ mode = c
+ }
+ }
+ if mode == nil || mode.Value != "dark" {
+ t.Fatalf("no estilo_appearance=dark cookie: %v", rec.Result().Cookies())
+ }
+ body := chromeGet(f.p.directory.Router(), directoryOrigin+"/", acceptHTML, person, mode).Body.String()
+ if !strings.Contains(body, `<html lang="en" data-theme="dark">`) {
+ t.Errorf("the directory does not draw the chosen theme: %.120s", body)
+ }
+ if !strings.Contains(body, `value="dark" aria-pressed="true"`) {
+ t.Error("Dark is not shown as the current appearance")
+ }
+}
+
+// estilo's error page on both origins, for a browser; everything else
+// keeps a plain-text answer (spec §3.3). The 404 used to be chi's bare
+// "404 page not found" whatever was asking.
+func TestErrorPagesSplitByAccept(t *testing.T) {
+ f := newMountedChooser(t)
+ person := f.session(t, chooserPerson)
+ cases := []struct {
+ name string
+ h http.Handler
+ target string
+ cookie *http.Cookie
+ account bool
+ }{
+ {"directory, signed in", f.p.directory.Router(), directoryOrigin + "/no/such/page", person, true},
+ {"directory, signed out", f.p.directory.Router(), directoryOrigin + "/no/such/page", nil, false},
+ {"directory, GET to a POST-only path", f.p.directory.Router(), directoryOrigin + "/appearance", person, true},
+ {"id.*", f.p.credential.Router(), Origin + "/no/such/page", nil, false},
+ {"id.*, GET to a POST-only path", f.p.credential.Router(), Origin + "/passkey/signin/begin", nil, false},
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ rec := chromeGet(c.h, c.target, acceptHTML, c.cookie)
+ body := rec.Body.String()
+ if rec.Code != http.StatusNotFound || !strings.HasPrefix(rec.Header().Get("Content-Type"), "text/html") {
+ t.Fatalf("browser: %d %q, want a 404 page", rec.Code, rec.Header().Get("Content-Type"))
+ }
+ if titleOf(body) != "Page not found · Oficina" || !strings.Contains(body, "<div rst-error>") || !strings.Contains(body, "We can’t find that page") || !strings.Contains(body, "<header estilo-bar") {
+ t.Errorf("not estilo's error page under the bar: %q", titleOf(body))
+ }
+ if got := strings.Contains(body, "<details estilo-account"); got != c.account {
+ t.Errorf("account menu %v, want %v", got, c.account)
+ }
+ if rec.Header().Get("Cache-Control") != "no-store" {
+ t.Errorf("Cache-Control %q", rec.Header().Get("Cache-Control"))
+ }
+ for _, accept := range []string{"", "*/*", "application/json"} {
+ rec := chromeGet(c.h, c.target, accept, c.cookie)
+ if rec.Code != http.StatusNotFound || !strings.HasPrefix(rec.Header().Get("Content-Type"), "text/plain") {
+ t.Errorf("Accept %q: %d %q, want a plain 404", accept, rec.Code, rec.Header().Get("Content-Type"))
+ }
+ }
+ })
+ }
+ // An unknown host is not a page anyone navigates to: still empty.
+ rec := chromeGet(f.p.directory.Router(), "https://elsewhere.example/no/such/page", acceptHTML)
+ if rec.Code != http.StatusNotFound || rec.Body.Len() != 0 {
+ t.Errorf("unknown host: %d %q", rec.Code, rec.Body.String())
+ }
+}
+
+// "Use a different address" and "Start again" keep where you were going
+// (r3 C1). Both were a bare /signin, which forgets return_to: the magic
+// link then ended at id.* "/", which sends you to the signed-out front
+// page, and a sign-in from an app lost the app.
+func TestStartingAgainKeepsTheDestination(t *testing.T) {
+ app := newTestApp(t)
+ h := app.Router()
+ form := chromeGet(h, Origin+"/signin?return_to=%2Fauthorize%2Fresume", acceptHTML)
+ var keep *http.Cookie
+ for _, c := range form.Result().Cookies() {
+ if c.Name == returnCookie && c.Value != "" {
+ keep = c
+ }
+ }
+ if keep == nil {
+ t.Fatal("the form did not remember return_to")
+ }
+ const again = `href="/signin?return_to=%2Fauthorize%2Fresume"`
+ sent := chromeGet(h, Origin+"/signin/sent", acceptHTML, keep).Body.String()
+ if !strings.Contains(sent, again+">Use a different address") {
+ t.Errorf("Use a different address forgets the destination:\n%s", sent)
+ }
+ r := httptest.NewRequest(http.MethodPost, Origin+linkPath, strings.NewReader("token=nope"))
+ r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ r.Header.Set("Origin", Origin)
+ r.AddCookie(keep)
+ rec := httptest.NewRecorder()
+ h.ServeHTTP(rec, r)
+ if !strings.Contains(rec.Body.String(), again+">Start again") {
+ t.Errorf("Start again forgets the destination:\n%s", rec.Body.String())
+ }
+ // With nowhere remembered, both stay a plain /signin.
+ if body := chromeGet(h, Origin+"/signin/sent", acceptHTML).Body.String(); !strings.Contains(body, `href="/signin">Use a different address`) {
+ t.Errorf("with no destination: %s", body)
+ }
+}
+
+// The hand-off hop keeps the sky's inline placeholder (r3 C2): it used to
+// draw neither sky nor card, so every sign-in flashed a blank page between
+// two skies. Only the placeholder: it is a data: URI, so the hop still
+// fetches nothing it will not show.
+func TestHopKeepsTheSkyPlaceholder(t *testing.T) {
+ p := newExchangePair(t)
+ credential := signInFully(t, p.credential, sealtest.CanaryAddress)
+ _, intent := p.start(t)
+ rec := exchangeRequest(p.credential.App, "GET", Origin+"/authorize/resume", "", nil, intent, credential)
+ body := rec.Body.String()
+ if rec.Code != http.StatusOK || !strings.Contains(body, `name="assertion"`) {
+ t.Fatalf("resume: %d", rec.Code)
+ }
+ if !strings.Contains(body, `estilo-sky-layer="placeholder" estilo-sky-tone="light" src="data:`) {
+ t.Error("the hop draws no sky")
+ }
+ if strings.Contains(body, "grain.svg") || strings.Contains(body, "filaments.svg") || strings.Contains(body, "busy.js") {
+ t.Error("the hop fetches sky layers or scripts it will not show")
+ }
+}
+
+// A cold first load paints the mark and Onest at once (r3 C4): the font
+// and the brand pictures are preloaded, on the front page and every page
+// in the layout.
+func TestColdLoadPreloadsTheFontAndMarks(t *testing.T) {
+ f := newMountedChooser(t)
+ font := `<link rel="preload" href="/static/estilo/fonts/v1/onest-latin.woff2" as="font" type="font/woff2" crossorigin>`
+ front := chromeGet(f.p.directory.Router(), directoryOrigin+"/", acceptHTML).Body.String()
+ signin := chromeGet(f.p.credential.Router(), Origin+"/signin", acceptHTML).Body.String()
+ for name, body := range map[string]string{"front page": front, "id.* sign in": signin} {
+ if !strings.Contains(body, font) {
+ t.Errorf("%s: Onest is not preloaded", name)
+ }
+ if !strings.Contains(body, `<link rel="preload" href="/static/brand/icon.svg?v=`) {
+ t.Errorf("%s: the Oficina mark is not preloaded", name)
+ }
+ }
+ for _, app := range []string{"docs", "sheets", "slides", "calendar", "calls", "memoria"} {
+ if !strings.Contains(front, `<link rel="preload" href="/static/brand/`+app+`.svg?v=`) {
+ t.Errorf("front page: %s's mark is not preloaded", app)
+ }
+ }
+}