Script, GH action to check that all go files compile, and any tests pass. (#16)
This commit is contained in:
29
.github/workflows/ci.yml
vendored
Normal file
29
.github/workflows/ci.yml
vendored
Normal file
@@ -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
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -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
|
||||
56
ci-check.sh
Executable file
56
ci-check.sh
Executable file
@@ -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
|
||||
13
context.go
13
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)}
|
||||
|
||||
@@ -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("<dataspa-inspector/>"),
|
||||
// )
|
||||
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
|
||||
|
||||
@@ -46,4 +46,3 @@ func main() {
|
||||
|
||||
v.Start()
|
||||
}
|
||||
|
||||
|
||||
6
via.go
6
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) {
|
||||
|
||||
165
via_test.go
165
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(), "<!doctype html>")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user