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.
99 lines
4.0 KiB
Go
99 lines
4.0 KiB
Go
package server
|
|
|
|
// Render tests for ux-ia-naming 2.2: /operator/plan-ladders opens map-first.
|
|
// operator_plan_ladders.html now includes operator_plan_topology.html as a
|
|
// partial (against PlanLaddersData.Topology) above the existing ladder list
|
|
// and create form. These render against literal view models -- no DB --
|
|
// mirroring the renderPlanLaddersPageBody / renderOperatorPartial pattern
|
|
// used by the sibling *_render_test.go files in this package.
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestPlanLaddersPageOpensMapFirst asserts the merged page renders the
|
|
// topology overview above the existing management surface, and that the
|
|
// management surface (create form + ladder list) still renders in full.
|
|
func TestPlanLaddersPageOpensMapFirst(t *testing.T) {
|
|
data := PlanLaddersData{
|
|
Ladders: []PlanLadderViewModel{
|
|
{PlanLadderID: "l-1", LadderKey: "hosting", Name: "Hosting", TierCount: 1},
|
|
},
|
|
Topology: PlanTopologyData{
|
|
Ladders: []TopologyLadderColumn{
|
|
{PlanLadderID: "l-1", LadderKey: "hosting", Name: "Hosting", IsActive: true},
|
|
},
|
|
Rows: []TopologyRow{
|
|
{Rank: 0, Cells: []TopologyCell{
|
|
{Present: true, ProductID: "p-1", ProductName: "Topology Grid Product", LifecycleStatus: "published", LadderID: "l-1"},
|
|
}},
|
|
},
|
|
},
|
|
}
|
|
body := renderOperatorPartial(t, "operator_plan_ladders.html", data)
|
|
|
|
for _, want := range []string{
|
|
"Plan topology", // topology section heading
|
|
"Topology Grid Product", // grid cell renders from Topology
|
|
"No structural issues detected", // clean-health strip (zero-valued Health)
|
|
`id="createPlanLadderForm"`, // create form still renders
|
|
"hosting", // ladder-list row still renders
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("map-first ladders page missing %q", want)
|
|
}
|
|
}
|
|
|
|
topoIdx := strings.Index(body, "Plan topology")
|
|
formIdx := strings.Index(body, `id="createPlanLadderForm"`)
|
|
if topoIdx == -1 || formIdx == -1 {
|
|
t.Fatalf("expected both the topology section and the create form to render")
|
|
}
|
|
if topoIdx > formIdx {
|
|
t.Errorf("expected the topology overview above the create-ladder form; topology at %d, form at %d", topoIdx, formIdx)
|
|
}
|
|
}
|
|
|
|
// TestPlanLaddersMapFirstHealthStripStates covers all three
|
|
// structural-health strip states at the merged page's new home: issues,
|
|
// healthy, and nothing-to-validate. The underlying template logic is
|
|
// exercised exhaustively by TestPlanTopologyTemplateRendering; this locks
|
|
// that the states still surface correctly once included from
|
|
// operator_plan_ladders.html rather than rendered by a standalone page.
|
|
func TestPlanLaddersMapFirstHealthStripStates(t *testing.T) {
|
|
t.Run("issues", func(t *testing.T) {
|
|
data := PlanLaddersData{Topology: PlanTopologyData{
|
|
Health: PlanLadderValidationData{MultiActivePools: []MultiActivePoolViewModel{{PoolID: "p1"}}},
|
|
}}
|
|
body := renderOperatorPartial(t, "operator_plan_ladders.html", data)
|
|
if !strings.Contains(body, "Structural issues") {
|
|
t.Errorf("expected the issues state to render at the new home")
|
|
}
|
|
})
|
|
|
|
t.Run("healthy", func(t *testing.T) {
|
|
body := renderOperatorPartial(t, "operator_plan_ladders.html", PlanLaddersData{})
|
|
if !strings.Contains(body, "No structural issues detected") {
|
|
t.Errorf("expected the healthy state to render at the new home")
|
|
}
|
|
})
|
|
|
|
t.Run("nothing to validate", func(t *testing.T) {
|
|
data := PlanLaddersData{Topology: PlanTopologyData{
|
|
Health: PlanLadderValidationData{MultiActivePools: []MultiActivePoolViewModel{{PoolID: "p1"}}},
|
|
NothingToValidate: true,
|
|
}}
|
|
body := renderOperatorPartial(t, "operator_plan_ladders.html", data)
|
|
if !strings.Contains(body, "Nothing to validate yet") {
|
|
t.Errorf("expected the nothing-to-validate state to render at the new home")
|
|
}
|
|
if strings.Contains(body, "Structural issues") {
|
|
t.Errorf("nothing-to-validate must take precedence over the issues state")
|
|
}
|
|
if strings.Contains(body, "No structural issues detected") {
|
|
t.Errorf("nothing-to-validate must not also claim healthy")
|
|
}
|
|
})
|
|
}
|