package server import ( "bytes" "html/template" "io/fs" "strings" "testing" "git.coopcloud.tech/wiki-cafe/member-console/internal/embeds" "git.coopcloud.tech/wiki-cafe/member-console/internal/web" ) func renderPlanLaddersPageBody(t *testing.T, data PlanLaddersData) string { t.Helper() partialsSub, err := fs.Sub(embeds.Templates, "templates/partials") if err != nil { t.Fatalf("fs.Sub partials: %v", err) } tmpl := template.New("operator").Funcs(template.FuncMap{ "renderBody": func(string, any) (template.HTML, error) { return "", nil }, "routeURL": web.RouteURL, "fieldErr": func(activeForm, expectedForm, activeScope, expectedScope string, errs web.FieldErrors, field string) string { return "" }, "stripeEntityURL": func(string, string) string { return "" }, }) tmpl, err = tmpl.ParseFS(partialsSub, "operator_*.html") if err != nil { t.Fatalf("ParseFS: %v", err) } var buf bytes.Buffer if err := tmpl.ExecuteTemplate(&buf, "operator_plan_ladders.html", data); err != nil { t.Fatalf("ExecuteTemplate: %v", err) } return buf.String() } // TestCreatePlanLadderSlugDisclosure covers UX-10 (ux-honest-surfaces 5.2): // the Create Plan Ladder form must disclose slug immutability at the field // itself, not only on the edit page reached after the value is already // fixed. func TestCreatePlanLadderSlugDisclosure(t *testing.T) { body := renderPlanLaddersPageBody(t, PlanLaddersData{}) if !strings.Contains(body, "Slug cannot be changed after creation.") { t.Errorf("expected the create form's slug field to disclose immutability, got: %s", body) } } // TestPlanLaddersListDeleteConsequenceCopy covers plan-ladder-management // ("Destructive ladder controls disclose live-use consequences inline", // revised by maintainer 2026-08-23): a ladder row with active positions // renders its Delete control DISABLED on the same row as Manage, with the // blocked reason as the wrapping tooltip; a ladder with no active positions // gets the live confirm-modal Delete and no blocked claim. func TestPlanLaddersListDeleteConsequenceCopy(t *testing.T) { body := renderPlanLaddersPageBody(t, PlanLaddersData{ Ladders: []PlanLadderViewModel{ {PlanLadderID: "l-occupied", LadderKey: "hosting", Name: "Hosting", ActiveAttachmentCount: 4}, {PlanLadderID: "l-empty", LadderKey: "legacy", Name: "Legacy", ActiveAttachmentCount: 0}, }, }) if !strings.Contains(body, `title="4 organization(s) hold a position; deletion is blocked until none remain."`) { t.Errorf("expected the blocked reason as the disabled Delete control's tooltip, got: %s", body) } occupiedIdx := strings.Index(body, "l-occupied") emptyIdx := strings.Index(body, "l-empty") if occupiedIdx == -1 || emptyIdx == -1 { t.Fatal("expected both ladder rows to render") } occupiedRow := body[occupiedIdx:emptyIdx] if !strings.Contains(occupiedRow, `disabled>Delete`) { t.Errorf("occupied ladder's Delete must render disabled, got row: %s", occupiedRow) } if strings.Contains(occupiedRow, "data-bs-target=\"#confirmActionModal\"") { t.Errorf("occupied ladder must not offer the confirm-modal Delete") } if strings.Contains(body[emptyIdx:], "organization(s) hold a position") { t.Errorf("ladder with no active attachments must not render the blocked claim") } if !strings.Contains(body[emptyIdx:], "data-bs-target=\"#confirmActionModal\"") { t.Errorf("empty ladder must keep the live confirm-modal Delete") } }