- TestInput_DefaultType: expects type="text" for empty Type - TestInput_EmailType: expects type="email" for explicit Type - TestInput_IDFallback: expects id from Name when no ID set - TestInput_ExplicitID: expects explicit ID to take precedence - TestTextarea_RendersClass: expects class="ui-textarea" - TestTextarea_DefaultRows: expects rows="4" for zero Rows - TestTextarea_ExplicitRows: expects rows="6" for explicit Rows
282 lines
8.7 KiB
Go
282 lines
8.7 KiB
Go
package ui
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"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"}))
|
|
|
|
// Plan 02: multi-class assertions (one check per class token)
|
|
for _, wantClass := range []string{
|
|
`ui-button`,
|
|
`ui-button-solid`,
|
|
`ui-button-default`,
|
|
`ui-button-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)
|
|
}
|
|
}
|
|
|
|
|
|
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 ui-button-default ui-button-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)
|
|
// Plan 02: ghost variant omits the tone class and emits "ui-button-ghost" standalone.
|
|
want := "ui-button ui-button-ghost ui-button-md"
|
|
if got != want {
|
|
t.Errorf("ButtonClass(Ghost, Solid, MD) = %q; want %q", got, want)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// textComponent is a test helper that wraps a raw string as a templ.Component,
|
|
// used for asserting typed CardProps fields (Header/Body/Footer).
|
|
func textComponent(text string) templ.Component {
|
|
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
|
|
_, err := w.Write([]byte(text))
|
|
return err
|
|
})
|
|
}
|
|
|
|
// Phase 13 Plan 02 — typed Card API and Badge primary variant tests (TDD RED)
|
|
|
|
func TestCard_RendersTypedRegions(t *testing.T) {
|
|
out := render(t, context.Background(), Card(CardProps{
|
|
Header: textComponent("header"),
|
|
Body: textComponent("body"),
|
|
}))
|
|
for _, want := range []string{
|
|
`ui-card-header`,
|
|
`header`,
|
|
`ui-card-body`,
|
|
`body`,
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Fatalf("expected %q in %q", want, out)
|
|
}
|
|
}
|
|
// nil footer must not emit ui-card-footer
|
|
if strings.Contains(out, "ui-card-footer") {
|
|
t.Errorf("expected no ui-card-footer when Footer is nil; got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestBadge_PrimaryVariant(t *testing.T) {
|
|
out := render(t, context.Background(), Badge(BadgeProps{Label: "new", Variant: BadgeVariantPrimary}))
|
|
if !strings.Contains(out, "ui-badge-primary") {
|
|
t.Errorf("expected ui-badge-primary in output; got: %s", out)
|
|
}
|
|
}
|
|
|
|
// Phase 13 Plan 03 — Input and Textarea component tests (TDD RED)
|
|
|
|
func TestInput_DefaultType(t *testing.T) {
|
|
out := render(t, context.Background(), Input(InputProps{Name: "x"}))
|
|
if !strings.Contains(out, `type="text"`) {
|
|
t.Errorf("expected type=\"text\" for empty Type; got: %s", out)
|
|
}
|
|
if !strings.Contains(out, `class="ui-input"`) {
|
|
t.Errorf("expected class=\"ui-input\"; got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestInput_EmailType(t *testing.T) {
|
|
out := render(t, context.Background(), Input(InputProps{Name: "email", Type: "email"}))
|
|
if !strings.Contains(out, `type="email"`) {
|
|
t.Errorf("expected type=\"email\"; got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestInput_IDFallback(t *testing.T) {
|
|
out := render(t, context.Background(), Input(InputProps{Name: "email"}))
|
|
if !strings.Contains(out, `id="email"`) {
|
|
t.Errorf("expected id=\"email\" (fallback from name); got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestInput_ExplicitID(t *testing.T) {
|
|
out := render(t, context.Background(), Input(InputProps{ID: "my-id", Name: "x"}))
|
|
if !strings.Contains(out, `id="my-id"`) {
|
|
t.Errorf("expected id=\"my-id\"; got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestTextarea_RendersClass(t *testing.T) {
|
|
out := render(t, context.Background(), Textarea(TextareaProps{Name: "body"}))
|
|
if !strings.Contains(out, `class="ui-textarea"`) {
|
|
t.Errorf("expected class=\"ui-textarea\"; got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestTextarea_DefaultRows(t *testing.T) {
|
|
out := render(t, context.Background(), Textarea(TextareaProps{Name: "x", Rows: 0}))
|
|
if !strings.Contains(out, `rows="4"`) {
|
|
t.Errorf("expected rows=\"4\" for zero Rows; got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestTextarea_ExplicitRows(t *testing.T) {
|
|
out := render(t, context.Background(), Textarea(TextareaProps{Name: "x", Rows: 6}))
|
|
if !strings.Contains(out, `rows="6"`) {
|
|
t.Errorf("expected rows=\"6\"; got: %s", out)
|
|
}
|
|
}
|