Add hot reload for development
Implement automatic rebuild and browser reload during development:
- File watcher monitors .go files for changes with configurable extensions
- Builder compiles Go source to WASM on file changes
- LiveReload WebSocket server notifies connected browsers to reload
- DevServer combines all components for easy development setup
- HTML injection adds reload script automatically
Usage:
dev := host.NewDevServer("public", "index.html", ".", "public/app.wasm")
dev.ListenAndServe(":8080")
Closes #9
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
216
host/watcher_test.go
Normal file
216
host/watcher_test.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package host
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWatcher_DetectsNewFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create initial file
|
||||
initialFile := filepath.Join(dir, "initial.go")
|
||||
if err := os.WriteFile(initialFile, []byte("package main"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var changeCount int32
|
||||
w := NewWatcher(dir, func() {
|
||||
atomic.AddInt32(&changeCount, 1)
|
||||
}, WithInterval(50*time.Millisecond))
|
||||
|
||||
if err := w.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Stop()
|
||||
|
||||
// Create new file
|
||||
newFile := filepath.Join(dir, "new.go")
|
||||
if err := os.WriteFile(newFile, []byte("package main"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for detection
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
if count := atomic.LoadInt32(&changeCount); count == 0 {
|
||||
t.Error("Expected change to be detected for new file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatcher_DetectsModifiedFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create initial file
|
||||
file := filepath.Join(dir, "test.go")
|
||||
if err := os.WriteFile(file, []byte("package main"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var changeCount int32
|
||||
w := NewWatcher(dir, func() {
|
||||
atomic.AddInt32(&changeCount, 1)
|
||||
}, WithInterval(50*time.Millisecond))
|
||||
|
||||
if err := w.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Stop()
|
||||
|
||||
// Ensure mod time will be different
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Modify file
|
||||
if err := os.WriteFile(file, []byte("package main\n// updated"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for detection
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
if count := atomic.LoadInt32(&changeCount); count == 0 {
|
||||
t.Error("Expected change to be detected for modified file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatcher_DetectsDeletedFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create initial file
|
||||
file := filepath.Join(dir, "test.go")
|
||||
if err := os.WriteFile(file, []byte("package main"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var changeCount int32
|
||||
w := NewWatcher(dir, func() {
|
||||
atomic.AddInt32(&changeCount, 1)
|
||||
}, WithInterval(50*time.Millisecond))
|
||||
|
||||
if err := w.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Stop()
|
||||
|
||||
// Delete file
|
||||
if err := os.Remove(file); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for detection
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
if count := atomic.LoadInt32(&changeCount); count == 0 {
|
||||
t.Error("Expected change to be detected for deleted file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatcher_IgnoresNonMatchingExtensions(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create initial go file
|
||||
goFile := filepath.Join(dir, "main.go")
|
||||
if err := os.WriteFile(goFile, []byte("package main"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var changeCount int32
|
||||
w := NewWatcher(dir, func() {
|
||||
atomic.AddInt32(&changeCount, 1)
|
||||
}, WithInterval(50*time.Millisecond), WithExtensions(".go"))
|
||||
|
||||
if err := w.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Stop()
|
||||
|
||||
// Create non-matching file
|
||||
txtFile := filepath.Join(dir, "readme.txt")
|
||||
if err := os.WriteFile(txtFile, []byte("readme"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
if count := atomic.LoadInt32(&changeCount); count != 0 {
|
||||
t.Error("Expected no change detected for non-matching extension")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatcher_CustomExtensions(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create initial file
|
||||
file := filepath.Join(dir, "style.css")
|
||||
if err := os.WriteFile(file, []byte("body {}"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var changeCount int32
|
||||
w := NewWatcher(dir, func() {
|
||||
atomic.AddInt32(&changeCount, 1)
|
||||
}, WithInterval(50*time.Millisecond), WithExtensions(".css", ".html"))
|
||||
|
||||
if err := w.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Stop()
|
||||
|
||||
// Ensure mod time will be different
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Modify css file
|
||||
if err := os.WriteFile(file, []byte("body { color: red; }"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for detection
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
if count := atomic.LoadInt32(&changeCount); count == 0 {
|
||||
t.Error("Expected change to be detected for custom extension")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatcher_SkipsHiddenDirs(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create hidden directory with go file
|
||||
hiddenDir := filepath.Join(dir, ".hidden")
|
||||
if err := os.MkdirAll(hiddenDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a visible go file first
|
||||
visibleFile := filepath.Join(dir, "visible.go")
|
||||
if err := os.WriteFile(visibleFile, []byte("package main"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var changeCount int32
|
||||
w := NewWatcher(dir, func() {
|
||||
atomic.AddInt32(&changeCount, 1)
|
||||
}, WithInterval(50*time.Millisecond))
|
||||
|
||||
if err := w.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Stop()
|
||||
|
||||
// Create file in hidden directory
|
||||
hiddenFile := filepath.Join(hiddenDir, "hidden.go")
|
||||
if err := os.WriteFile(hiddenFile, []byte("package main"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
if count := atomic.LoadInt32(&changeCount); count != 0 {
|
||||
t.Error("Expected no change detected for hidden directory")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user