Print an act's answer as text for a golden file

Add actAnswer to the server's tests: the status, each HX-* header in name
order with each HX-Trigger event on a line of its own beside its message,
and the text of every element the caller's selectors pick, in document
order. A selector that picks nothing prints nothing, so a golden shows which
regions an answer filled. Later tests of the plan-wide acts and of
the Stripe sync will read their answers through it.

No output changed: the commit adds one test file and touches no production
file. The names and results of ./internal/server, run after a database reset
at the parent and at this commit, differ by the one new passing test.

7 breaks, each caught by the new test; the collapse break edits the HTML
helper's Text, where the collapse now lives.

Unprobed: an HX-Trigger header that is not JSON, because every HX-Trigger the
console sets is JSON (the toasts through one JSON-producing function), and the
test stops on one that is not.
This commit is contained in:
2026-10-10 14:50:48 -05:00
parent 25fb6e45af
commit ab0effa4c7
+77
View File
@@ -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(`<main><div class="alert alert-danger" role="alert">
Choose whether to keep
or migrate.</div><p>Other</p><div class="alert alert-danger">Second.</div></main>`)
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)
}
}