feat(web): reusable flash/notification banner (PRG)
Add a one-shot flash component used across the app — connect success, disconnect, account delete, and registration — instead of per-page ad-hoc markup. setFlash queues a short-lived HttpOnly+SameSite cookie carrying an opaque code; takeFlash consumes it on the next full-page render (not on HTMX fragments). flashBanner maps the code to a styled, role=status banner; the message text lives server-side in flashMessages so the cookie never carries free text and a forged/unknown code renders nothing. Wire it into the list page (the PRG landing spot for connect/registration) and set it on registration and connect-callback success. Styled with the existing design-system tokens; header gains an Account nav link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestFlashBannerRendersEachKind proves the reusable notification component
|
||||
// renders a banner with the right message and severity class for every flash
|
||||
// code, and renders nothing for an empty or unknown (e.g. forged) code.
|
||||
func TestFlashBannerRendersEachKind(t *testing.T) {
|
||||
cases := []struct {
|
||||
code string
|
||||
wantText string
|
||||
wantKind string
|
||||
}{
|
||||
{flashConnected, "YouTube account connected", "flash-success"},
|
||||
{flashConnectFailed, "Could not connect", "flash-error"},
|
||||
{flashDisconnected, "Account disconnected", "flash-success"},
|
||||
{flashDeleted, "account and all its data were deleted", "flash-success"},
|
||||
{flashRegistered, "Welcome to Tapir", "flash-success"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.code, func(t *testing.T) {
|
||||
var sb strings.Builder
|
||||
if err := flashBanner(tc.code).Render(context.Background(), &sb); err != nil {
|
||||
t.Fatalf("render: %v", err)
|
||||
}
|
||||
got := sb.String()
|
||||
if !strings.Contains(got, tc.wantText) {
|
||||
t.Errorf("banner %q = %q, want it to contain %q", tc.code, got, tc.wantText)
|
||||
}
|
||||
if !strings.Contains(got, tc.wantKind) {
|
||||
t.Errorf("banner %q = %q, want severity class %q", tc.code, got, tc.wantKind)
|
||||
}
|
||||
if !strings.Contains(got, `role="status"`) {
|
||||
t.Errorf("banner %q must carry role=status for assistive tech, got %q", tc.code, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlashBannerRendersNothingForUnknownCode(t *testing.T) {
|
||||
for _, code := range []string{"", "bogus", "<script>"} {
|
||||
var sb strings.Builder
|
||||
if err := flashBanner(code).Render(context.Background(), &sb); err != nil {
|
||||
t.Fatalf("render: %v", err)
|
||||
}
|
||||
if got := strings.TrimSpace(sb.String()); got != "" {
|
||||
t.Errorf("flashBanner(%q) = %q, want empty (no banner)", code, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user