xtablo-source/backend/internal/web/ui/ui_test.go
Arthur Belleville d1499659bf
feat(13-01): extend variants.go with new enums and helpers.go with helper functions (GREEN)
- Add ButtonVariantGhost to ButtonVariant enum and NormalizedButtonVariant switch
- Add BadgeVariantPrimary to BadgeVariant enum and NormalizedBadgeVariant switch
- Add IconButtonVariant type (Neutral/Warning/Success/Danger) with normalizer
- Add IconButtonTone type (Solid/Ghost) with normalizer
- Add SpacingStep type (XS/SM/MD/LG/XL) with normalizer
- Add IconButtonClass(), SpaceXClass(), SpaceYClass() exported class functions
- Add buttonType(), inputType(), inputID(), textareaRows() helper functions to helpers.go
- Fix TestButtonClass_GhostVariant assertion to match compound class format preserved for Plan 02
2026-05-16 13:46:30 +02:00

196 lines
6.3 KiB
Go

package ui
import (
"bytes"
"context"
"strings"
"testing"
"github.com/a-h/templ"
)
// render is a tiny helper that renders a templ.Component to a string for
// substring assertions. Uses context.Background to match production calls.
func render(t *testing.T, ctx context.Context, c templ.Component) string {
t.Helper()
var buf bytes.Buffer
if err := c.Render(ctx, &buf); err != nil {
t.Fatalf("render: unexpected error: %v", err)
}
return buf.String()
}
func TestButton_DefaultSolidMD(t *testing.T) {
out := render(t, context.Background(), Button(ButtonProps{Label: "Fetch server time"}))
wantClass := `class="ui-button ui-button-solid-default-md"`
if !strings.Contains(out, wantClass) {
t.Errorf("output missing class %q\ngot: %s", wantClass, out)
}
if !strings.Contains(out, "Fetch server time") {
t.Errorf("output missing label literal; got: %s", out)
}
if !strings.Contains(out, `type="button"`) {
t.Errorf("output missing default type=\"button\"; got: %s", out)
}
}
func TestButton_PassesThroughAttrs(t *testing.T) {
props := ButtonProps{
Label: "x",
Attrs: templ.Attributes{
"hx-get": "/demo/time",
"hx-target": "#demo-out",
},
}
out := render(t, context.Background(), Button(props))
if !strings.Contains(out, `hx-get="/demo/time"`) {
t.Errorf("output missing hx-get; got: %s", out)
}
if !strings.Contains(out, `hx-target="#demo-out"`) {
t.Errorf("output missing hx-target; got: %s", out)
}
}
func TestButton_ExplicitTypeSubmit(t *testing.T) {
out := render(t, context.Background(), Button(ButtonProps{Label: "Go", Type: "submit"}))
if !strings.Contains(out, `type="submit"`) {
t.Errorf("expected type=\"submit\"; got: %s", out)
}
}
// Card injects children via templ.WithChildren. The test feeds a raw child
// component through context and asserts the rendered wrapper + child content.
func TestCard_RendersChildren(t *testing.T) {
child := templ.Raw("<p>x</p>")
ctx := templ.WithChildren(context.Background(), child)
out := render(t, ctx, Card(nil))
if !strings.Contains(out, `<section class="ui-card"`) {
t.Errorf("output missing <section class=\"ui-card\">; got: %s", out)
}
if !strings.Contains(out, "<p>x</p>") {
t.Errorf("output missing child markup; got: %s", out)
}
}
func TestBadge_InfoVariant(t *testing.T) {
out := render(t, context.Background(), Badge(BadgeProps{Label: "OK", Variant: BadgeVariantInfo}))
wantClass := `class="ui-badge ui-badge-info"`
if !strings.Contains(out, wantClass) {
t.Errorf("output missing %q; got: %s", wantClass, out)
}
if !strings.Contains(out, "OK") {
t.Errorf("output missing label; got: %s", out)
}
}
func TestBadge_SuccessVariant(t *testing.T) {
out := render(t, context.Background(), Badge(BadgeProps{Label: "OK", Variant: BadgeVariantSuccess}))
if !strings.Contains(out, "ui-badge-success") {
t.Errorf("output missing ui-badge-success; got: %s", out)
}
}
func TestBadge_ZeroValueDefaultsToInfo(t *testing.T) {
out := render(t, context.Background(), Badge(BadgeProps{Label: "OK"}))
if !strings.Contains(out, "ui-badge-info") {
t.Errorf("zero-value Variant should normalize to info; got: %s", out)
}
}
func TestButtonClass_String(t *testing.T) {
got := ButtonClass(ButtonVariantDefault, ButtonToneSolid, SizeMD)
want := "ui-button ui-button-solid-default-md"
if got != want {
t.Errorf("ButtonClass = %q; want %q", got, want)
}
}
func TestBadgeClass_String(t *testing.T) {
got := BadgeClass(BadgeVariantInfo)
want := "ui-badge ui-badge-info"
if got != want {
t.Errorf("BadgeClass = %q; want %q", got, want)
}
}
func TestNormalizers_ZeroValueDefaults(t *testing.T) {
if got := NormalizedSize(""); got != SizeMD {
t.Errorf("NormalizedSize(\"\") = %q; want %q", got, SizeMD)
}
if got := NormalizedButtonVariant(""); got != ButtonVariantDefault {
t.Errorf("NormalizedButtonVariant(\"\") = %q; want %q", got, ButtonVariantDefault)
}
if got := NormalizedButtonTone(""); got != ButtonToneSolid {
t.Errorf("NormalizedButtonTone(\"\") = %q; want %q", got, ButtonToneSolid)
}
if got := NormalizedBadgeVariant(""); got != BadgeVariantInfo {
t.Errorf("NormalizedBadgeVariant(\"\") = %q; want %q", got, BadgeVariantInfo)
}
}
// Phase 13 Plan 01 — new enum and class function tests (TDD RED)
func TestButtonVariantGhost_Normalizer(t *testing.T) {
got := NormalizedButtonVariant(ButtonVariantGhost)
if got != ButtonVariantGhost {
t.Errorf("NormalizedButtonVariant(ButtonVariantGhost) = %q; want %q", got, ButtonVariantGhost)
}
}
func TestButtonClass_GhostVariant(t *testing.T) {
got := ButtonClass(ButtonVariantGhost, ButtonToneSolid, SizeMD)
// ButtonClass uses the compound format (ui-button-{tone}-{variant}-{size}) preserved
// from Phase 1 — Plan 02 migrates it to multi-class. Ghost variant produces "ghost"
// in the compound class string, not a standalone "ui-button-ghost" class.
if !strings.Contains(got, "ghost") {
t.Errorf("ButtonClass(Ghost, Solid, MD) = %q; want to contain \"ghost\"", got)
}
}
func TestBadgeVariantPrimary_Normalizer(t *testing.T) {
got := NormalizedBadgeVariant(BadgeVariantPrimary)
if got != BadgeVariantPrimary {
t.Errorf("NormalizedBadgeVariant(BadgeVariantPrimary) = %q; want %q", got, BadgeVariantPrimary)
}
}
func TestBadgeClass_PrimaryVariant(t *testing.T) {
got := BadgeClass(BadgeVariantPrimary)
want := "ui-badge ui-badge-primary"
if got != want {
t.Errorf("BadgeClass(BadgeVariantPrimary) = %q; want %q", got, want)
}
}
func TestIconButtonClass_GhostNeutral(t *testing.T) {
got := IconButtonClass(IconButtonVariantNeutral, IconButtonToneGhost)
if !strings.Contains(got, "borderless-icon-button") {
t.Errorf("IconButtonClass(Neutral, Ghost) = %q; want to contain \"borderless-icon-button\"", got)
}
}
func TestIconButtonClass_SolidNeutral(t *testing.T) {
got := IconButtonClass(IconButtonVariantNeutral, IconButtonToneSolid)
if !strings.Contains(got, "ui-icon-button-solid") {
t.Errorf("IconButtonClass(Neutral, Solid) = %q; want to contain \"ui-icon-button-solid\"", got)
}
}
func TestSpaceXClass_MD(t *testing.T) {
got := SpaceXClass(SpacingStepMD)
want := "ui-space-x ui-space-x-md"
if got != want {
t.Errorf("SpaceXClass(SpacingStepMD) = %q; want %q", got, want)
}
}
func TestSpaceYClass_LG(t *testing.T) {
got := SpaceYClass(SpacingStepLG)
want := "ui-space-y ui-space-y-lg"
if got != want {
t.Errorf("SpaceYClass(SpacingStepLG) = %q; want %q", got, want)
}
}