go-html/layout_test.go
Virgil ae286563fd
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run
fix(html): normalise nil render context
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-03 18:05:06 +00:00

240 lines
7.1 KiB
Go

package html
import (
"errors"
"strings"
"testing"
)
func TestLayout_HLCRF(t *testing.T) {
ctx := NewContext()
layout := NewLayout("HLCRF").
H(Raw("header")).L(Raw("left")).C(Raw("main")).R(Raw("right")).F(Raw("footer"))
got := layout.Render(ctx)
// Must contain semantic elements
for _, want := range []string{"<header", "<aside", "<main", "<footer"} {
if !strings.Contains(got, want) {
t.Errorf("HLCRF layout missing %q in:\n%s", want, got)
}
}
// Must contain ARIA roles
for _, want := range []string{`role="banner"`, `role="complementary"`, `role="main"`, `role="contentinfo"`} {
if !strings.Contains(got, want) {
t.Errorf("HLCRF layout missing role %q in:\n%s", want, got)
}
}
// Must contain data-block IDs
for _, want := range []string{`data-block="H-0"`, `data-block="L-0"`, `data-block="C-0"`, `data-block="R-0"`, `data-block="F-0"`} {
if !strings.Contains(got, want) {
t.Errorf("HLCRF layout missing %q in:\n%s", want, got)
}
}
// Must contain content
for _, want := range []string{"header", "left", "main", "right", "footer"} {
if !strings.Contains(got, want) {
t.Errorf("HLCRF layout missing content %q in:\n%s", want, got)
}
}
}
func TestLayout_HCF(t *testing.T) {
ctx := NewContext()
layout := NewLayout("HCF").
H(Raw("header")).L(Raw("left")).C(Raw("main")).R(Raw("right")).F(Raw("footer"))
got := layout.Render(ctx)
// HCF should have header, main, footer
for _, want := range []string{`data-block="H-0"`, `data-block="C-0"`, `data-block="F-0"`} {
if !strings.Contains(got, want) {
t.Errorf("HCF layout missing %q in:\n%s", want, got)
}
}
// HCF must NOT have L or R slots
for _, unwanted := range []string{`data-block="L-0"`, `data-block="R-0"`} {
if strings.Contains(got, unwanted) {
t.Errorf("HCF layout should NOT contain %q in:\n%s", unwanted, got)
}
}
}
func TestLayout_ContentOnly(t *testing.T) {
ctx := NewContext()
layout := NewLayout("C").
H(Raw("header")).L(Raw("left")).C(Raw("main")).R(Raw("right")).F(Raw("footer"))
got := layout.Render(ctx)
// Only C slot should render
if !strings.Contains(got, `data-block="C-0"`) {
t.Errorf("C layout missing data-block=\"C-0\" in:\n%s", got)
}
if !strings.Contains(got, "<main") {
t.Errorf("C layout missing <main in:\n%s", got)
}
// No other slots
for _, unwanted := range []string{`data-block="H-0"`, `data-block="L-0"`, `data-block="R-0"`, `data-block="F-0"`} {
if strings.Contains(got, unwanted) {
t.Errorf("C layout should NOT contain %q in:\n%s", unwanted, got)
}
}
}
func TestLayout_FluentAPI(t *testing.T) {
layout := NewLayout("HLCRF")
// Fluent methods should return the same layout for chaining
result := layout.H(Raw("h")).L(Raw("l")).C(Raw("c")).R(Raw("r")).F(Raw("f"))
if result != layout {
t.Error("fluent methods must return the same *Layout for chaining")
}
got := layout.Render(NewContext())
if got == "" {
t.Error("fluent chain should produce non-empty output")
}
}
func TestLayout_IgnoresInvalidSlots(t *testing.T) {
ctx := NewContext()
// "C" variant: populating L and R should have no effect
layout := NewLayout("C").L(Raw("left")).C(Raw("main")).R(Raw("right"))
got := layout.Render(ctx)
if !strings.Contains(got, "main") {
t.Errorf("C variant should render main content, got:\n%s", got)
}
if strings.Contains(got, "left") {
t.Errorf("C variant should ignore L slot content, got:\n%s", got)
}
if strings.Contains(got, "right") {
t.Errorf("C variant should ignore R slot content, got:\n%s", got)
}
}
func TestLayout_RendersEmptySlots(t *testing.T) {
ctx := NewContext()
layout := NewLayout("HCF")
got := layout.Render(ctx)
for _, want := range []string{`<header role="banner" data-block="H-0"></header>`, `<main role="main" data-block="C-0"></main>`, `<footer role="contentinfo" data-block="F-0"></footer>`} {
if !strings.Contains(got, want) {
t.Errorf("empty slot should still render %q in:\n%s", want, got)
}
}
}
func TestValidateLayoutVariant(t *testing.T) {
tests := []struct {
name string
variant string
wantErr bool
}{
{name: "valid", variant: "HCF", wantErr: false},
{name: "invalid", variant: "HXC", wantErr: true},
{name: "empty", variant: "", wantErr: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateLayoutVariant(tt.variant)
if tt.wantErr {
if err == nil {
t.Fatalf("ValidateLayoutVariant(%q) = nil, want error", tt.variant)
}
if !errors.Is(err, ErrInvalidLayoutVariant) {
t.Fatalf("ValidateLayoutVariant(%q) = %v, want ErrInvalidLayoutVariant", tt.variant, err)
}
return
}
if err != nil {
t.Fatalf("ValidateLayoutVariant(%q) = %v, want nil", tt.variant, err)
}
})
}
}
func TestLayout_VariantError(t *testing.T) {
tests := []struct {
name string
variant string
wantErr bool
wantErrString string
wantRender string
}{
{
name: "valid variant",
variant: "HCF",
wantRender: `<header role="banner" data-block="H-0">header</header>` +
`<main role="main" data-block="C-0">main</main>` +
`<footer role="contentinfo" data-block="F-0">footer</footer>`,
},
{
name: "mixed invalid variant",
variant: "HXC",
wantErr: true,
wantErrString: "html: invalid layout variant HXC (invalid slot: 'X')",
wantRender: `<header role="banner" data-block="H-0">header</header>` +
`<main role="main" data-block="C-0">main</main>`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
layout := NewLayout(tt.variant)
layout.H(Raw("header")).C(Raw("main")).F(Raw("footer"))
if tt.wantErr {
if layout.VariantError() == nil {
t.Fatalf("VariantError() = nil, want sentinel error for %q", tt.variant)
}
if !errors.Is(layout.VariantError(), ErrInvalidLayoutVariant) {
t.Fatalf("VariantError() = %v, want errors.Is(..., ErrInvalidLayoutVariant)", layout.VariantError())
}
if got := layout.VariantError().Error(); got != tt.wantErrString {
t.Fatalf("VariantError().Error() = %q, want %q", got, tt.wantErrString)
}
if err, ok := layout.VariantError().(*layoutVariantError); ok {
if got := string(err.InvalidSlots()); got != "X" {
t.Fatalf("InvalidSlots() = %q, want %q", got, "X")
}
} else {
t.Fatalf("VariantError() has unexpected concrete type %T", layout.VariantError())
}
} else if layout.VariantError() != nil {
t.Fatalf("VariantError() = %v, want nil", layout.VariantError())
}
got := layout.Render(NewContext())
if got != tt.wantRender {
t.Fatalf("Render() = %q, want %q", got, tt.wantRender)
}
})
}
}
func TestLayout_RenderNilReceiver(t *testing.T) {
var layout *Layout
got := layout.Render(NewContext())
if got != "" {
t.Fatalf("nil Layout should render empty string, got %q", got)
}
}
func TestLayout_RenderNilContext(t *testing.T) {
layout := NewLayout("C").C(Raw("content"))
got := layout.Render(nil)
if !strings.Contains(got, `data-block="C-0"`) {
t.Fatalf("Layout.Render(nil) should still render the block ID, got:\n%s", got)
}
if !strings.Contains(got, "content") {
t.Fatalf("Layout.Render(nil) should still render content, got:\n%s", got)
}
}