package h import ( "strings" "sync" "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 := `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