From e3e632676cd146c40030350d1f005b1a2b3183bd Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 11 Aug 2026 12:20:24 +0000 Subject: [PATCH] Clarify web UI terminology and copy the entrypoint URL (refs #57) Terminology: the nav labelled its link Sources while every heading said Webhooks. Both nav links and the sources list page title now say Webhooks, matching the product name and the database.Webhook model. The /sources and /source/{id} routes are deliberately unchanged; renaming them would break existing bookmarks for no gain, since the URL is not what a user reads. Profile: the "Settings" column held only placeholder copy promising settings that would appear later. Password change is the only real account setting and it already has its own card below, so the column is removed and the two-column grid collapses to the single remaining one. Entrypoint URL copy button as progressive enhancement. The button is rendered with the hidden attribute and a data-copy-target naming the element that holds the URL. app.js reveals it only after confirming both a resolvable target and a usable Clipboard API, so a browser without either shows no dead control, and the URL is plain selectable text in every case. Template rendering assertions cover the nav labels, the absence of any remaining user-visible "Sources", and the button's hidden-by-default markup, so the copy cannot drift back silently. Note on the retention copy: it is untouched here. The reaper treats a non-positive RetentionDays as retain-forever, but on this branch no UI path can produce one, so copy describing that would be false today. The work belongs with the change that makes the value reachable. --- internal/handlers/ui_copy_test.go | 143 ++++++++++++++++++++++++++++++ static/js/app.js | 60 +++++++++++++ templates/navbar.html | 4 +- templates/profile.html | 30 +++---- templates/source_detail.html | 7 +- templates/sources_list.html | 2 +- 6 files changed, 224 insertions(+), 22 deletions(-) create mode 100644 internal/handlers/ui_copy_test.go diff --git a/internal/handlers/ui_copy_test.go b/internal/handlers/ui_copy_test.go new file mode 100644 index 0000000..dc5d96d --- /dev/null +++ b/internal/handlers/ui_copy_test.go @@ -0,0 +1,143 @@ +package handlers_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "sneak.berlin/go/webhooker/internal/database" + "sneak.berlin/go/webhooker/internal/handlers" + "sneak.berlin/go/webhooker/internal/session" +) + +// renderPage renders a page template through the real template set as +// an authenticated user and returns the resulting HTML. +func renderPage( + t *testing.T, + h *handlers.Handlers, + sess *session.Session, + page string, + data map[string]any, +) string { + t.Helper() + + cookies := authenticatedCookies(t, sess, "test-user-id", "testuser") + + req := httptest.NewRequestWithContext( + context.Background(), http.MethodGet, "/", nil, + ) + for _, c := range cookies { + req.AddCookie(c) + } + + w := httptest.NewRecorder() + h.RenderTemplateForTest(w, req, page, data) + + return w.Body.String() +} + +// TestNavbarUsesWebhookTerminology pins the user-visible navigation +// label to "Webhooks". The /sources route is deliberately unchanged, so +// the assertion targets the link text rather than the href. +func TestNavbarUsesWebhookTerminology(t *testing.T) { + t.Parallel() + + var h *handlers.Handlers + + var sess *session.Session + + app := newTestApp(t, &h, &sess) + app.RequireStart() + + t.Cleanup(app.RequireStop) + + body := renderPage(t, h, sess, "sources_list.html", map[string]any{ + "Webhooks": []handlers.WebhookListItem{}, + }) + + assert.Contains(t, body, `class="btn-text">Webhooks`) + assert.Contains( + t, body, `class="btn-text w-full text-left">Webhooks`, + ) + assert.Contains( + t, body, + `

Webhooks

`, + ) + assert.NotContains( + t, body, ">Sources<", + "no user-visible element may still be labelled Sources", + ) + assert.Contains( + t, body, `href="/sources"`, + "the /sources route itself must not change", + ) +} + +// TestEditPageUsesWebhookTerminology pins the edit page's heading and +// its back link. The link's href still points at /source/{id}, which is +// intentional: only user-visible copy changes. +func TestEditPageUsesWebhookTerminology(t *testing.T) { + t.Parallel() + + var h *handlers.Handlers + + var sess *session.Session + + app := newTestApp(t, &h, &sess) + app.RequireStart() + + t.Cleanup(app.RequireStop) + + webhook := database.Webhook{Name: "wh", RetentionDays: 14} + webhook.ID = "wh-1" + + body := renderPage(t, h, sess, "source_edit.html", map[string]any{ + "Webhook": webhook, + "Error": "", + }) + + assert.Contains(t, body, "Edit Webhook") + assert.NotContains(t, body, ">Sources<") + assert.Contains(t, body, `href="/source/wh-1"`) +} + +// TestEntrypointCopyButtonIsProgressiveEnhancement proves the copy +// affordance degrades: the button ships with the hidden attribute, so a +// browser that never runs app.js shows no dead control, and the URL is +// rendered as ordinary selectable text either way. +func TestEntrypointCopyButtonIsProgressiveEnhancement(t *testing.T) { + t.Parallel() + + var h *handlers.Handlers + + var sess *session.Session + + app := newTestApp(t, &h, &sess) + app.RequireStart() + + t.Cleanup(app.RequireStop) + + entrypoint := database.Entrypoint{Path: "abc123"} + entrypoint.ID = "ep-1" + + body := renderPage(t, h, sess, "source_detail.html", map[string]any{ + "Webhook": database.Webhook{Name: "wh"}, + "Entrypoints": []database.Entrypoint{entrypoint}, + "Targets": []database.Target{}, + "Events": []database.Event{}, + "BaseURL": "https://hooks.example.com", + }) + + assert.Contains( + t, body, + `