Files
member-console/internal/server/operator_sidebar_render_test.go
cgalo5758 257955c9d3 Add operator list-scale contract and People directory
Governed operator lists (organizations, grants, people, billing×4) gain
server-side search, status filters, and 50-row pages with true totals
from count(*) OVER(); state is URL-addressable, out-of-range pages
clamp,
and no-match is distinct from true-empty.

People is the eighth flat sidebar entry: /operator/persons lists persons
newest-joined first (excluding the reserved system person), rows linking
to the existing detail.

Billing gains an operator invoice detail at
/operator/billing/invoices/{invoiceID} reusing the member projection;
open invoices past due present as Overdue (derived, filterable, stored
status untouched); all four views lead with the linked organization and
mute object IDs.

Grants filter over the derived Live/Superseded/Inactive state, the SQL
HAVING predicate pinned to the Go derivation by test. Embedded lists
(org composite ledger, Tier changes) adopt the shared controls under
namespaced params with sibling-state-preserving URLs and scoped htmx
swaps that hold the viewport.

Review corrections: blocked ladder Delete renders disabled with tooltip
and mutations fire toasts; collapse triggers paint their open state;
sections use outside headings; plan topology drops the orphan-product
check; domains policy collapses behind a disclosure.
2026-08-24 03:58:18 -05:00

132 lines
5.1 KiB
Go

package server
import (
"regexp"
"strings"
"testing"
)
// sidebarRegion slices the rendered operator.html document down to the
// sidebar <aside>, so link-text assertions can't accidentally match the
// cross-app top navbar (which repeats some of the same words, e.g.
// "Products") or the main content.
func sidebarRegion(t *testing.T, out string) string {
t.Helper()
const open = `<aside class="app-sidebar operator-ia-sidebar`
start := strings.Index(out, open)
if start < 0 {
t.Fatalf("rendered output has no operator sidebar")
}
rest := out[start:]
end := strings.Index(rest, "</aside>")
if end < 0 {
t.Fatalf("rendered output sidebar has no closing </aside>")
}
return rest[:end]
}
// sidebarLinkActive reports whether the sidebar <a> whose text is exactly
// linkText carries the "active" class. Fails the test if no such link is
// found in the sidebar region.
func sidebarLinkActive(t *testing.T, sidebar, linkText string) bool {
t.Helper()
re := regexp.MustCompile(`<a class="nav-link([^"]*)"[^>]*>` + regexp.QuoteMeta(linkText) + `</a>`)
m := re.FindStringSubmatch(sidebar)
if m == nil {
t.Fatalf("sidebar link %q not found in:\n%s", linkText, sidebar)
}
return strings.Contains(m[1], "active")
}
// TestOperatorSidebarFlatTopLevelOrder covers operator-panel-navigation
// ("No rendered sidebar groups", "The sidebar is a flat list of section
// entries"): exactly the eight top-level entries Overview, People,
// Organizations, Grants, Billing, Products, Domains, Integrations in that
// order, with no children, no indentation, and no hairline group-separator
// markup (maintainer 2026-08-23: second-level surfaces are reached
// in-page, not from the sidebar; People joined via ux-operator-scale).
func TestOperatorSidebarFlatTopLevelOrder(t *testing.T) {
out := sidebarRegion(t, renderOperator(t, OperatorPageData{CSRFToken: "csrf"}))
topLevel := []string{">Overview<", ">People<", ">Organizations<", ">Grants<", ">Billing<", ">Products<", ">Domains<", ">Integrations<"}
positions := make([]int, len(topLevel))
for i, want := range topLevel {
idx := strings.Index(out, want)
if idx < 0 {
t.Fatalf("sidebar missing top-level entry %q in:\n%s", want, out)
}
positions[i] = idx
}
for i := 1; i < len(positions); i++ {
if positions[i] <= positions[i-1] {
t.Errorf("sidebar top-level entries out of order: %q (at %d) did not follow %q (at %d)",
topLevel[i], positions[i], topLevel[i-1], positions[i-1])
}
}
if got := strings.Count(out, "nav-link"); got != len(topLevel) {
t.Errorf("sidebar must hold exactly the %d top-level entries, found %d nav-link occurrences in:\n%s", len(topLevel), got, out)
}
for _, banned := range []string{
">Org Types<",
">Accounts<", ">Subscriptions<", ">Invoices<", ">Payments<",
">Entitlement Sets<", ">Plan Ladders<", ">Plan Topology<",
"ps-4",
"operator-ia-group-start",
} {
if strings.Contains(out, banned) {
t.Errorf("sidebar must not contain %q (children and group markup are retired), got:\n%s", banned, out)
}
}
}
// TestOperatorSidebarSecondLevelMarksSection covers operator-panel-navigation
// ("A second-level page marks its section's sidebar entry active"): a page
// reached from within a section (Org Types, the billing views, Entitlement
// Sets, Plan Ladders) lights up that section's single sidebar entry, and no
// other.
func TestOperatorSidebarSecondLevelMarksSection(t *testing.T) {
cases := []struct {
name string
activeCapability string
sectionText string
}{
{"person-detail", "persons", "People"},
{"org-types", "org-types", "Organizations"},
{"entitlement-sets", "entitlement-sets", "Products"},
{"plan-ladders", "plan-ladders", "Products"},
{"billing-accounts", "billing-accounts", "Billing"},
{"billing-invoices", "billing-invoices", "Billing"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
out := sidebarRegion(t, renderOperator(t, OperatorPageData{
CSRFToken: "csrf",
ActiveCapability: tc.activeCapability,
}))
if !sidebarLinkActive(t, out, tc.sectionText) {
t.Errorf("section entry %q must be marked active for capability %q, sidebar:\n%s", tc.sectionText, tc.activeCapability, out)
}
// A sibling section must not light up for another section's page.
if tc.sectionText != "Organizations" && sidebarLinkActive(t, out, "Organizations") {
t.Errorf("unrelated section Organizations must not be marked active for %q, sidebar:\n%s", tc.activeCapability, out)
}
})
}
}
// TestOperatorSidebarOverviewOnlyActiveOnLanding guards against the
// section active-marking logic accidentally lighting up every top-level
// entry (an `or` chain with a missing `eq` degenerating to always-true).
func TestOperatorSidebarOverviewOnlyActiveOnLanding(t *testing.T) {
out := sidebarRegion(t, renderOperator(t, OperatorPageData{CSRFToken: "csrf"}))
for _, text := range []string{"People", "Organizations", "Grants", "Billing", "Products", "Domains", "Integrations"} {
if sidebarLinkActive(t, out, text) {
t.Errorf("%q must not be active on the landing surface, sidebar:\n%s", text, out)
}
}
if !sidebarLinkActive(t, out, "Overview") {
t.Errorf("Overview must be active on the landing surface, sidebar:\n%s", out)
}
}