package h import ( "strings" "sync" "sync/atomic" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" ) func TestRendererShouldRenderDocType(t *testing.T) { t.Parallel() result := Render(Html( Div(), ), WithDocType()) assert.Equal(t, `
`, result) } func TestSimpleRender(t *testing.T) { t.Parallel() result := Render( Div(Attribute("id", "my-div"), Attribute("class", "my-class")), ) assert.Equal(t, `
`, result) } func TestRender(t *testing.T) { t.Parallel() div := Div( Id("my-div"), Attribute("data-attr-2", "value"), Attributes(&AttributeMap{ "data-attr-3": "value", }), HxBeforeRequest( SetText("before request"), ), HxAfterRequest( SetText("after request"), ), Children( Div(Text("hello, world")), ), Text("hello, child"), ) div.attributes.Set("data-attr-1", "value") expected := `
hello, world
hello, child
` result := Render(div) assert.Equal(t, expected, strings.ReplaceAll(result, "var self=this;var e=event;", "")) } func TestRenderAttributes_1(t *testing.T) { t.Parallel() div := Div( AttributePairs("class", "bg-red-500"), Attributes(&AttributeMap{ "id": Id("my-div"), }), Attribute("disabled", "true"), ) assert.Equal(t, `
`, Render(div), ) } func TestRenderAttributes_2(t *testing.T) { div := Div( AttributePairs("class", "bg-red-500", "id", "my-div"), Button( AttributePairs("class", "bg-blue-500", "id", "my-button"), Text("Click me"), Attribute("disabled", "true"), Attribute("data-attr", "value"), ), ) assert.Equal(t, `
`, Render(div)) } func TestRenderEmptyDiv(t *testing.T) { t.Parallel() assert.Equal(t, `
`, Render(Div()), ) } func TestRenderVoidElement(t *testing.T) { t.Parallel() assert.Equal(t, ``, Render(Input("text")), ) assert.Equal(t, ``, Render(Tag("input"))) } func TestRawContent(t *testing.T) { t.Parallel() str := "
hello, world
" raw := UnsafeRaw(str) assert.Equal(t, str, Render(raw)) } func TestConditional(t *testing.T) { t.Parallel() result := Render( Div( Ternary(true, Text("true"), Text("false")), ), ) assert.Equal(t, "
true
", result) result = Render( Div( If(false, Text("true")), ), ) assert.Equal(t, "
", result) } func TestTagSelfClosing(t *testing.T) { t.Parallel() assert.Equal(t, ``, Render( Input("text"), )) // assert the tag cannot have children assert.Equal(t, ``, Render( Input("text", Div()), )) assert.Equal(t, `
`, Render( Div(Id("test")), )) assert.Equal(t, `
`, Render( Div(Id("test"), Div()), )) } func TestCached(t *testing.T) { t.Parallel() count := 0 page := Cached(time.Hour, func() *Element { count++ return ComplexPage() }) firstRender := Render(page()) secondRender := Render(page()) assert.Equal(t, firstRender, secondRender) assert.Equal(t, 1, count) assert.Equal(t, firstRender, Render(ComplexPage())) } func TestCachedT(t *testing.T) { t.Parallel() count := 0 page := CachedT(time.Hour, func(a string) *Element { count++ return ComplexPage() }) firstRender := Render(page("a")) secondRender := Render(page("a")) assert.Equal(t, firstRender, secondRender) assert.Equal(t, 1, count) assert.Equal(t, firstRender, Render(ComplexPage())) } func TestCachedT2(t *testing.T) { t.Parallel() count := 0 page := CachedT2(time.Hour, func(a string, b string) *Element { count++ return ComplexPage() }) firstRender := Render(page("a", "b")) secondRender := Render(page("a", "b")) assert.Equal(t, firstRender, secondRender) assert.Equal(t, 1, count) assert.Equal(t, firstRender, Render(ComplexPage())) } func TestCachedT3(t *testing.T) { t.Parallel() count := 0 page := CachedT3(time.Hour, func(a string, b string, c string) *Element { count++ return ComplexPage() }) firstRender := Render(page("a", "b", "c")) secondRender := Render(page("a", "b", "c")) assert.Equal(t, firstRender, secondRender) assert.Equal(t, 1, count) assert.Equal(t, firstRender, Render(ComplexPage())) } func TestCachedT4(t *testing.T) { t.Parallel() count := 0 page := CachedT4(time.Hour, func(a string, b string, c string, d string) *Element { count++ return ComplexPage() }) firstRender := Render(page("a", "b", "c", "d")) secondRender := Render(page("a", "b", "c", "d")) assert.Equal(t, firstRender, secondRender) assert.Equal(t, 1, count) assert.Equal(t, firstRender, Render(ComplexPage())) } func TestCachedExpired(t *testing.T) { t.Parallel() count := 0 page := Cached(time.Millisecond*3, func() *Element { count++ return ComplexPage() }) firstRender := Render(page()) time.Sleep(time.Millisecond * 5) secondRender := Render(page()) assert.Equal(t, firstRender, secondRender) assert.Equal(t, 2, count) } func TestCacheMultiple(t *testing.T) { t.Parallel() count := 0 cachedItem := Cached(time.Hour, func() *Element { count++ return Div(Text("hello")) }) wg := sync.WaitGroup{} for i := 0; i < 2; i++ { wg.Add(1) go func() { defer wg.Done() Render(Div( cachedItem(), cachedItem(), cachedItem(), )) }() } wg.Wait() assert.Equal(t, 1, count) } func TestCacheByKey(t *testing.T) { t.Parallel() renderCount := 0 callCount := 0 cachedItem := CachedPerKey(time.Hour, func() (any, GetElementFunc) { key := "key" if callCount == 3 { key = "key2" } if callCount == 4 { key = "key" } callCount++ return key, func() *Element { renderCount++ return Div(Text("hello")) } }) Render(Div( cachedItem(), cachedItem(), cachedItem(), cachedItem(), cachedItem(), )) assert.Equal(t, 5, callCount) assert.Equal(t, 2, renderCount) } func TestCacheByKeyT(t *testing.T) { t.Parallel() renderCount := 0 cachedItem := CachedPerKeyT(time.Hour, func(key string) (any, GetElementFunc) { return key, func() *Element { renderCount++ return Div(Text("hello")) } }) Render(Div( cachedItem("one"), cachedItem("one"), cachedItem("two"), cachedItem("two"), cachedItem("three"), )) assert.Equal(t, 3, renderCount) } func TestCacheByKeyT4(t *testing.T) { t.Parallel() renderCount := 0 cachedItem := CachedPerKeyT4(time.Hour, func(arg1 string, arg2 string, arg3 string, arg4 string) (any, GetElementFunc) { return arg1, func() *Element { renderCount++ return Div(Text("hello")) } }) Render(Div( cachedItem("one", uuid.NewString(), uuid.NewString(), uuid.NewString()), cachedItem("one", uuid.NewString(), uuid.NewString(), uuid.NewString()), cachedItem("two", uuid.NewString(), uuid.NewString(), uuid.NewString()), cachedItem("two", uuid.NewString(), uuid.NewString(), uuid.NewString()), cachedItem("three", uuid.NewString(), uuid.NewString(), uuid.NewString()), )) assert.Equal(t, 3, renderCount) } func TestCacheByKeyT3(t *testing.T) { t.Parallel() renderCount := 0 cachedItem := CachedPerKeyT3(time.Hour, func(arg1 string, arg2 string, arg3 string) (any, GetElementFunc) { return arg1, func() *Element { renderCount++ return Div(Text("hello")) } }) Render(Div( cachedItem("one", uuid.NewString(), uuid.NewString()), cachedItem("one", uuid.NewString(), uuid.NewString()), cachedItem("two", uuid.NewString(), uuid.NewString()), cachedItem("two", uuid.NewString(), uuid.NewString()), cachedItem("three", uuid.NewString(), uuid.NewString()), )) assert.Equal(t, 3, renderCount) } func TestCacheByKeyT2(t *testing.T) { t.Parallel() renderCount := 0 cachedItem := CachedPerKeyT2(time.Hour, func(arg1 string, arg2 string) (any, GetElementFunc) { return arg1, func() *Element { renderCount++ return Div(Text("hello")) } }) Render(Div( cachedItem("one", uuid.NewString()), cachedItem("one", uuid.NewString()), cachedItem("two", uuid.NewString()), cachedItem("two", uuid.NewString()), cachedItem("three", uuid.NewString()), )) assert.Equal(t, 3, renderCount) } func TestCacheByKeyConcurrent(t *testing.T) { t.Parallel() var renderCount, callCount atomic.Uint32 cachedItem := CachedPerKey(time.Hour, func() (any, GetElementFunc) { fn := func() *Element { renderCount.Add(1) return Div(Text("hello")) } switch callCount.Add(1) { case 4: return "key2", fn default: return "key", fn } }) wg := sync.WaitGroup{} for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() Render(Div( cachedItem(), )) }() } wg.Wait() assert.Equal(t, 5, int(callCount.Load())) assert.Equal(t, 2, int(renderCount.Load())) } func TestCacheByKeyT1_2(t *testing.T) { t.Parallel() renderCount := 0 cachedItem := CachedPerKeyT(time.Hour, func(key string) (any, GetElementFunc) { return key, func() *Element { renderCount++ return Pf(key) } }) assert.Equal(t, "

one

", Render(cachedItem("one"))) assert.Equal(t, "

two

", Render(cachedItem("two"))) assert.Equal(t, "

two

", Render(cachedItem("two"))) assert.Equal(t, 2, renderCount) } func TestCacheByKeyT1Expired(t *testing.T) { t.Parallel() renderCount := 0 cachedItem := CachedPerKeyT(time.Millisecond, func(key string) (any, GetElementFunc) { return key, func() *Element { renderCount++ return Pf(key) } }) assert.Equal(t, "

one

", Render(cachedItem("one"))) assert.Equal(t, "

two

", Render(cachedItem("two"))) time.Sleep(time.Millisecond * 2) assert.Equal(t, "

two

", Render(cachedItem("two"))) assert.Equal(t, 3, renderCount) } func TestCacheByKeyT1Expired_2(t *testing.T) { t.Parallel() renderCount := 0 cachedItem := CachedPerKeyT(time.Millisecond*5, func(key string) (any, GetElementFunc) { return key, func() *Element { renderCount++ return Pf(key) } }) assert.Equal(t, "

one

", Render(cachedItem("one"))) time.Sleep(time.Millisecond * 3) assert.Equal(t, "

two

", Render(cachedItem("two"))) assert.Equal(t, "

two

", Render(cachedItem("two"))) assert.Equal(t, "

two

", Render(cachedItem("two"))) time.Sleep(time.Millisecond * 3) assert.Equal(t, "

one

", Render(cachedItem("one"))) assert.Equal(t, "

two

", Render(cachedItem("two"))) assert.Equal(t, 3, renderCount) } func TestClearExpiredCached(t *testing.T) { renderCount := 0 cachedItem := Cached(time.Millisecond*2, func() *Element { renderCount++ return Div(Text("hello")) }) // First render Render(cachedItem()) assert.Equal(t, 1, renderCount) // Should use cache immediately Render(cachedItem()) assert.Equal(t, 1, renderCount) // Wait for expiration time.Sleep(time.Millisecond * 3) // Should re-render after expiration Render(cachedItem()) assert.Equal(t, 2, renderCount) } func TestClearExpiredCacheByKey(t *testing.T) { renderCount := 0 // Create two cached functions with different TTLs shortLivedCache := CachedPerKeyT(time.Millisecond*1, func(key int) (int, GetElementFunc) { return key, func() *Element { renderCount++ return Div(Text("short-lived")) } }) longLivedCache := CachedPerKeyT(time.Hour, func(key int) (int, GetElementFunc) { return key, func() *Element { renderCount++ return Div(Text("long-lived")) } }) // Render 100 short-lived items for i := 0; i < 100; i++ { Render(shortLivedCache(i)) } assert.Equal(t, 100, renderCount) // Render a long-lived item Render(longLivedCache(999)) assert.Equal(t, 101, renderCount) // Wait for expiration of the short-lived items time.Sleep(time.Millisecond * 3) // Re-render some expired items - should trigger new renders for i := 0; i < 10; i++ { Render(shortLivedCache(i)) } assert.Equal(t, 111, renderCount) // 101 + 10 re-renders // The long-lived item should still be cached Render(longLivedCache(999)) assert.Equal(t, 111, renderCount) // No additional render // Clear cache manually on both shortNode := shortLivedCache(0).meta.(*ByKeyEntry).parent.meta.(*CachedNode) shortNode.ClearCache() longNode := longLivedCache(0).meta.(*ByKeyEntry).parent.meta.(*CachedNode) longNode.ClearCache() // Everything should re-render now Render(shortLivedCache(0)) assert.Equal(t, 112, renderCount) Render(longLivedCache(999)) assert.Equal(t, 113, renderCount) } func TestBackgroundCleaner(t *testing.T) { renderCount := 0 cachedItem := CachedPerKeyT(time.Millisecond*100, func(key int) (int, GetElementFunc) { return key, func() *Element { renderCount++ return Div(Text("hello")) } }) // Render 100 items for i := 0; i < 100; i++ { Render(cachedItem(i)) } assert.Equal(t, 100, renderCount) // Items should be cached immediately for i := 0; i < 10; i++ { Render(cachedItem(i)) } assert.Equal(t, 100, renderCount) // No additional renders // Wait for expiration and cleanup time.Sleep(time.Second * 3) // Items should be expired and need re-rendering for i := 0; i < 10; i++ { Render(cachedItem(i)) } assert.Equal(t, 110, renderCount) // 10 re-renders after expiration } func TestEscapeHtml(t *testing.T) { t.Parallel() assert.Equal(t, "<script>alert(1)</script>", Render(Text(""))) assert.Equal(t, "

<script>alert(1)</script>

", Render(Pf(""))) } func BenchmarkCacheByKey(b *testing.B) { b.ReportAllocs() page := CachedPerKeyT(time.Second*3, func(userId string) (any, GetElementFunc) { return userId, func() *Element { return MailTo(userId) } }) for i := 0; i < 5000; i++ { userId := uuid.NewString() Render(page(userId)) } Render(page(uuid.NewString())) } func BenchmarkMailToStatic(b *testing.B) { b.ReportAllocs() ctx := RenderContext{ builder: &strings.Builder{}, } page := MailTo("myemail") for i := 0; i < b.N; i++ { page.Render(&ctx) ctx.builder.Reset() } } func BenchmarkMailToDynamic(b *testing.B) { b.ReportAllocs() ctx := RenderContext{ builder: &strings.Builder{}, } for i := 0; i < b.N; i++ { MailTo(uuid.NewString()).Render(&ctx) ctx.builder.Reset() } } func BenchmarkCachedComplexPage(b *testing.B) { b.ReportAllocs() ctx := RenderContext{ builder: &strings.Builder{}, } for i := 0; i < b.N; i++ { CachedComplexPage().Render(&ctx) ctx.builder.Reset() } } func BenchmarkComplexPage(b *testing.B) { b.ReportAllocs() ctx := RenderContext{ builder: &strings.Builder{}, } for i := 0; i < b.N; i++ { ComplexPage().Render(&ctx) ctx.builder.Reset() } } var CachedComplexPage = Cached(time.Hour, func() *Element { return ComplexPage() }) func ComplexPage() *Element { return Html( Head( Meta("title", "Complex Page"), Meta( "charset", "UTF-8", ), Meta( "viewport", "width=device-width, initial-scale=1.0", ), Link( "stylesheet", "https://example.com/styles.css", ), ), Body( Header( Class("bg-gray-800 text-white py-4"), Div( Class("container mx-auto"), H1(Class("text-3xl font-bold"), Text("Welcome to the Complex Page")), Nav( Ul( Class("flex space-x-4"), Li(A(Href("#"), Text("Home"))), Li(A(Href("#"), Text("About"))), Li(A(Href("#"), Text("Services"))), Li(A(Href("#"), Text("Contact"))), ), ), ), ), Main( Class("container mx-auto mt-10"), Section( Class("grid grid-cols-3 gap-4"), Article( Class("col-span-2"), H2(Class("text-2xl font-semibold mb-4"), Text("Featured Article")), Img(Src("https://example.com/featured.jpg"), Alt("Featured Image")), P(Class("mt-2 text-lg"), Text("This is a large article to test rendering performance.")), ), Aside( Class("bg-gray-100 p-4"), H3(Class("text-xl font-bold"), Text("Related Links")), Ul( Li(A(Href("#"), Text("Related Link 1"))), Li(A(Href("#"), Text("Related Link 2"))), Li(A(Href("#"), Text("Related Link 3"))), ), ), ), Section( Class("my-8"), H2(Class("text-2xl font-semibold mb-4"), Text("User Registration Form")), Form( Post("/register", "click"), Div( Class("grid grid-cols-2 gap-4"), Label(For("first_name"), Text("First Name")), Input("text", Id("first_name"), Name("first_name"), Class("border p-2 w-full")), Label(For("last_name"), Text("Last Name")), Input("text", Id("last_name"), Name("last_name"), Class("border p-2 w-full")), Label(For("email"), Text("Email")), Input("email", Id("email"), Name("email"), Class("border p-2 w-full")), Label(For("password"), Text("Password")), Input("password", Id("password"), Name("password"), Class("border p-2 w-full")), ), Button( Type("submit"), Class("bg-blue-500 text-white py-2 px-4 mt-4"), Text("Register"), ), ), ), Section( Class("my-8"), H2(Class("text-2xl font-semibold mb-4"), Text("Data Table")), Table( Class("table-auto w-full border-collapse border"), THead( Tr( Th(Class("border px-4 py-2"), Text("ID")), Th(Class("border px-4 py-2"), Text("Name")), Th(Class("border px-4 py-2"), Text("Age")), Th(Class("border px-4 py-2"), Text("Occupation")), ), ), TBody( Tr( Td(Class("border px-4 py-2"), Text("1")), Td(Class("border px-4 py-2"), Text("John Doe")), Td(Class("border px-4 py-2"), Text("28")), Td(Class("border px-4 py-2"), Text("Engineer")), ), Tr( Td(Class("border px-4 py-2"), Text("2")), Td(Class("border px-4 py-2"), Text("Jane Smith")), Td(Class("border px-4 py-2"), Text("34")), Td(Class("border px-4 py-2"), Text("Designer")), ), Tr( Td(Class("border px-4 py-2"), Text("3")), Td(Class("border px-4 py-2"), Text("Alice Johnson")), Td(Class("border px-4 py-2"), Text("45")), Td(Class("border px-4 py-2"), Text("Manager")), ), ), ), ), ), Footer( Class("bg-gray-800 text-white py-4 mt-10"), Div( Class("container mx-auto text-center"), Text("© 2024 Complex Page Inc. All rights reserved."), ), ), ), ) } func MailTo(email string) *Element { return Div( H1( Text("Contact Us"), ), Div( Style("font-family: 'sans-serif'"), Id("test"), Attribute("data-contents", `something with "quotes" and a `), Div( Text("email:"), A( Href(email), Text("Email me"), ), ), ), Hr( Attribute("noshade", ""), ), Hr( Attribute("optionA", ""), Attribute("optionB", ""), Attribute("optionC", "other"), ), Hr( Attribute("noshade", ""), ), ) }