From f7b5b24dd5c91491d1125177d12d6ccac2dfbbaa Mon Sep 17 00:00:00 2001 From: Jeff Winkler Date: Sat, 15 Nov 2025 13:47:49 -0500 Subject: [PATCH] Script, GH action to check that all go files compile, and any tests pass. (#16) --- .github/workflows/ci.yml | 29 +++++ .gitignore | 10 ++ ci-check.sh | 56 ++++++++++ context.go | 13 ++- internal/examples/chatroom/main.go | 30 ++---- internal/examples/picocss/main.go | 1 - via.go | 6 ++ via_test.go | 165 ++++++++--------------------- 8 files changed, 160 insertions(+), 150 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100755 ci-check.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e21c1fc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + +permissions: + contents: read + +jobs: + build-test: + name: Build and Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.25.4' + cache: true + + - name: Run CI checks + run: bash ./ci-check.sh diff --git a/.gitignore b/.gitignore index af4648d..3e7241e 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,13 @@ go.work.sum # Air artifacts *tmp/ + +# binaries +internal/examples/chatroom/chatroom +internal/examples/counter/counter +internal/examples/countercomp/countercomp +internal/examples/greeter/greeter +internal/examples/livereload/livereload +internal/examples/plugins/plugins +internal/examples/realtimechart/realtimechart +internal/examples/picocss/picocss \ No newline at end of file diff --git a/ci-check.sh b/ci-check.sh new file mode 100755 index 0000000..74c481b --- /dev/null +++ b/ci-check.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -e +set -u +set -o pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$ROOT" + +echo "== CI: Format code ==" +go fmt ./... +echo "OK: formatting complete" + +echo "== CI: Run go vet ==" +if ! go vet ./...; then + echo "ERROR: go vet failed." + exit 1 +fi +echo "OK: go vet passed" + +echo "== CI: Build all packages ==" +if ! go build ./...; then + echo "ERROR: go build ./... failed." + exit 1 +fi +echo "OK: packages built" + +echo "== CI: Build example apps under internal/examples ==" +if [ -d "internal/examples" ]; then + count=0 + while IFS= read -r -d '' mainfile; do + dir="$(dirname "$mainfile")" + echo "Building $dir" + if ! (cd "$dir" && go build); then + echo "ERROR: example build failed: $dir" + exit 1 + fi + count=$((count + 1)) + done < <(find internal/examples -type f -name "main.go" -print0) + + if [ "$count" -eq 0 ]; then + echo "NOTE: no example main.go files found under internal/examples" + else + echo "OK: built $count example(s)" + fi +else + echo "NOTE: internal/examples not found, skipping example builds" +fi + +echo "== CI: Run tests ==" +if ! go test ./... 2>&1 | grep -v '\[no test files\]'; then + echo "ERROR: tests failed." + exit 1 +fi + +echo "SUCCESS: All checks passed." +exit 0 diff --git a/context.go b/context.go index 564f4f5..f9e0ff6 100644 --- a/context.go +++ b/context.go @@ -273,13 +273,17 @@ func (c *Context) SyncElements(elem h.H) { // SyncSignals pushes the current signal changes to the browser immediately // over the live SSE event stream. func (c *Context) SyncSignals() { - sse := c.getSSE() - if sse == nil { + patchChan := c.getPatchChan() + if patchChan == nil { c.app.logWarn(c, "signals out of sync: no sse stream") return } updatedSigs := make(map[string]any) - for id, sig := range c.signals { + + c.signals.Range(func(idAny, val any) bool { + // We know the types. + sig, _ := val.(*signal) // adjust *Signal to your actual signal type + id, _ := val.(string) if sig.err != nil { c.app.logWarn(c, "signal out of sync'%s': %v", sig.id, sig.err) } @@ -287,7 +291,8 @@ func (c *Context) SyncSignals() { updatedSigs[id] = fmt.Sprintf("%v", sig.v) sig.changed = false } - } + return true // continue iteration + }) if len(updatedSigs) != 0 { outgoingSignals, _ := json.Marshal(updatedSigs) patchChan <- patch{patchTypeSignals, string(outgoingSignals)} diff --git a/internal/examples/chatroom/main.go b/internal/examples/chatroom/main.go index acf8540..23e09df 100644 --- a/internal/examples/chatroom/main.go +++ b/internal/examples/chatroom/main.go @@ -83,11 +83,6 @@ func main() { } `)), ) - // Uncomment for inspector. Plugin candidate. - // v.AppendToFoot( - // h.Script(h.Src("https://cdn.jsdelivr.net/gh/dataSPA/dataSPA-inspector@latest/dataspa-inspector.bundled.js"), h.Type("module")), - // h.Raw(""), - // ) rooms := NewRooms[Chat, UserInfo]("Clojure", "Dotnet", "Go", "Java", "JS", "Kotlin", "Python", "Rust") rooms.Start() @@ -144,24 +139,13 @@ func main() { c.View(func() h.H { var tabs []h.H rooms.Visit(func(n string) { - if n == roomNameString { - tabs = append(tabs, h.Li( - h.A( - h.Href(""), - h.Attr("aria-current", "page"), - h.Text(n), - switchRoomAction.OnClick(WithSignal(roomName, n)), - ), - )) - } else { - tabs = append(tabs, h.Li( - h.A( - h.Href("#"), - h.Text(n), - switchRoomAction.OnClick(via.WithSignal(roomName, n)), - ), - )) - } + tabs = append(tabs, h.Li( + h.A( + h.If(n == roomNameString, h.Attr("aria-current", "page")), + h.Text(n), + switchRoomAction.OnClick(WithSignal(roomName, n)), + ), + )) }) var messages []h.H diff --git a/internal/examples/picocss/main.go b/internal/examples/picocss/main.go index 09b96e3..68a1bb9 100644 --- a/internal/examples/picocss/main.go +++ b/internal/examples/picocss/main.go @@ -46,4 +46,3 @@ func main() { v.Start() } - diff --git a/via.go b/via.go index 119d88c..4bebf94 100644 --- a/via.go +++ b/via.go @@ -174,6 +174,11 @@ func (v *V) registerCtx(c *Context) { } v.contextRegistry[c.id] = c v.logDebug(c, "new context added to registry") + v.summarize() +} + +func (v *V) summarize() { + fmt.Println("Have", len(v.contextRegistry), "sessions") } func (v *V) unregisterCtx(id string) { @@ -184,6 +189,7 @@ func (v *V) unregisterCtx(id string) { } v.logDebug(nil, "ctx '%s' removed from registry", id) delete(v.contextRegistry, id) + v.summarize() } func (v *V) getCtx(id string) (*Context, error) { diff --git a/via_test.go b/via_test.go index ad0cc25..887dcbd 100644 --- a/via_test.go +++ b/via_test.go @@ -1,165 +1,86 @@ package via import ( - "compress/gzip" - "io" "net/http" "net/http/httptest" - "strings" "testing" - "github.com/andybalholm/brotli" "github.com/go-via/via/h" "github.com/stretchr/testify/assert" ) -func TestCompressionBrotli(t *testing.T) { +func TestPageRoute(t *testing.T) { v := New() v.Page("/", func(c *Context) { c.View(func() h.H { - return h.Div(h.Text(strings.Repeat("Hello Via! ", 200))) - }) - }) - - req := httptest.NewRequest("GET", "/", nil) - req.Header.Set("Accept-Encoding", "br") - w := httptest.NewRecorder() - - v.handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "br", w.Header().Get("Content-Encoding")) - - reader := brotli.NewReader(w.Body) - decompressed, err := io.ReadAll(reader) - assert.NoError(t, err) - assert.Contains(t, string(decompressed), "Hello Via!") -} - -func TestCompressionGzip(t *testing.T) { - v := New() - v.Page("/", func(c *Context) { - c.View(func() h.H { - return h.Div(h.Text(strings.Repeat("Hello Via! ", 200))) - }) - }) - - req := httptest.NewRequest("GET", "/", nil) - req.Header.Set("Accept-Encoding", "gzip") - w := httptest.NewRecorder() - - v.handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "gzip", w.Header().Get("Content-Encoding")) - - reader, err := gzip.NewReader(w.Body) - assert.NoError(t, err) - decompressed, err := io.ReadAll(reader) - assert.NoError(t, err) - assert.Contains(t, string(decompressed), "Hello Via!") -} - -func TestCompressionNone(t *testing.T) { - v := New() - v.Page("/", func(c *Context) { - c.View(func() h.H { - return h.Div(h.Text(strings.Repeat("Hello Via! ", 200))) + return h.Div(h.Text("Hello Via!")) }) }) req := httptest.NewRequest("GET", "/", nil) w := httptest.NewRecorder() - - v.handler.ServeHTTP(w, req) + v.mux.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) - assert.Empty(t, w.Header().Get("Content-Encoding")) assert.Contains(t, w.Body.String(), "Hello Via!") + assert.Contains(t, w.Body.String(), "") } -func TestCompressionMinSize(t *testing.T) { +func TestDatastarJS(t *testing.T) { v := New() - v.Page("/", func(c *Context) { - c.View(func() h.H { - return h.Div(h.Text("Small")) - }) - }) - - req := httptest.NewRequest("GET", "/", nil) - req.Header.Set("Accept-Encoding", "br") - w := httptest.NewRecorder() - - v.handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Empty(t, w.Header().Get("Content-Encoding")) -} - -func TestCompressionLargeResponse(t *testing.T) { - v := New() - v.Page("/", func(c *Context) { - c.View(func() h.H { - return h.Div(h.Text(strings.Repeat("A", 2000))) - }) - }) - - req := httptest.NewRequest("GET", "/", nil) - req.Header.Set("Accept-Encoding", "br") - w := httptest.NewRecorder() - - v.handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "br", w.Header().Get("Content-Encoding")) -} - -func TestCompressionDatastarJS(t *testing.T) { - v := New() - req := httptest.NewRequest("GET", "/_datastar.js", nil) - req.Header.Set("Accept-Encoding", "br") w := httptest.NewRecorder() - - v.handler.ServeHTTP(w, req) + v.mux.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "br", w.Header().Get("Content-Encoding")) + assert.Equal(t, "application/javascript", w.Header().Get("Content-Type")) } -func TestCompressionBrotliPreferred(t *testing.T) { +func TestSignal(t *testing.T) { + var sig *signal v := New() v.Page("/", func(c *Context) { + sig = c.Signal("test") + c.View(func() h.H { return h.Div() }) + }) + + req := httptest.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + v.mux.ServeHTTP(w, req) + + assert.Equal(t, "test", sig.v.Interface()) +} + +func TestAction(t *testing.T) { + var trigger *actionTrigger + var sig *signal + v := New() + v.Page("/", func(c *Context) { + trigger = c.Action(func() {}) + sig = c.Signal("value") c.View(func() h.H { - return h.Div(h.Text(strings.Repeat("Hello Via! ", 200))) + return h.Div( + h.Button(trigger.OnClick()), + h.Input(trigger.OnChange()), + h.Input(trigger.OnKeyDown("Enter")), + h.Button(trigger.OnClick(WithSignal(sig, "test"))), + h.Button(trigger.OnClick(WithSignalInt(sig, 42))), + ) }) }) req := httptest.NewRequest("GET", "/", nil) - req.Header.Set("Accept-Encoding", "gzip, br") w := httptest.NewRecorder() - - v.handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "br", w.Header().Get("Content-Encoding")) + v.mux.ServeHTTP(w, req) + body := w.Body.String() + assert.Contains(t, body, "data-on:click") + assert.Contains(t, body, "data-on:change__debounce.200ms") + assert.Contains(t, body, "data-on:keydown") + assert.Contains(t, body, "/_action/") } -func TestCompressionZstdDisabled(t *testing.T) { +func TestConfig(t *testing.T) { v := New() - v.Page("/", func(c *Context) { - c.View(func() h.H { - return h.Div(h.Text(strings.Repeat("Hello Via! ", 200))) - }) - }) - - req := httptest.NewRequest("GET", "/", nil) - req.Header.Set("Accept-Encoding", "zstd, br, gzip") - w := httptest.NewRecorder() - - v.handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "br", w.Header().Get("Content-Encoding"), "Should use Brotli, not zstd") - assert.NotEqual(t, "zstd", w.Header().Get("Content-Encoding")) + v.Config(Options{DocumentTitle: "Test"}) + assert.Equal(t, "Test", v.cfg.DocumentTitle) }