diff --git a/internal/server/act_answer_test.go b/internal/server/act_answer_test.go new file mode 100644 index 00000000..2c7c3c0e --- /dev/null +++ b/internal/server/act_answer_test.go @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial +// SPDX-FileCopyrightText: 2025-2026 Christian Galo + +package server + +import ( + "encoding/json" + "fmt" + "maps" + "net/http" + "net/http/httptest" + "slices" + "strings" + "testing" + + "git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/htmltest" +) + +// actAnswer is what an act answered, as text for a golden file: the status; +// each HX-* header the handler set, in name order, HX-Trigger as one line +// per event it fires with its message; then, for each selector in regions, +// the text of every element it selects, in document order. A region that +// selects nothing prints nothing, so the file shows which regions an answer +// filled. Pass the text through the fixture's normalizer before comparing. +func actAnswer(t *testing.T, rec *httptest.ResponseRecorder, regions ...string) string { + t.Helper() + var b strings.Builder + fmt.Fprintf(&b, "status %d\n", rec.Code) + for _, name := range slices.Sorted(maps.Keys(rec.Header())) { + if !strings.HasPrefix(name, "Hx-") { + continue + } + value := rec.Header().Get(name) + if name != "Hx-Trigger" { + fmt.Fprintf(&b, "%s %s\n", name, value) + continue + } + var fired map[string]any + if err := json.Unmarshal([]byte(value), &fired); err != nil { + t.Fatalf("HX-Trigger %q: %v", value, err) + } + for _, event := range slices.Sorted(maps.Keys(fired)) { + fmt.Fprintf(&b, "%s %s: %v\n", name, event, fired[event]) + } + } + page := htmltest.Parse(t, rec.Body.String()) + for _, sel := range regions { + for _, text := range page.Texts(sel) { + fmt.Fprintf(&b, "%s: %s\n", sel, text) + } + } + return b.String() +} + +// TestActAnswerPrintsStatusHeadersAndRegions: the status, the HX-* headers in +// name order with each fired event on its own line, and the selected regions' +// text with its spaces collapsed; other headers and unselected markup stay +// out. +func TestActAnswerPrintsStatusHeadersAndRegions(t *testing.T) { + rec := httptest.NewRecorder() + fireErrorToast(rec, "Removal failed and nothing was changed.") + rec.Header().Set("HX-Retarget", "#main") + rec.Header().Set("Content-Type", "text/html") + rec.WriteHeader(http.StatusUnprocessableEntity) + _, _ = rec.WriteString(`

Other

Second.
`) + got := actAnswer(t, rec, ".alert-danger", ".alert-success") + want := "status 422\n" + + "Hx-Retarget #main\n" + + "Hx-Trigger showErrorToast: Removal failed and nothing was changed.\n" + + ".alert-danger: Choose whether to keep or migrate.\n" + + ".alert-danger: Second.\n" + if got != want { + t.Errorf("actAnswer =\n%s\nwant\n%s", got, want) + } +}