From 7efd2ab93af27e8b34415196d2e73c6c649fdd52 Mon Sep 17 00:00:00 2001 From: Snider Date: Fri, 20 Feb 2026 05:33:15 +0000 Subject: [PATCH] test: add benchmarks, Unicode edge cases, and stress tests Performance benchmarks across all major APIs: - BenchmarkRender (depth 1/3/5/7 tree, full page) - BenchmarkImprint (small/large pipeline) - BenchmarkCompareVariants (2/3 variants) - BenchmarkLayout (C, HCF, HLCRF, nested, many children) - BenchmarkEach (10/100/1000 items) - BenchmarkResponsive, BenchmarkStripTags - Codegen: GenerateClass, TagToClassName, GenerateBundle, GenerateRegistration Edge case tests: - Unicode: emoji, RTL (Arabic/Hebrew), zero-width chars, mixed scripts - Deep nesting: 10/20 levels, mixed slot types - Large Each iterations: 1000/5000 items, nested Each - Layout variant validation: invalid chars, lowercase, duplicates, empty - Nil context handling for Render, Imprint, CompareVariants - Switch no-match, Entitled nil context, empty tag El Co-Authored-By: Virgil --- bench_test.go | 290 +++++++++++++++++++++++++ codegen/bench_test.go | 46 ++++ edge_test.go | 485 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 821 insertions(+) create mode 100644 bench_test.go create mode 100644 codegen/bench_test.go create mode 100644 edge_test.go diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..63e6ab5 --- /dev/null +++ b/bench_test.go @@ -0,0 +1,290 @@ +package html + +import ( + "fmt" + "testing" + + i18n "forge.lthn.ai/core/go-i18n" +) + +func init() { + svc, _ := i18n.New() + i18n.SetDefault(svc) +} + +// --- BenchmarkRender --- + +// buildTree creates an El tree of the given depth with branching factor 3. +func buildTree(depth int) Node { + if depth <= 0 { + return Raw("leaf") + } + children := make([]Node, 3) + for i := range children { + children[i] = buildTree(depth - 1) + } + return El("div", children...) +} + +func BenchmarkRender_Depth1(b *testing.B) { + node := buildTree(1) + ctx := NewContext() + b.ResetTimer() + for b.Loop() { + node.Render(ctx) + } +} + +func BenchmarkRender_Depth3(b *testing.B) { + node := buildTree(3) + ctx := NewContext() + b.ResetTimer() + for b.Loop() { + node.Render(ctx) + } +} + +func BenchmarkRender_Depth5(b *testing.B) { + node := buildTree(5) + ctx := NewContext() + b.ResetTimer() + for b.Loop() { + node.Render(ctx) + } +} + +func BenchmarkRender_Depth7(b *testing.B) { + node := buildTree(7) + ctx := NewContext() + b.ResetTimer() + for b.Loop() { + node.Render(ctx) + } +} + +func BenchmarkRender_FullPage(b *testing.B) { + page := NewLayout("HCF"). + H(El("h1", Text("Dashboard"))). + C( + El("div", + El("p", Text("Welcome")), + Each([]string{"Home", "Settings", "Profile"}, func(item string) Node { + return El("a", Raw(item)) + }), + ), + ). + F(El("small", Text("Footer"))) + ctx := NewContext() + + b.ResetTimer() + for b.Loop() { + page.Render(ctx) + } +} + +// --- BenchmarkImprint --- + +func BenchmarkImprint_Small(b *testing.B) { + page := NewLayout("HCF"). + H(El("h1", Text("Building project"))). + C(El("p", Text("Files deleted successfully"))). + F(El("small", Text("Completed"))) + ctx := NewContext() + + b.ResetTimer() + for b.Loop() { + Imprint(page, ctx) + } +} + +func BenchmarkImprint_Large(b *testing.B) { + items := make([]string, 20) + for i := range items { + items[i] = fmt.Sprintf("Item %d was created successfully", i) + } + page := NewLayout("HLCRF"). + H(El("h1", Text("Building project"))). + L(El("nav", Each(items[:5], func(s string) Node { return El("a", Text(s)) }))). + C(El("div", Each(items, func(s string) Node { return El("p", Text(s)) }))). + R(El("aside", Text("Completed rendering operation"))). + F(El("small", Text("Finished processing all items"))) + ctx := NewContext() + + b.ResetTimer() + for b.Loop() { + Imprint(page, ctx) + } +} + +// --- BenchmarkCompareVariants --- + +func BenchmarkCompareVariants_TwoVariants(b *testing.B) { + r := NewResponsive(). + Variant("desktop", NewLayout("HLCRF"). + H(El("h1", Text("Building project"))). + C(El("p", Text("Files deleted successfully"))). + F(El("small", Text("Completed")))). + Variant("mobile", NewLayout("HCF"). + H(El("h1", Text("Building project"))). + C(El("p", Text("Files deleted successfully"))). + F(El("small", Text("Completed")))) + ctx := NewContext() + + b.ResetTimer() + for b.Loop() { + CompareVariants(r, ctx) + } +} + +func BenchmarkCompareVariants_ThreeVariants(b *testing.B) { + r := NewResponsive(). + Variant("desktop", NewLayout("HLCRF"). + H(El("h1", Text("Building project"))). + L(El("nav", Text("Navigation links"))). + C(El("p", Text("Files deleted successfully"))). + R(El("aside", Text("Sidebar content"))). + F(El("small", Text("Completed")))). + Variant("tablet", NewLayout("HCF"). + H(El("h1", Text("Building project"))). + C(El("p", Text("Files deleted successfully"))). + F(El("small", Text("Completed")))). + Variant("mobile", NewLayout("C"). + C(El("p", Text("Files deleted successfully")))) + ctx := NewContext() + + b.ResetTimer() + for b.Loop() { + CompareVariants(r, ctx) + } +} + +// --- BenchmarkLayout --- + +func BenchmarkLayout_ContentOnly(b *testing.B) { + layout := NewLayout("C").C(Raw("content")) + ctx := NewContext() + + b.ResetTimer() + for b.Loop() { + layout.Render(ctx) + } +} + +func BenchmarkLayout_HCF(b *testing.B) { + layout := NewLayout("HCF"). + H(Raw("header")).C(Raw("main")).F(Raw("footer")) + ctx := NewContext() + + b.ResetTimer() + for b.Loop() { + layout.Render(ctx) + } +} + +func BenchmarkLayout_HLCRF(b *testing.B) { + layout := NewLayout("HLCRF"). + H(Raw("header")).L(Raw("left")).C(Raw("main")).R(Raw("right")).F(Raw("footer")) + ctx := NewContext() + + b.ResetTimer() + for b.Loop() { + layout.Render(ctx) + } +} + +func BenchmarkLayout_Nested(b *testing.B) { + inner := NewLayout("HCF").H(Raw("ih")).C(Raw("ic")).F(Raw("if")) + layout := NewLayout("HLCRF"). + H(Raw("header")).L(inner).C(Raw("main")).R(Raw("right")).F(Raw("footer")) + ctx := NewContext() + + b.ResetTimer() + for b.Loop() { + layout.Render(ctx) + } +} + +func BenchmarkLayout_ManySlotChildren(b *testing.B) { + nodes := make([]Node, 50) + for i := range nodes { + nodes[i] = El("p", Raw(fmt.Sprintf("paragraph %d", i))) + } + layout := NewLayout("HLCRF"). + H(Raw("header")). + C(nodes...). + F(Raw("footer")) + ctx := NewContext() + + b.ResetTimer() + for b.Loop() { + layout.Render(ctx) + } +} + +// --- BenchmarkEach --- + +func BenchmarkEach_10(b *testing.B) { + benchEach(b, 10) +} + +func BenchmarkEach_100(b *testing.B) { + benchEach(b, 100) +} + +func BenchmarkEach_1000(b *testing.B) { + benchEach(b, 1000) +} + +func benchEach(b *testing.B, n int) { + b.Helper() + items := make([]int, n) + for i := range items { + items[i] = i + } + node := Each(items, func(i int) Node { + return El("li", Raw(fmt.Sprintf("item-%d", i))) + }) + ctx := NewContext() + + b.ResetTimer() + for b.Loop() { + node.Render(ctx) + } +} + +// --- BenchmarkResponsive --- + +func BenchmarkResponsive_ThreeVariants(b *testing.B) { + r := NewResponsive(). + Variant("desktop", NewLayout("HLCRF").H(Raw("h")).L(Raw("l")).C(Raw("c")).R(Raw("r")).F(Raw("f"))). + Variant("tablet", NewLayout("HCF").H(Raw("h")).C(Raw("c")).F(Raw("f"))). + Variant("mobile", NewLayout("C").C(Raw("c"))) + ctx := NewContext() + + b.ResetTimer() + for b.Loop() { + r.Render(ctx) + } +} + +// --- BenchmarkStripTags --- + +func BenchmarkStripTags_Short(b *testing.B) { + input := `
hello
` + for b.Loop() { + StripTags(input) + } +} + +func BenchmarkStripTags_Long(b *testing.B) { + layout := NewLayout("HLCRF"). + H(Raw("header content")).L(Raw("left sidebar")). + C(Raw("main body content with multiple words")). + R(Raw("right sidebar")).F(Raw("footer content")) + input := layout.Render(NewContext()) + + b.ResetTimer() + for b.Loop() { + StripTags(input) + } +} diff --git a/codegen/bench_test.go b/codegen/bench_test.go new file mode 100644 index 0000000..0fdaecd --- /dev/null +++ b/codegen/bench_test.go @@ -0,0 +1,46 @@ +package codegen + +import "testing" + +func BenchmarkGenerateClass(b *testing.B) { + for b.Loop() { + GenerateClass("photo-grid", "C") + } +} + +func BenchmarkTagToClassName(b *testing.B) { + for b.Loop() { + TagToClassName("my-super-widget-component") + } +} + +func BenchmarkGenerateBundle_Small(b *testing.B) { + slots := map[string]string{ + "H": "nav-bar", + "C": "main-content", + } + b.ResetTimer() + for b.Loop() { + GenerateBundle(slots) + } +} + +func BenchmarkGenerateBundle_Full(b *testing.B) { + slots := map[string]string{ + "H": "nav-bar", + "L": "side-panel", + "C": "main-content", + "R": "aside-widget", + "F": "page-footer", + } + b.ResetTimer() + for b.Loop() { + GenerateBundle(slots) + } +} + +func BenchmarkGenerateRegistration(b *testing.B) { + for b.Loop() { + GenerateRegistration("photo-grid", "PhotoGrid") + } +} diff --git a/edge_test.go b/edge_test.go new file mode 100644 index 0000000..a26797a --- /dev/null +++ b/edge_test.go @@ -0,0 +1,485 @@ +package html + +import ( + "fmt" + "strings" + "testing" + + i18n "forge.lthn.ai/core/go-i18n" +) + +// --- Unicode / RTL edge cases --- + +func TestText_Emoji(t *testing.T) { + svc, _ := i18n.New() + i18n.SetDefault(svc) + ctx := NewContext() + + tests := []struct { + name string + input string + }{ + {"simple emoji", "\U0001F680"}, + {"emoji sequence", "\U0001F468\u200D\U0001F4BB"}, + {"mixed text and emoji", "Hello \U0001F30D World"}, + {"flag emoji", "\U0001F1EC\U0001F1E7"}, + {"emoji in sentence", "Status: \u2705 Complete"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + node := Text(tt.input) + got := node.Render(ctx) + if got == "" { + t.Error("Text with emoji should not produce empty output") + } + // Emoji should pass through (they are not HTML special chars) + if !strings.Contains(got, tt.input) { + // Some chars may get escaped, but emoji bytes should survive + t.Logf("note: emoji text rendered as %q", got) + } + }) + } +} + +func TestEl_Emoji(t *testing.T) { + ctx := NewContext() + node := El("span", Raw("\U0001F680 Launch")) + got := node.Render(ctx) + want := "\U0001F680 Launch" + if got != want { + t.Errorf("El with emoji = %q, want %q", got, want) + } +} + +func TestText_RTL(t *testing.T) { + svc, _ := i18n.New() + i18n.SetDefault(svc) + ctx := NewContext() + + tests := []struct { + name string + input string + }{ + {"Arabic", "\u0645\u0631\u062D\u0628\u0627"}, + {"Hebrew", "\u05E9\u05DC\u05D5\u05DD"}, + {"mixed LTR and RTL", "Hello \u0645\u0631\u062D\u0628\u0627 World"}, + {"Arabic with numbers", "\u0627\u0644\u0639\u062F\u062F 42"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + node := Text(tt.input) + got := node.Render(ctx) + if got == "" { + t.Error("Text with RTL content should not produce empty output") + } + }) + } +} + +func TestEl_RTL(t *testing.T) { + ctx := NewContext() + node := Attr(El("div", Raw("\u0645\u0631\u062D\u0628\u0627")), "dir", "rtl") + got := node.Render(ctx) + if !strings.Contains(got, `dir="rtl"`) { + t.Errorf("RTL element missing dir attribute in: %s", got) + } + if !strings.Contains(got, "\u0645\u0631\u062D\u0628\u0627") { + t.Errorf("RTL element missing Arabic text in: %s", got) + } +} + +func TestText_ZeroWidth(t *testing.T) { + svc, _ := i18n.New() + i18n.SetDefault(svc) + ctx := NewContext() + + tests := []struct { + name string + input string + }{ + {"zero-width space", "hello\u200Bworld"}, + {"zero-width joiner", "hello\u200Dworld"}, + {"zero-width non-joiner", "hello\u200Cworld"}, + {"soft hyphen", "super\u00ADcalifragilistic"}, + {"BOM character", "\uFEFFhello"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + node := Text(tt.input) + got := node.Render(ctx) + if got == "" { + t.Error("Text with zero-width characters should not produce empty output") + } + }) + } +} + +func TestText_MixedScripts(t *testing.T) { + svc, _ := i18n.New() + i18n.SetDefault(svc) + ctx := NewContext() + + tests := []struct { + name string + input string + }{ + {"Latin + CJK", "Hello \u4F60\u597D"}, + {"Latin + Cyrillic", "Hello \u041F\u0440\u0438\u0432\u0435\u0442"}, + {"CJK + Arabic", "\u4F60\u597D \u0645\u0631\u062D\u0628\u0627"}, + {"Latin + Devanagari", "Hello \u0928\u092E\u0938\u094D\u0924\u0947"}, + {"Latin + Thai", "Hello \u0E2A\u0E27\u0E31\u0E2A\u0E14\u0E35"}, + {"all scripts mixed", "EN \u4F60\u597D \u0645\u0631\u062D\u0628\u0627 \u041F\u0440\u0438\u0432\u0435\u0442 \U0001F30D"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + node := Text(tt.input) + got := node.Render(ctx) + if got == "" { + t.Error("Text with mixed scripts should not produce empty output") + } + }) + } +} + +func TestStripTags_Unicode(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + {"emoji in tags", "\U0001F680", "\U0001F680"}, + {"RTL in tags", "
\u0645\u0631\u062D\u0628\u0627
", "\u0645\u0631\u062D\u0628\u0627"}, + {"CJK in tags", "

\u4F60\u597D\u4E16\u754C

", "\u4F60\u597D\u4E16\u754C"}, + {"mixed unicode regions", "
\U0001F680
\u4F60\u597D
", "\U0001F680 \u4F60\u597D"}, + {"zero-width in tags", "a\u200Bb", "a\u200Bb"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := StripTags(tt.input) + if got != tt.want { + t.Errorf("StripTags(%q) = %q, want %q", tt.input, got, tt.want) + } + }) + } +} + +func TestAttr_UnicodeValue(t *testing.T) { + ctx := NewContext() + node := Attr(El("div"), "title", "\U0001F680 Rocket Launch") + got := node.Render(ctx) + want := "title=\"\U0001F680 Rocket Launch\"" + if !strings.Contains(got, want) { + t.Errorf("attribute with emoji should be preserved, got: %s", got) + } +} + +// --- Deep nesting stress tests --- + +func TestLayout_DeepNesting_10Levels(t *testing.T) { + ctx := NewContext() + + // Build 10 levels of nested layouts + current := NewLayout("C").C(Raw("deepest")) + for i := 0; i < 9; i++ { + current = NewLayout("C").C(current) + } + + got := current.Render(ctx) + + // Should contain the deepest content + if !strings.Contains(got, "deepest") { + t.Error("10 levels deep: missing leaf content") + } + + // Should have 10 levels of C-0 nesting + expectedBlock := "C-0" + for i := 1; i < 10; i++ { + expectedBlock += "-C-0" + } + if !strings.Contains(got, fmt.Sprintf(`data-block="%s"`, expectedBlock)) { + t.Errorf("10 levels deep: missing expected block ID %q in:\n%s", expectedBlock, got) + } + + // Must have exactly 10
tags + if count := strings.Count(got, " tags, got %d", count) + } +} + +func TestLayout_DeepNesting_20Levels(t *testing.T) { + ctx := NewContext() + + current := NewLayout("C").C(Raw("bottom")) + for i := 0; i < 19; i++ { + current = NewLayout("C").C(current) + } + + got := current.Render(ctx) + + if !strings.Contains(got, "bottom") { + t.Error("20 levels deep: missing leaf content") + } + if count := strings.Count(got, " tags, got %d", count) + } +} + +func TestLayout_DeepNesting_MixedSlots(t *testing.T) { + ctx := NewContext() + + // Alternate slot types at each level: C -> L -> C -> L -> ... + current := NewLayout("C").C(Raw("leaf")) + for i := 0; i < 5; i++ { + if i%2 == 0 { + current = NewLayout("HLCRF").L(current) + } else { + current = NewLayout("HCF").C(current) + } + } + + got := current.Render(ctx) + if !strings.Contains(got, "leaf") { + t.Error("mixed deep nesting: missing leaf content") + } +} + +func TestEach_LargeIteration_1000(t *testing.T) { + ctx := NewContext() + items := make([]int, 1000) + for i := range items { + items[i] = i + } + + node := Each(items, func(i int) Node { + return El("li", Raw(fmt.Sprintf("%d", i))) + }) + + got := node.Render(ctx) + + if count := strings.Count(got, "
  • "); count != 1000 { + t.Errorf("Each with 1000 items: expected 1000
  • , got %d", count) + } + if !strings.Contains(got, "
  • 0
  • ") { + t.Error("Each with 1000 items: missing first item") + } + if !strings.Contains(got, "
  • 999
  • ") { + t.Error("Each with 1000 items: missing last item") + } +} + +func TestEach_LargeIteration_5000(t *testing.T) { + ctx := NewContext() + items := make([]int, 5000) + for i := range items { + items[i] = i + } + + node := Each(items, func(i int) Node { + return El("span", Raw(fmt.Sprintf("%d", i))) + }) + + got := node.Render(ctx) + + if count := strings.Count(got, ""); count != 5000 { + t.Errorf("Each with 5000 items: expected 5000 , got %d", count) + } +} + +func TestEach_NestedEach(t *testing.T) { + ctx := NewContext() + rows := []int{0, 1, 2} + cols := []string{"a", "b", "c"} + + node := Each(rows, func(row int) Node { + return El("tr", Each(cols, func(col string) Node { + return El("td", Raw(fmt.Sprintf("%d-%s", row, col))) + })) + }) + + got := node.Render(ctx) + + if count := strings.Count(got, ""); count != 3 { + t.Errorf("nested Each: expected 3 , got %d", count) + } + if count := strings.Count(got, ""); count != 9 { + t.Errorf("nested Each: expected 9 , got %d", count) + } + if !strings.Contains(got, "1-b") { + t.Error("nested Each: missing cell content '1-b'") + } +} + +// --- Layout variant validation --- + +func TestLayout_InvalidVariant_Chars(t *testing.T) { + ctx := NewContext() + + tests := []struct { + name string + variant string + }{ + {"all invalid", "XYZ"}, + {"lowercase valid", "hlcrf"}, + {"numbers", "123"}, + {"special chars", "!@#"}, + {"mixed valid and invalid", "HXC"}, + {"empty string", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + layout := NewLayout(tt.variant). + H(Raw("header")).L(Raw("left")).C(Raw("main")).R(Raw("right")).F(Raw("footer")) + got := layout.Render(ctx) + + // Invalid variant chars should silently produce no output for those slots + // This documents the current behaviour: no panic, no error. + if tt.variant == "XYZ" || tt.variant == "hlcrf" || tt.variant == "123" || + tt.variant == "!@#" || tt.variant == "" { + if got != "" { + t.Errorf("NewLayout(%q) with all invalid chars should produce empty output, got %q", tt.variant, got) + } + } + }) + } +} + +func TestLayout_InvalidVariant_MixedValidInvalid(t *testing.T) { + ctx := NewContext() + + // "HXC" — H and C are valid, X is not. Only H and C should render. + layout := NewLayout("HXC"). + H(Raw("header")).C(Raw("main")) + got := layout.Render(ctx) + + if !strings.Contains(got, "header") { + t.Errorf("HXC variant should render H slot, got:\n%s", got) + } + if !strings.Contains(got, "main") { + t.Errorf("HXC variant should render C slot, got:\n%s", got) + } + // Should only have 2 semantic elements + if count := strings.Count(got, "data-block="); count != 2 { + t.Errorf("HXC variant should produce 2 blocks, got %d in:\n%s", count, got) + } +} + +func TestLayout_DuplicateVariantChars(t *testing.T) { + ctx := NewContext() + + // "CCC" — C appears three times. Should render C slot content three times. + layout := NewLayout("CCC").C(Raw("content")) + got := layout.Render(ctx) + + count := strings.Count(got, "content") + if count != 3 { + t.Errorf("CCC variant should render C slot 3 times, got %d occurrences in:\n%s", count, got) + } +} + +func TestLayout_EmptySlots(t *testing.T) { + ctx := NewContext() + + // Variant includes all slots but none are populated — should produce empty output. + layout := NewLayout("HLCRF") + got := layout.Render(ctx) + + if got != "" { + t.Errorf("layout with no slot content should produce empty output, got %q", got) + } +} + +// --- Render convenience function edge cases --- + +func TestRender_NilContext(t *testing.T) { + node := Raw("test") + got := Render(node, nil) + if got != "test" { + t.Errorf("Render with nil context = %q, want %q", got, "test") + } +} + +func TestImprint_NilContext(t *testing.T) { + svc, _ := i18n.New() + i18n.SetDefault(svc) + + node := NewLayout("C").C(El("p", Text("Building project"))) + imp := Imprint(node, nil) + + if imp.TokenCount == 0 { + t.Error("Imprint with nil context should still produce tokens") + } +} + +func TestCompareVariants_NilContext(t *testing.T) { + svc, _ := i18n.New() + i18n.SetDefault(svc) + + r := NewResponsive(). + Variant("a", NewLayout("C").C(Text("Building project"))). + Variant("b", NewLayout("C").C(Text("Building project"))) + + scores := CompareVariants(r, nil) + if _, ok := scores["a:b"]; !ok { + t.Error("CompareVariants with nil context should still produce scores") + } +} + +func TestCompareVariants_SingleVariant(t *testing.T) { + svc, _ := i18n.New() + i18n.SetDefault(svc) + + r := NewResponsive(). + Variant("only", NewLayout("C").C(Text("Building project"))) + + scores := CompareVariants(r, NewContext()) + if len(scores) != 0 { + t.Errorf("CompareVariants with single variant should produce no pairs, got %d", len(scores)) + } +} + +// --- escapeHTML / escapeAttr edge cases --- + +func TestEscapeAttr_AllSpecialChars(t *testing.T) { + ctx := NewContext() + node := Attr(El("div"), "data-val", `&<>"'`) + got := node.Render(ctx) + + if strings.Contains(got, `"&<>"'"`) { + t.Error("attribute value with special chars must be fully escaped") + } + if !strings.Contains(got, "&<>"'") { + t.Errorf("expected all special chars escaped in attribute, got: %s", got) + } +} + +func TestElNode_EmptyTag(t *testing.T) { + ctx := NewContext() + node := El("", Raw("content")) + got := node.Render(ctx) + + // Empty tag is weird but should not panic + if !strings.Contains(got, "content") { + t.Errorf("El with empty tag should still render children, got %q", got) + } +} + +func TestSwitchNode_NoMatch(t *testing.T) { + ctx := NewContext() + cases := map[string]Node{ + "a": Raw("alpha"), + "b": Raw("beta"), + } + node := Switch(func(*Context) string { return "c" }, cases) + got := node.Render(ctx) + if got != "" { + t.Errorf("Switch with no matching case should produce empty string, got %q", got) + } +} + +func TestEntitled_NilContext(t *testing.T) { + node := Entitled("premium", Raw("content")) + got := node.Render(nil) + if got != "" { + t.Errorf("Entitled with nil context should produce empty string, got %q", got) + } +}