readysite / pkg / frontend / frontend_test.go
4.3 KB
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")
	}
}
← Back