wails/v3/pkg/application/application_server_test.go
Lea Anthony 9a363d7be5
feat(v3): add server mode for headless HTTP deployment (#4903)
* feat(v3): add server mode for headless HTTP deployment

Server mode allows Wails applications to run as pure HTTP servers
without native GUI dependencies. Enable with `-tags server` build tag.

Features:
- HTTP server with configurable host/port via ServerOptions
- WAILS_SERVER_HOST and WAILS_SERVER_PORT env var overrides
- WebSocket event broadcasting to connected browsers
- Browser clients represented as BrowserWindow (Window interface)
- Health check endpoint at /health
- Graceful shutdown with configurable timeout
- Docker support with Dockerfile.server template and tasks

Build and run:
  wails3 task build:server
  wails3 task run:server
  wails3 task build:docker
  wails3 task run:docker

Documentation at docs/guides/server-build.mdx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(v3): add server mode for headless HTTP deployment

Server mode allows Wails applications to run as pure HTTP servers
without native GUI dependencies. Enable with `-tags server` build tag.

Features:
- HTTP server with configurable host/port via ServerOptions
- WAILS_SERVER_HOST and WAILS_SERVER_PORT env var overrides
- WebSocket event broadcasting to connected browsers
- Browser clients represented as BrowserWindow (Window interface)
- Health check endpoint at /health
- Graceful shutdown with configurable timeout
- Docker support with Dockerfile.server template and tasks

Build and run:
  wails3 task build:server
  wails3 task run:server
  wails3 task build:docker
  wails3 task run:docker

Documentation at docs/guides/server-build.mdx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address CodeRabbit review comments

- Fix corrupted test file with embedded terminal output
- Fix module name mismatch in gin-routing (was gin-example)
- Fix replace directive version mismatch in gin-service
- Fix placeholder module name in ios example (was changeme)
- Fix Dockerfile COPY path to work from both build contexts
- Fix bare URL in README (MD034 compliance)
- Fix comment accuracy in getScreens (returns error, not empty slice)
- Remove deprecated docker-compose version field
- Add port documentation in Taskfile template

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address CodeRabbit review comments

- Add note about healthcheck wget not being available in distroless images
- Add !server build constraint to menu_windows.go and menu_darwin.go
- Downgrade window-visibility-test go.mod from 1.25 to 1.24 to match CI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 14:33:44 +11:00

168 lines
3.4 KiB
Go

//go:build server
package application
import (
"context"
"net/http"
"sync"
"testing"
"time"
)
// resetGlobalApp resets the global application state for testing
func resetGlobalApp() {
globalApplication = nil
}
func TestServerMode_HealthEndpoint(t *testing.T) {
resetGlobalApp()
// Create a server mode app (server mode is enabled via build tag)
app := New(Options{
Name: "Test Server",
Server: ServerOptions{
Host: "127.0.0.1",
Port: 18081, // Use specific port for this test
},
Assets: AssetOptions{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}),
},
})
// Start app in background
var wg sync.WaitGroup
wg.Add(1)
errCh := make(chan error, 1)
go func() {
defer wg.Done()
errCh <- app.Run()
}()
// Wait for server to start
time.Sleep(200 * time.Millisecond)
// Test health endpoint
resp, err := http.Get("http://127.0.0.1:18081/health")
if err != nil {
t.Fatalf("health check failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
// Shutdown
app.Quit()
// Wait for shutdown
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case err := <-errCh:
if err != nil {
t.Errorf("app.Run() returned error: %v", err)
}
case <-ctx.Done():
t.Error("timeout waiting for app shutdown")
}
}
func TestServerMode_AssetServing(t *testing.T) {
resetGlobalApp()
testContent := "Hello from server mode!"
app := New(Options{
Name: "Test Assets",
Server: ServerOptions{
Host: "127.0.0.1",
Port: 18082, // Use specific port for this test
},
Assets: AssetOptions{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(testContent))
}),
},
})
errCh := make(chan error, 1)
go func() {
errCh <- app.Run()
}()
time.Sleep(200 * time.Millisecond)
// Test asset serving
resp, err := http.Get("http://127.0.0.1:18082/")
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
app.Quit()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case <-errCh:
case <-ctx.Done():
t.Error("timeout waiting for app shutdown")
}
}
func TestServerMode_Defaults(t *testing.T) {
resetGlobalApp()
app := New(Options{
Name: "Test Defaults",
Server: ServerOptions{
Port: 18083, // Use specific port to avoid conflicts
},
Assets: AssetOptions{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}),
},
})
errCh := make(chan error, 1)
go func() {
errCh <- app.Run()
}()
time.Sleep(200 * time.Millisecond)
// Should be listening on localhost:18083
resp, err := http.Get("http://localhost:18083/health")
if err != nil {
t.Fatalf("request to address failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
app.Quit()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case <-errCh:
case <-ctx.Done():
t.Error("timeout waiting for app shutdown")
}
}