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 UX-10 (ux-honest-surfaces // 5.3): a ladder row with active attachments must state inline, beside the // Delete control, that deletion is blocked while organizations are attached // -- visible on the list itself, before any confirm dialog opens -- while a // ladder with no active attachments carries no such 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, "4 organization(s) attached; deletion is blocked until they're detached.") { t.Errorf("expected inline consequence copy beside the occupied ladder's Delete control, got: %s", body) } emptyIdx := strings.Index(body, "l-empty") if emptyIdx == -1 { t.Fatal("expected the empty ladder row to render") } if strings.Contains(body[emptyIdx:], "organization(s) attached") { t.Errorf("ladder with no active attachments must not render the live-use consequence copy") } }