frontend_test.go
package frontend
import (
"os"
"strings"
"testing"
)
func TestScript_DevMode(t *testing.T) {
f := New()
f.DevMode = true
script := string(f.Script())
if !strings.Contains(script, "EventSource") {
t.Error("expected HMR EventSource code in dev mode script")
}
if !strings.Contains(script, "/_frontend/hmr") {
t.Error("expected HMR SSE endpoint in dev mode script")
}
if !strings.Contains(script, "__renderAll") {
t.Error("expected __renderAll orchestration in script")
}
if !strings.Contains(script, "/_frontend/components.js") {
t.Error("expected components bundle script tag")
}
}
func TestScript_ProdMode(t *testing.T) {
f := New()
f.DevMode = false
script := string(f.Script())
if strings.Contains(script, "EventSource") {
t.Error("expected no HMR EventSource code in production mode")
}
if strings.Contains(script, "/_frontend/hmr") {
t.Error("expected no HMR SSE endpoint in production mode")
}
// Orchestration and bundle should still be present
if !strings.Contains(script, "__renderAll") {
t.Error("expected __renderAll orchestration in production script")
}
if !strings.Contains(script, "/_frontend/components.js") {
t.Error("expected components bundle script tag in production script")
}
}
func TestScript_CacheBusting(t *testing.T) {
// Create a temporary output directory with a components.js file
tmpDir := t.TempDir()
f := New()
f.OutputDir = tmpDir
f.DevMode = false
// Write a fake bundle file
if err := os.WriteFile(tmpDir+"/components.js", []byte("console.log('test')"), 0644); err != nil {
t.Fatalf("failed to write test bundle: %v", err)
}
script := string(f.Script())
if !strings.Contains(script, "?v=") {
t.Error("expected cache-busting hash in script tag when bundle exists")
}
}
func TestRender(t *testing.T) {
f := New()
t.Run("with props", func(t *testing.T) {
html := string(f.Render("Counter", map[string]any{"initial": 5}))
if !strings.Contains(html, `data-component="Counter"`) {
t.Error("expected data-component attribute with component name")
}
if !strings.Contains(html, `data-props=`) {
t.Error("expected data-props attribute")
}
// Props are HTML-escaped in the data-props attribute
if !strings.Contains(html, "initial") {
t.Error("expected props to contain 'initial' key")
}
if !strings.Contains(html, `skeleton`) {
t.Error("expected skeleton placeholder in island container")
}
})
t.Run("nil props", func(t *testing.T) {
html := string(f.Render("Button", nil))
if !strings.Contains(html, `data-component="Button"`) {
t.Error("expected data-component attribute")
}
if !strings.Contains(html, `{}`) {
t.Error("expected empty props object for nil props")
}
})
t.Run("html escaping", func(t *testing.T) {
html := string(f.Render(`<script>alert("xss")</script>`, nil))
if strings.Contains(html, `<script>alert`) {
t.Error("expected component name to be HTML-escaped")
}
})
}
func TestWithBundler(t *testing.T) {
// WithBundler registers HTTP handlers on DefaultServeMux, which conflicts
// with parallel tests. We test it returns a non-nil option function.
// Note: We cannot call WithBundler multiple times in the same test process
// because it registers routes on the default mux. We test the factory indirectly.
f := New()
if f.SourceDir != "components" {
t.Errorf("expected default SourceDir 'components', got %q", f.SourceDir)
}
if f.OutputDir != "views/static/scripts/gen" {
t.Errorf("expected default OutputDir 'views/static/scripts/gen', got %q", f.OutputDir)
}
if f.clients == nil {
t.Error("expected clients map to be initialized")
}
}
func TestNew(t *testing.T) {
// Ensure ENV=production sets DevMode correctly
original := os.Getenv("ENV")
defer os.Setenv("ENV", original)
os.Setenv("ENV", "production")
f := New()
if f.DevMode {
t.Error("expected DevMode to be false when ENV=production")
}
os.Setenv("ENV", "")
f = New()
if !f.DevMode {
t.Error("expected DevMode to be true when ENV is not production")
}
}
func TestNotifyClients(t *testing.T) {
f := New()
// Register a mock client
ch := make(chan struct{}, 1)
f.mu.Lock()
f.clients[ch] = struct{}{}
f.mu.Unlock()
f.notifyClients()
select {
case <-ch:
// Successfully received notification
default:
t.Error("expected client channel to receive notification")
}
}