package server_test
import (
"net/http/httptest"
"strings"
"testing"
"git.coopcloud.tech/wiki-cafe/member-console/internal/server"
)
// renderMemberShell renders a member page through the root template set
// with the given shell, the way the /, /products, and /billing handlers do.
func renderMemberShell(t *testing.T, page string, shell server.Shell) string {
t.Helper()
st := rootTemplates(t)
rec := httptest.NewRecorder()
switch page {
case "index.html":
st.Render(rec, page, dashboardData{Shell: shell})
case "products.html":
st.Render(rec, page, server.ProductsPageData{Shell: shell})
default:
st.Render(rec, page, server.BillingPageData{Shell: shell})
}
if rec.Code != 200 {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
return rec.Body.String()
}
func memberShell(active string, operator bool) server.Shell {
return server.Shell{
Surface: server.ShellMember,
Name: "Alice Admin",
Username: "alice",
Email: "alice@example.test",
KeycloakAccountURL: "https://idp.example.test/account",
IsOperator: operator,
Active: active,
}
}
// TestShellAccountMenu pins chrome-conventions "Session controls live in
// the account menu on both surfaces" and the unchanged "The
// identity-provider handoff is marked subtly" on the member surface: the
// top bar's account menu is the only home of Identity and Access and Sign
// out, in that order around a divider, under a trigger that names the
// person; the handoff keeps its SVG marker nowrap-bound to the label and
// its plain-words tooltip; nothing session-related renders in the rail.
func TestShellAccountMenu(t *testing.T) {
for _, page := range []string{"index.html", "products.html", "billing.html"} {
t.Run(page, func(t *testing.T) {
body := renderMemberShell(t, page, memberShell("dashboard", true))
if got := strings.Count(body, `class="dropdown-menu dropdown-menu-end"`); got != 1 {
t.Fatalf("expected one account menu, got %d", got)
}
if !strings.Contains(body, `data-bs-toggle="dropdown" aria-expanded="false">Alice Admin`) {
t.Error("the account menu's trigger must be a button labelled with the person's display name")
}
if !strings.Contains(body, `
Alice Admin alice@example.test
`) {
t.Error("the menu must open with a non-interactive name-and-email header")
}
// The identity-provider handoff, unchanged in substance: one
// SVG marker glued to the label's last word, one tooltip, no
// ↗ character, no inline explanation copy.
if got := strings.Count(body, `Identity and Access