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.
132 lines
5.1 KiB
Go
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)
|
|
}
|
|
}
|