// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial // SPDX-FileCopyrightText: 2025-2026 Christian Galo 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/forms" "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 "" }, "helpIcon": helpIcon, }) tmpl, err = web.ParseUIPartials(template.Must(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() } // TestCreatePlanLadderFormCollectsNameOnly covers plan-ladder-management // ("Operator creates a plan ladder") after entity-keys retired the // ladder key: the create form asks for the display name and the description // and collects nothing else that identifies the ladder, so there is no // permanent-after-creation value left to disclose (the "Irreversibility is // disclosed at creation time" requirement was removed with the key). The // page also carries no "Key" vocabulary for ladders (ui-vocabulary); the // retired word is covered by the embeds vocabulary guard. func TestCreatePlanLadderFormCollectsNameOnly(t *testing.T) { // The create form lives on its own page now (design D20, round 4), // rendering the declaration operator.plan-ladder (form-library) unbound. body := renderOperatorPartial(t, "operator_plan_ladder_new.html", PlanLadderNewData{ Form: planLadderCreateForm(forms.NewValues(), nil), }) if !strings.Contains(body, `data-form="operator.plan-ladder"`) { t.Errorf("expected the create form to render the declaration, got: %s", body) } if !strings.Contains(body, `name="name"`) { t.Errorf("expected the create form to collect the display name, got: %s", body) } if !strings.Contains(body, `name="description"`) { t.Errorf("expected the create form to collect the description, got: %s", body) } for _, gone := range []string{`id="ladderKey"`, `name="ladder_key"`, "data-key-from", "seeds and scripts find the ladder by it"} { if strings.Contains(body, gone) { t.Errorf("expected the create form to carry no %q, got: %s", gone, 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", Name: "Hosting", ActiveAttachmentCount: 4}, {PlanLadderID: "l-empty", 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 aria-describedby="delete-ladder-reason-l-occupied">Delete`) { t.Errorf("occupied ladder's Delete must render disabled via the shared disabledControl part, 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") } }