diff --git a/boolsetter.go b/boolsetter.go
index 1997c55..954a491 100644
--- a/boolsetter.go
+++ b/boolsetter.go
@@ -36,5 +36,5 @@ func makeBoolSetter(v interface{}) BoolSetter {
case *atomic.Value:
return atomicSetter{v}
}
- panic(fmt.Errorf("expected jaws.BoolGetter or bool, not %T", v))
+ panic(fmt.Errorf("expected jaws.BoolSetter or bool, not %T", v))
}
diff --git a/clickhandler_test.go b/clickhandler_test.go
index ac77c79..f791ef8 100644
--- a/clickhandler_test.go
+++ b/clickhandler_test.go
@@ -2,7 +2,6 @@ package jaws
import (
"testing"
- "time"
"github.com/linkdata/jaws/what"
)
@@ -22,8 +21,7 @@ func (tje *testJawsClick) JawsClick(e *Element, name string) (err error) {
var _ ClickHandler = (*testJawsClick)(nil)
func Test_clickHandlerWapper_JawsEvent(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
nextJid = 0
rq := newTestRequest()
defer rq.Close()
@@ -34,14 +32,15 @@ func Test_clickHandlerWapper_JawsEvent(t *testing.T) {
}
want := `
inner
`
- if got := string(rq.Div("inner", tjc)); got != want {
+ rq.Div("inner", tjc)
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Div() = %q, want %q", got, want)
}
rq.inCh <- wsMsg{Data: "text", Jid: 1, What: what.Input}
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
t.Errorf("%q", s)
default:
@@ -49,8 +48,8 @@ func Test_clickHandlerWapper_JawsEvent(t *testing.T) {
rq.inCh <- wsMsg{Data: "adam", Jid: 1, What: what.Click}
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case name := <-tjc.clickCh:
if name != "adam" {
t.Error(name)
diff --git a/element.go b/element.go
index d610748..905444e 100644
--- a/element.go
+++ b/element.go
@@ -12,9 +12,9 @@ import (
// An Element is an instance of a *Request, an UI object and a Jid.
type Element struct {
- ui UI // (read-only) the UI object
- jid jid.Jid // (read-only) JaWS ID, unique to this Element within it's Request
- *Request // (read-only) the Request the Element belongs to
+ ui UI // (read-only) the UI object
+ jid jid.Jid // (read-only) JaWS ID, unique to this Element within it's Request
+ rq *Request // (read-only) the Request the Element belongs to
// internals
updating bool // about to have Update() called
wsQueue []wsMsg // changes queued
@@ -22,17 +22,47 @@ type Element struct {
}
func (e *Element) String() string {
- return fmt.Sprintf("Element{%T, id=%q, Tags: %v}", e.ui, e.jid, e.Request.TagsOf(e))
+ return fmt.Sprintf("Element{%T, id=%q, Tags: %v}", e.ui, e.jid, e.rq.TagsOf(e))
+}
+
+// Jaws returns the Jaws the Element belongs to.
+func (e *Element) Jaws() *Jaws {
+ return e.rq.Jaws
+}
+
+// Request returns the Request the Element belongs to.
+func (e *Element) Request() *Request {
+ return e.rq
+}
+
+// Session returns the Elements's Session, or nil.
+func (e *Element) Session() *Session {
+ return e.rq.Session()
+}
+
+// Get calls Session().Get()
+func (e *Element) Get(key string) (val interface{}) {
+ return e.Session().Get(key)
+}
+
+// Set calls Session().Get()
+func (e *Element) Set(key string, val interface{}) {
+ e.Session().Set(key, val)
+}
+
+// Dirty calls Request().Dirty()
+func (e *Element) Dirty(tags ...interface{}) {
+ e.rq.Dirty(tags...)
}
// Tag adds the given tags to the Element.
func (e *Element) Tag(tags ...interface{}) {
- e.Request.Tag(e, tags...)
+ e.rq.Tag(e, tags...)
}
// HasTag returns true if this Element has the given tag.
func (e *Element) HasTag(tag interface{}) bool {
- return e.Request.HasTag(e, tag)
+ return e.rq.HasTag(e, tag)
}
// Jid returns the JaWS ID for this Element, unique within it's Request.
@@ -46,8 +76,8 @@ func (e *Element) Ui() UI {
}
// Render calls Request.JawsRender() for this Element.
-func (e *Element) Render(w io.Writer, params []interface{}) {
- e.Request.JawsRender(e, w, params)
+func (e *Element) Render(w io.Writer, params []interface{}) error {
+ return e.rq.JawsRender(e, w, params)
}
func (e *Element) queue(wht what.What, data string) {
@@ -58,7 +88,7 @@ func (e *Element) queue(wht what.What, data string) {
What: wht,
})
} else {
- e.Request.cancelFn(ErrWebsocketQueueOverflow)
+ e.rq.cancel(ErrWebsocketQueueOverflow)
}
}
diff --git a/element_test.go b/element_test.go
index c623305..697c7c7 100644
--- a/element_test.go
+++ b/element_test.go
@@ -21,7 +21,7 @@ type testUi struct {
getCalled int32
setCalled int32
s string
- renderFn func(e *Element, w io.Writer, params []any)
+ renderFn func(e *Element, w io.Writer, params []any) error
updateFn func(e *Element)
}
@@ -39,12 +39,13 @@ func (tss *testUi) JawsSetString(e *Element, s string) error {
return nil
}
-func (tss *testUi) JawsRender(e *Element, w io.Writer, params []any) {
+func (tss *testUi) JawsRender(e *Element, w io.Writer, params []any) (err error) {
e.Tag(tss)
atomic.AddInt32(&tss.renderCalled, 1)
if tss.renderFn != nil {
- tss.renderFn(e, w, params)
+ err = tss.renderFn(e, w, params)
}
+ return
}
func (tss *testUi) JawsUpdate(e *Element) {
@@ -54,8 +55,22 @@ func (tss *testUi) JawsUpdate(e *Element) {
}
}
+func TestElement_helpers(t *testing.T) {
+ is := newTestHelper(t)
+ rq := newTestRequest()
+ defer rq.Close()
+
+ tss := &testUi{}
+ e := rq.NewElement(tss)
+ is.Equal(e.Jaws(), rq.jw.Jaws)
+ is.Equal(e.Request(), rq.Request)
+ is.Equal(e.Session(), nil)
+ e.Set("foo", "bar") // no session, so no effect
+ is.Equal(e.Get("foo"), nil)
+}
+
func TestElement_Tag(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
@@ -68,7 +83,7 @@ func TestElement_Tag(t *testing.T) {
}
func TestElement_Queued(t *testing.T) {
- is := testHelper{t}
+ th := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
@@ -85,7 +100,7 @@ func TestElement_Queued(t *testing.T) {
e.Order([]jid.Jid{1, 2})
replaceHtml := template.HTML(fmt.Sprintf("", e.Jid().String()))
e.Replace(replaceHtml)
- is.Equal(e.wsQueue, []wsMsg{
+ th.Equal(e.wsQueue, []wsMsg{
{
Data: "hidden\n",
Jid: e.jid,
@@ -141,26 +156,25 @@ func TestElement_Queued(t *testing.T) {
}
pendingRq := rq.Jaws.NewRequest(httptest.NewRequest(http.MethodGet, "/", nil))
- pendingRq.UI(tss)
+ RequestWriter{pendingRq, httptest.NewRecorder()}.UI(tss)
rq.UI(tss)
rq.Jaws.Dirty(tss)
rq.Dirty(tss)
- tmr := time.NewTimer(testTimeout)
for atomic.LoadInt32(&tss.updateCalled) < 1 {
select {
- case <-tmr.C:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
default:
time.Sleep(time.Millisecond)
}
}
- is.Equal(tss.updateCalled, int32(1))
- is.Equal(tss.renderCalled, int32(2))
+ th.Equal(tss.updateCalled, int32(1))
+ th.Equal(tss.renderCalled, int32(2))
}
func TestElement_ReplacePanicsOnMissingId(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
defer func() {
diff --git a/html.go b/html.go
index a46eb0b..2f8d1de 100644
--- a/html.go
+++ b/html.go
@@ -4,7 +4,6 @@ import (
"html/template"
"io"
"strconv"
- "strings"
"github.com/linkdata/jaws/jid"
)
@@ -70,12 +69,6 @@ func WriteHtmlInput(w io.Writer, jid jid.Jid, typ, val string, attrs ...string)
return
}
-func HtmlInput(jid jid.Jid, typ, val string, attrs ...string) template.HTML {
- var sb strings.Builder
- _ = WriteHtmlInput(&sb, jid, typ, val, attrs...)
- return template.HTML(sb.String()) // #nosec G203
-}
-
func WriteHtmlInner(w io.Writer, jid jid.Jid, tag, typ string, inner template.HTML, attrs ...string) (err error) {
need := 1 + len(tag)*2 + jidPrealloc + 8 + len(typ) + 1 + 1 + getAttrsLen(attrs) + 1 + len(inner) + 2 + 1
b := make([]byte, 0, need)
@@ -96,12 +89,6 @@ func WriteHtmlInner(w io.Writer, jid jid.Jid, tag, typ string, inner template.HT
return
}
-func HtmlInner(jid jid.Jid, tag, typ string, inner template.HTML, attrs ...string) template.HTML {
- var sb strings.Builder
- _ = WriteHtmlInner(&sb, jid, tag, typ, inner, attrs...)
- return template.HTML(sb.String()) // #nosec G203
-}
-
func WriteHtmlSelect(w io.Writer, jid jid.Jid, nba *NamedBoolArray, attrs ...string) (err error) {
need := 12 + jidPrealloc + 2 + getAttrsLen(attrs) + 2 + 10
nba.ReadLocked(func(nba []*NamedBool) {
@@ -132,9 +119,3 @@ func WriteHtmlSelect(w io.Writer, jid jid.Jid, nba *NamedBoolArray, attrs ...str
_, err = w.Write(b)
return
}
-
-func HtmlSelect(jid jid.Jid, nba *NamedBoolArray, attrs ...string) template.HTML {
- var sb strings.Builder
- _ = WriteHtmlSelect(&sb, jid, nba, attrs...)
- return template.HTML(sb.String()) // #nosec G203
-}
diff --git a/html_test.go b/html_test.go
index df11c65..27d8831 100644
--- a/html_test.go
+++ b/html_test.go
@@ -2,6 +2,7 @@ package jaws
import (
"html/template"
+ "strings"
"testing"
"github.com/linkdata/jaws/jid"
@@ -17,7 +18,7 @@ func TestHtmlInput(t *testing.T) {
tests := []struct {
name string
args args
- want template.HTML
+ want string
}{
{
name: "HtmlInput no attrs",
@@ -61,7 +62,11 @@ func TestHtmlInput(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if got := HtmlInput(tt.args.jid, tt.args.typ, tt.args.val, tt.args.attrs...); got != tt.want {
+ var sb strings.Builder
+ if err := WriteHtmlInput(&sb, tt.args.jid, tt.args.typ, tt.args.val, tt.args.attrs...); err != nil {
+ t.Fatal(err)
+ }
+ if got := sb.String(); got != tt.want {
t.Errorf("HtmlInput() = %v, want %v", got, tt.want)
}
})
@@ -79,7 +84,7 @@ func TestHtmlInner(t *testing.T) {
tests := []struct {
name string
args args
- want template.HTML
+ want string
}{
{
name: "HtmlInner no attrs",
@@ -115,7 +120,11 @@ func TestHtmlInner(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if got := HtmlInner(tt.args.jid, tt.args.tag, tt.args.typ, tt.args.inner, tt.args.attrs...); got != tt.want {
+ var sb strings.Builder
+ if err := WriteHtmlInner(&sb, tt.args.jid, tt.args.tag, tt.args.typ, tt.args.inner, tt.args.attrs...); err != nil {
+ t.Fatal(err)
+ }
+ if got := sb.String(); got != tt.want {
t.Errorf("HtmlInner() = %v, want %v", got, tt.want)
}
})
@@ -131,7 +140,7 @@ func TestHtmlSelect(t *testing.T) {
tests := []struct {
name string
args args
- want template.HTML
+ want string
}{
{
name: "HtmlSelect empty NamedBoolArray and one attr",
@@ -166,7 +175,11 @@ func TestHtmlSelect(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if got := HtmlSelect(tt.args.jid, tt.args.val, tt.args.attrs...); got != tt.want {
+ var sb strings.Builder
+ if err := WriteHtmlSelect(&sb, tt.args.jid, tt.args.val, tt.args.attrs...); err != nil {
+ t.Fatal(err)
+ }
+ if got := sb.String(); got != tt.want {
t.Errorf("HtmlSelect() = %v, want %v", got, tt.want)
}
})
diff --git a/jaws.go b/jaws.go
index 5525506..2e8db72 100644
--- a/jaws.go
+++ b/jaws.go
@@ -23,6 +23,7 @@ import (
"sort"
"strconv"
"strings"
+ "sync"
"sync/atomic"
"time"
@@ -48,11 +49,11 @@ type Jaws struct {
unsubCh chan chan Message
updateTicker *time.Ticker
headPrefix string
+ reqPool sync.Pool
mu deadlock.RWMutex // protects following
kg *bufio.Reader
closeCh chan struct{}
- pending map[uint64]*Request
- active map[*Request]struct{}
+ requests map[uint64]*Request
sessions map[uint64]*Session
dirty map[interface{}]int
dirtOrder int
@@ -61,8 +62,8 @@ type Jaws struct {
// NewWithDone returns a new JaWS object using the given completion channel.
// This is expected to be created once per HTTP server and handles
// publishing HTML changes across all connections.
-func NewWithDone(doneCh <-chan struct{}) *Jaws {
- return &Jaws{
+func NewWithDone(doneCh <-chan struct{}) (jw *Jaws) {
+ jw = &Jaws{
CookieName: DefaultCookieName,
doneCh: doneCh,
bcastCh: make(chan Message, 1),
@@ -71,11 +72,17 @@ func NewWithDone(doneCh <-chan struct{}) *Jaws {
updateTicker: time.NewTicker(DefaultUpdateInterval),
headPrefix: HeadHTML([]string{JavascriptPath}, nil),
kg: bufio.NewReader(rand.Reader),
- pending: make(map[uint64]*Request),
- active: make(map[*Request]struct{}),
+ requests: make(map[uint64]*Request),
sessions: make(map[uint64]*Session),
dirty: make(map[interface{}]int),
}
+ jw.reqPool.New = func() any {
+ return (&Request{
+ Jaws: jw,
+ tagMap: make(map[any][]*Element),
+ }).clearLocked()
+ }
+ return
}
// New returns a new JaWS object that must be closed using Close().
@@ -107,15 +114,13 @@ func (jw *Jaws) Done() <-chan struct{} {
return jw.doneCh
}
-// RequestCount returns the number of active and pending Requests.
+// RequestCount returns the number of Requests.
//
-// "Active" Requests are those for which there is a WebSocket connection
-// and messages are being routed for. "Pending" are those for which the
-// initial HTTP request has been made but we have not yet received the
-// WebSocket callback and started processing.
+// The count includes all Requests, including those being rendered,
+// those waiting for the WebSocket callback and those active.
func (jw *Jaws) RequestCount() (n int) {
jw.mu.RLock()
- n = len(jw.pending) + len(jw.active)
+ n = len(jw.requests)
jw.mu.RUnlock()
return
}
@@ -169,9 +174,9 @@ func (jw *Jaws) NewRequest(hr *http.Request) (rq *Request) {
defer jw.mu.Unlock()
for rq == nil {
jawsKey := jw.nonZeroRandomLocked()
- if _, ok := jw.pending[jawsKey]; !ok {
- rq = getRequest(jw, jawsKey, hr)
- jw.pending[jawsKey] = rq
+ if _, ok := jw.requests[jawsKey]; !ok {
+ rq = jw.getRequestLocked(jawsKey, hr)
+ jw.requests[jawsKey] = rq
}
}
return
@@ -201,11 +206,9 @@ func (jw *Jaws) UseRequest(jawsKey uint64, hr *http.Request) (rq *Request) {
if jawsKey != 0 {
var err error
jw.mu.Lock()
- if waitingRq, ok := jw.pending[jawsKey]; ok {
- if err = waitingRq.start(hr); err == nil {
- delete(jw.pending, jawsKey)
+ if waitingRq, ok := jw.requests[jawsKey]; ok {
+ if err = waitingRq.claim(hr); err == nil {
rq = waitingRq
- jw.active[rq] = struct{}{}
}
}
jw.mu.Unlock()
@@ -391,11 +394,8 @@ func (jw *Jaws) distributeDirt() int {
var reqs []*Request
if len(dirt) > 0 {
- reqs = make([]*Request, 0, len(jw.pending)+len(jw.active))
- for _, rq := range jw.pending {
- reqs = append(reqs, rq)
- }
- for rq := range jw.active {
+ reqs = make([]*Request, 0, len(jw.requests))
+ for _, rq := range jw.requests {
reqs = append(reqs, rq)
}
}
@@ -441,17 +441,15 @@ func (jw *Jaws) Alert(lvl, msg string) {
// Count returns the number of requests waiting for their WebSocket callbacks.
func (jw *Jaws) Pending() (n int) {
jw.mu.RLock()
- n = len(jw.pending)
+ for _, rq := range jw.requests {
+ if !rq.claimed {
+ n++
+ }
+ }
jw.mu.RUnlock()
return
}
-func (jw *Jaws) deactivate(rq *Request) {
- jw.mu.Lock()
- delete(jw.active, rq)
- jw.mu.Unlock()
-}
-
// ServeWithTimeout begins processing requests with the given timeout.
// It is intended to run on it's own goroutine.
// It returns when the completion channel is closed.
@@ -525,13 +523,7 @@ func (jw *Jaws) Serve() {
jw.ServeWithTimeout(time.Second * 10)
}
-func (jw *Jaws) subscribe(rq *Request, minSize int) chan Message {
- size := minSize
- if rq != nil {
- if size = 4 + len(rq.elems)*4; size < minSize {
- size = minSize
- }
- }
+func (jw *Jaws) subscribe(rq *Request, size int) chan Message {
msgCh := make(chan Message, size)
select {
case <-jw.Done():
@@ -549,45 +541,19 @@ func (jw *Jaws) unsubscribe(msgCh chan Message) {
}
}
-func maybeErrPendingCancelled(rq *Request, deadline time.Time) (err error) {
- if err = context.Cause(rq.ctx); err == nil && rq.Created.Before(deadline) {
- err = newErrNoWebSocketRequest(rq)
- }
- if err != nil {
- err = newErrPendingCancelled(rq, err)
- }
- return
-}
-
func (jw *Jaws) maintenance(requestTimeout time.Duration) {
- var killReqs []*Request
- var killSess []uint64
-
- jw.mu.RLock()
- now := time.Now()
- deadline := now.Add(-requestTimeout)
- for _, rq := range jw.pending {
- if err := jw.Log(maybeErrPendingCancelled(rq, deadline)); err != nil {
- rq.cancel(err)
- killReqs = append(killReqs, rq)
+ deadline := time.Now().Add(-requestTimeout)
+ jw.mu.Lock()
+ defer jw.mu.Unlock()
+ for _, rq := range jw.requests {
+ if rq.maintenance(deadline) {
+ jw.recycleLocked(rq)
}
}
for k, sess := range jw.sessions {
if sess.isDead() {
- killSess = append(killSess, k)
- }
- }
- jw.mu.RUnlock()
-
- if len(killReqs)+len(killSess) > 0 {
- jw.mu.Lock()
- for _, rq := range killReqs {
- delete(jw.pending, rq.JawsKey)
- }
- for _, k := range killSess {
delete(jw.sessions, k)
}
- jw.mu.Unlock()
}
}
@@ -712,3 +678,35 @@ func (jw *Jaws) Append(target interface{}, html template.HTML) {
Data: html,
})
}
+
+func (jw *Jaws) getRequestLocked(jawsKey uint64, hr *http.Request) (rq *Request) {
+ rq = jw.reqPool.Get().(*Request)
+ rq.JawsKey = jawsKey
+ rq.Created = time.Now()
+ rq.Initial = hr
+ rq.ctx, rq.cancelFn = context.WithCancelCause(context.Background())
+ if hr != nil {
+ rq.remoteIP = parseIP(hr.RemoteAddr)
+ if sess := jw.getSessionLocked(getCookieSessionsIds(hr.Header, jw.CookieName), rq.remoteIP); sess != nil {
+ sess.addRequest(rq)
+ rq.session = sess
+ }
+ }
+ return rq
+}
+
+func (jw *Jaws) recycleLocked(rq *Request) {
+ rq.mu.Lock()
+ defer rq.mu.Unlock()
+ if rq.JawsKey != 0 {
+ delete(jw.requests, rq.JawsKey)
+ rq.clearLocked()
+ jw.reqPool.Put(rq)
+ }
+}
+
+func (jw *Jaws) recycle(rq *Request) {
+ jw.mu.Lock()
+ defer jw.mu.Unlock()
+ jw.recycleLocked(rq)
+}
diff --git a/jaws_test.go b/jaws_test.go
index 7dc689b..5e51582 100644
--- a/jaws_test.go
+++ b/jaws_test.go
@@ -18,7 +18,7 @@ import (
)
func TestJaws_parseIP(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
is.True(!parseIP("").IsValid())
is.True(parseIP("192.168.0.1").Compare(netip.MustParseAddr("192.168.0.1")) == 0)
is.True(parseIP("192.168.0.2:1234").Compare(netip.MustParseAddr("192.168.0.2")) == 0)
@@ -35,7 +35,7 @@ func TestJaws_parseIP(t *testing.T) {
func TestJaws_getCookieSessionsIds(t *testing.T) {
const sessId = 1234
sessCookie := JawsKeyString(sessId)
- is := testHelper{t}
+ is := newTestHelper(t)
is.Equal(getCookieSessionsIds(nil, "meh"), nil)
is.Equal(getCookieSessionsIds(http.Header{}, "meh"), nil)
is.Equal(getCookieSessionsIds(http.Header{"Cookie": []string{}}, "meh"), nil)
@@ -52,7 +52,7 @@ func TestJaws_MultipleCloseCalls(t *testing.T) {
}
func TestJaws_MakeID(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
jw := New()
defer jw.Close()
go jw.Serve()
@@ -64,7 +64,7 @@ func TestJaws_MakeID(t *testing.T) {
}
func TestJaws_maybePanic(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
defer func() {
if recover() == nil {
is.Fail()
@@ -74,7 +74,7 @@ func TestJaws_maybePanic(t *testing.T) {
}
func TestJaws_Logger(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
jw := New()
defer jw.Close()
var b bytes.Buffer
@@ -87,7 +87,7 @@ func TestJaws_Logger(t *testing.T) {
}
func TestJaws_MustLog(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
jw := New()
defer jw.Close()
@@ -118,12 +118,11 @@ func TestJaws_BroadcastDoesntBlockWhenClosed(t *testing.T) {
}
func TestJaws_BroadcastWaitsWhenFull(t *testing.T) {
- is := testHelper{t}
-
+ th := newTestHelper(t)
jw := New()
go jw.ServeWithTimeout(testTimeout)
- subCh := jw.subscribe(nil, 0)
+ subCh := jw.subscribe(jw.NewRequest(nil), 0)
defer jw.unsubscribe(subCh)
// ensure our sub has been processed
@@ -132,21 +131,21 @@ func TestJaws_BroadcastWaitsWhenFull(t *testing.T) {
// send two broadcasts
select {
- case <-time.NewTimer(testTimeout).C:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
case jw.bcastCh <- Message{What: what.Reload}:
}
select {
- case <-time.NewTimer(testTimeout).C:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
case jw.bcastCh <- Message{What: what.Reload}:
}
// read one of the broadcasts, the other is
// left to fall into the retry loop
select {
- case <-time.NewTimer(testTimeout).C:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
case <-subCh:
}
@@ -155,30 +154,31 @@ func TestJaws_BroadcastWaitsWhenFull(t *testing.T) {
// finally, read the msg
select {
- case <-time.NewTimer(testTimeout).C:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
case <-subCh:
}
}
func TestJaws_BroadcastFullClosesChannel(t *testing.T) {
- is := testHelper{t}
+ th := newTestHelper(t)
jw := New()
go jw.ServeWithTimeout(time.Millisecond)
doneCh := make(chan struct{})
failCh := make(chan struct{})
- subCh1 := jw.subscribe(nil, 0)
+ subCh1 := jw.subscribe(jw.NewRequest(nil), 0)
+
defer jw.unsubscribe(subCh1)
- subCh2 := jw.subscribe(nil, 0)
+ subCh2 := jw.subscribe(jw.NewRequest(nil), 0)
defer jw.unsubscribe(subCh2)
jw.subCh <- subscription{}
jw.subCh <- subscription{}
go func() {
select {
- case <-time.NewTimer(testTimeout).C:
+ case <-th.C:
close(failCh)
case <-subCh2:
close(doneCh)
@@ -186,16 +186,16 @@ func TestJaws_BroadcastFullClosesChannel(t *testing.T) {
}()
select {
- case <-time.NewTimer(testTimeout).C:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
case jw.bcastCh <- Message{What: what.Reload}:
}
select {
- case <-time.NewTimer(testTimeout).C:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
case <-failCh:
- is.Fail()
+ th.Timeout()
case <-doneCh:
}
@@ -205,89 +205,91 @@ func TestJaws_BroadcastFullClosesChannel(t *testing.T) {
select {
case msg, ok := <-subCh1:
- is.True(!ok)
- is.Equal(msg, Message{})
+ th.True(!ok)
+ th.Equal(msg, Message{})
default:
}
}
func TestJaws_UseRequest(t *testing.T) {
- is := testHelper{t}
+ th := newTestHelper(t)
jw := New()
defer jw.Close()
- is.Equal(0, jw.RequestCount())
+ th.Equal(0, jw.RequestCount())
rq1 := jw.NewRequest(nil)
- is.True(rq1.JawsKey != 0)
+ th.True(rq1.JawsKey != 0)
rq2 := jw.NewRequest(&http.Request{RemoteAddr: "10.0.0.2:1010"})
- is.True(rq2.JawsKey != 0)
- is.True(rq1.JawsKey != rq2.JawsKey)
- is.Equal(jw.Pending(), 2)
+ th.True(rq2.JawsKey != 0)
+ th.True(rq1.JawsKey != rq2.JawsKey)
+ th.Equal(jw.Pending(), 2)
rqfail := jw.UseRequest(0, nil) // wrong JawsKey
- is.Equal(rqfail, nil)
- is.Equal(jw.Pending(), 2)
+ th.Equal(rqfail, nil)
+ th.Equal(jw.Pending(), 2)
rqfail = jw.UseRequest(rq1.JawsKey, &http.Request{RemoteAddr: "10.0.0.1:1010"}) // wrong IP, expect blank
- is.Equal(rqfail, nil)
- is.Equal(jw.Pending(), 2)
+ th.Equal(rqfail, nil)
+ th.Equal(jw.Pending(), 2)
rqfail = jw.UseRequest(rq2.JawsKey, &http.Request{RemoteAddr: "10.0.0.1:1010"}) // wrong IP, expect .2
- is.Equal(rqfail, nil)
- is.Equal(jw.Pending(), 2)
+ th.Equal(rqfail, nil)
+ th.Equal(jw.Pending(), 2)
rq2ret := jw.UseRequest(rq2.JawsKey, &http.Request{RemoteAddr: "10.0.0.2:1212"}) // different port is OK
- is.Equal(rq2, rq2ret)
- is.Equal(jw.Pending(), 1)
+ th.Equal(rq2, rq2ret)
+ th.Equal(jw.Pending(), 1)
rq1ret := jw.UseRequest(rq1.JawsKey, nil)
- is.Equal(rq1, rq1ret)
- is.Equal(jw.Pending(), 0)
+ th.Equal(rq1, rq1ret)
+ th.Equal(jw.Pending(), 0)
}
func TestJaws_BlockingRandomPanics(t *testing.T) {
- is := testHelper{t}
+ th := newTestHelper(t)
defer func() {
if recover() == nil {
- is.Fail()
+ th.Error("expected error")
}
}()
jw := New()
defer jw.Close()
jw.kg = bufio.NewReader(&bytes.Buffer{})
jw.NewRequest(nil)
- is.Fail()
}
func TestJaws_CleansUpUnconnected(t *testing.T) {
- const numReqs = 1000
- is := testHelper{t}
+ const numReqs = 100
+ th := newTestHelper(t)
jw := New()
defer jw.Close()
var b bytes.Buffer
w := bufio.NewWriter(&b)
jw.Logger = log.New(w, "", 0)
hr := httptest.NewRequest(http.MethodGet, "/", nil)
- is.Equal(jw.Pending(), 0)
+ th.Equal(jw.Pending(), 0)
deadline := time.Now().Add(testTimeout)
var expectLen int
for i := 0; i < numReqs; i++ {
rq := jw.NewRequest(hr)
- if (i % (numReqs / 10)) == 0 {
+ if (i % (numReqs / 5)) == 0 {
elem := rq.NewElement(NewUiDiv(makeHtmlGetter("meh")))
for j := 0; j < maxWsQueueLengthPerElement*10; j++ {
elem.SetInner("foo")
}
}
- err := maybeErrPendingCancelled(rq, deadline)
+ err := context.Cause(rq.ctx)
+ if err == nil && rq.Created.Before(deadline) {
+ err = newErrPendingCancelled(rq, newErrNoWebSocketRequest(rq))
+ }
if err == nil {
t.Fatal("expected error")
}
expectLen += len(err.Error() + "\n")
}
- is.Equal(jw.Pending(), numReqs)
+ th.Equal(jw.Pending(), numReqs)
go jw.ServeWithTimeout(time.Millisecond)
@@ -299,25 +301,23 @@ func TestJaws_CleansUpUnconnected(t *testing.T) {
}
}
- is.Equal(jw.Pending(), 0)
+ th.Equal(jw.Pending(), 0)
jw.Close()
select {
- case <-time.NewTimer(testTimeout).C:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
case <-jw.Done():
}
w.Flush()
if x := b.Len(); x != expectLen {
t.Log(b.String())
- is.Equal(b.Len(), expectLen)
+ th.Equal(b.Len(), expectLen)
}
}
func TestJaws_UnconnectedLivesUntilDeadline(t *testing.T) {
- is := testHelper{t}
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
jw := New()
defer jw.Close()
@@ -328,36 +328,39 @@ func TestJaws_UnconnectedLivesUntilDeadline(t *testing.T) {
rq2.Created = time.Now().Add(-time.Second * 10)
rq2ctx := rq2.Context()
- is.Equal(jw.Pending(), 2)
+ th.Equal(jw.Pending(), 2)
go jw.ServeWithTimeout(time.Second)
for jw.Pending() > 1 {
select {
- case <-tmr.C:
- is.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
case <-jw.Done():
- is.Error("unexpected close")
+ th.Error("unexpected close")
default:
time.Sleep(time.Millisecond)
}
}
- is.Equal(jw.Pending(), 1)
+ th.Equal(jw.Pending(), 1)
jw.Close()
select {
- case <-tmr.C:
- is.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
case <-jw.Done():
}
- is.NoErr(context.Cause(rq1ctx))
- is.True(errors.Is(context.Cause(rq2ctx), ErrNoWebSocketRequest{}))
-
// neither should have been recycled
- is.Equal(rq1.Jaws, jw)
- is.Equal(rq2.Jaws, jw)
+ th.Equal(rq1.Jaws, jw)
+ th.Equal(rq2.Jaws, jw)
+
+ th.NoErr(context.Cause(rq1ctx))
+ if !errors.Is(context.Cause(rq2ctx), ErrNoWebSocketRequest{}) {
+ th.Error(context.Cause(rq2ctx))
+ }
+
}
func TestJaws_BroadcastsCallable(t *testing.T) {
@@ -382,7 +385,7 @@ func TestJaws_BroadcastsCallable(t *testing.T) {
}
func TestJaws_subscribeOnClosedReturnsNil(t *testing.T) {
- is := testHelper{t}
+ th := newTestHelper(t)
jw := New()
jw.Close()
<-jw.doneCh
@@ -393,23 +396,23 @@ func TestJaws_subscribeOnClosedReturnsNil(t *testing.T) {
}
}
- is.Equal(jw.subscribe(nil, 1), nil)
+ th.Equal(jw.subscribe(jw.NewRequest(nil), 1), nil)
}
func TestJaws_GenerateHeadHTML(t *testing.T) {
const extraScript = "someExtraScript.js?disregard"
const extraStyle = "http://other.server/someExtraStyle.css"
- is := testHelper{t}
+ th := newTestHelper(t)
jw := New()
jw.Close()
jw.GenerateHeadHTML()
- is.True(strings.Contains(string(jw.headPrefix), JavascriptPath))
+ th.True(strings.Contains(string(jw.headPrefix), JavascriptPath))
jw.GenerateHeadHTML(extraScript, extraStyle)
- is.True(strings.Contains(string(jw.headPrefix), JavascriptPath))
- is.True(strings.Contains(string(jw.headPrefix), extraScript))
- is.True(strings.Contains(string(jw.headPrefix), extraStyle))
+ th.True(strings.Contains(string(jw.headPrefix), JavascriptPath))
+ th.True(strings.Contains(string(jw.headPrefix), extraScript))
+ th.True(strings.Contains(string(jw.headPrefix), extraStyle))
- is.True(jw.GenerateHeadHTML("random.crap") != nil)
- is.True(jw.GenerateHeadHTML("\n") != nil)
+ th.True(jw.GenerateHeadHTML("random.crap") != nil)
+ th.True(jw.GenerateHeadHTML("\n") != nil)
}
diff --git a/js_test.go b/js_test.go
index 3206236..b00a06a 100644
--- a/js_test.go
+++ b/js_test.go
@@ -3,6 +3,7 @@ package jaws
import (
"bytes"
"compress/gzip"
+ _ "embed"
"hash/fnv"
"strconv"
"strings"
@@ -12,33 +13,39 @@ import (
func Test_Javascript(t *testing.T) {
const prefix = "/jaws/jaws."
const suffix = ".js"
- is := testHelper{t}
+ th := newTestHelper(t)
h := fnv.New64a()
_, err := h.Write(JavascriptText)
- is.NoErr(err)
- is.Equal(JavascriptPath, prefix+strconv.FormatUint(h.Sum64(), 36)+suffix)
+ th.NoErr(err)
+ th.Equal(JavascriptPath, prefix+strconv.FormatUint(h.Sum64(), 36)+suffix)
- is.True(len(JavascriptText) > 0)
- is.True(len(JavascriptGZip) > 0)
- is.True(len(JavascriptGZip) < len(JavascriptText))
+ th.True(len(JavascriptText) > 0)
+ th.True(len(JavascriptGZip) > 0)
+ th.True(len(JavascriptGZip) < len(JavascriptText))
b := bytes.Buffer{}
gw := gzip.NewWriter(&b)
_, err = gw.Write(JavascriptText)
- is.NoErr(err)
- is.NoErr(gw.Close())
- is.Equal(b.Bytes(), JavascriptGZip)
+ th.NoErr(err)
+ th.NoErr(gw.Close())
+ th.Equal(b.Bytes(), JavascriptGZip)
}
func Test_HeadHTML(t *testing.T) {
const extraScript = "someExtraScript.js"
const extraStyle = "someExtraStyle.css"
- is := testHelper{t}
+ th := newTestHelper(t)
txt := HeadHTML(nil, nil)
- is.Equal(strings.Contains(string(txt), JavascriptPath), false)
+ th.Equal(strings.Contains(string(txt), JavascriptPath), false)
txt = HeadHTML([]string{JavascriptPath, extraScript}, []string{extraStyle})
- is.Equal(strings.Contains(string(txt), JavascriptPath), true)
- is.Equal(strings.Contains(string(txt), extraScript), true)
- is.Equal(strings.Contains(string(txt), extraStyle), true)
+ th.Equal(strings.Contains(string(txt), JavascriptPath), true)
+ th.Equal(strings.Contains(string(txt), extraScript), true)
+ th.Equal(strings.Contains(string(txt), extraStyle), true)
+}
+
+func TestJawsKeyString(t *testing.T) {
+ th := newTestHelper(t)
+ th.Equal(JawsKeyString(0), "")
+ th.Equal(JawsKeyString(1), "1")
}
diff --git a/namedbool_test.go b/namedbool_test.go
index 10e0ac5..5702000 100644
--- a/namedbool_test.go
+++ b/namedbool_test.go
@@ -6,7 +6,7 @@ import (
)
func TestNamedBool(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
nba := NewNamedBoolArray()
nba.Add("1", "one")
diff --git a/namedboolarray_test.go b/namedboolarray_test.go
index 52adb9f..73273b7 100644
--- a/namedboolarray_test.go
+++ b/namedboolarray_test.go
@@ -7,7 +7,7 @@ import (
)
func Test_NamedBoolArray(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
nba := NewNamedBoolArray()
is.Equal(len(nba.data), 0)
diff --git a/request.go b/request.go
index bffe14b..d98fe62 100644
--- a/request.go
+++ b/request.go
@@ -6,12 +6,12 @@ import (
"fmt"
"html"
"html/template"
+ "io"
"net/http"
"net/netip"
"slices"
"strconv"
"strings"
- "sync"
"sync/atomic"
"time"
@@ -37,6 +37,8 @@ type Request struct {
remoteIP netip.Addr // (read-only) remote IP, or nil
session *Session // (read-only) session, if established
mu deadlock.RWMutex // protects following
+ claimed bool // if UseRequest() has been called for it
+ running bool // if ServeHTTP() is running
todoDirt []interface{} // dirty tags
ctx context.Context // current context, derived from either Jaws or WS HTTP req
cancelFn context.CancelCauseFunc // cancel function
@@ -54,31 +56,6 @@ type eventFnCall struct {
const maxWsQueueLengthPerElement = 20
var ErrWebsocketQueueOverflow = errors.New("websocket queue overflow")
-var requestPool = sync.Pool{New: newRequest}
-
-func newRequest() interface{} {
- rq := &Request{
- tagMap: make(map[interface{}][]*Element),
- }
- return rq
-}
-
-func getRequest(jw *Jaws, jawsKey uint64, hr *http.Request) (rq *Request) {
- rq = requestPool.Get().(*Request)
- rq.Jaws = jw
- rq.JawsKey = jawsKey
- rq.Initial = hr
- rq.Created = time.Now()
- rq.ctx, rq.cancelFn = context.WithCancelCause(context.Background())
- if hr != nil {
- rq.remoteIP = parseIP(hr.RemoteAddr)
- if sess := jw.getSessionLocked(getCookieSessionsIds(hr.Header, jw.CookieName), rq.remoteIP); sess != nil {
- sess.addRequest(rq)
- rq.session = sess
- }
- }
- return rq
-}
func (rq *Request) JawsKeyString() string {
jawsKey := uint64(0)
@@ -92,20 +69,26 @@ func (rq *Request) String() string {
return "Request<" + rq.JawsKeyString() + ">"
}
-func (rq *Request) start(hr *http.Request) (err error) {
+var ErrRequestAlreadyClaimed = errors.New("request already claimed")
+
+func (rq *Request) claim(hr *http.Request) (err error) {
+ rq.mu.Lock()
+ defer rq.mu.Unlock()
+ if rq.claimed {
+ return ErrRequestAlreadyClaimed
+ }
var actualIP netip.Addr
ctx := context.Background()
if hr != nil {
actualIP = parseIP(hr.RemoteAddr)
ctx = hr.Context()
}
- rq.mu.Lock()
if equalIP(rq.remoteIP, actualIP) {
rq.ctx, rq.cancelFn = context.WithCancelCause(ctx)
+ rq.claimed = true
} else {
err = fmt.Errorf("/jaws/%s: expected IP %q, got %q", rq.JawsKeyString(), rq.remoteIP.String(), actualIP.String())
}
- rq.mu.Unlock()
return
}
@@ -122,28 +105,20 @@ func (rq *Request) killSession() {
rq.mu.Unlock()
}
-func (rq *Request) recycle() {
- rq.mu.Lock()
- jw := rq.Jaws
- if jw != nil {
- rq.Jaws = nil
- rq.JawsKey = 0
- rq.connectFn = nil
- rq.Initial = nil
- rq.Created = time.Time{}
- rq.ctx = context.Background()
- rq.cancelFn = nil
- rq.todoDirt = rq.todoDirt[:0]
- rq.remoteIP = netip.Addr{}
- rq.elems = rq.elems[:0]
- rq.killSessionLocked()
- clear(rq.tagMap)
- }
- rq.mu.Unlock()
- if jw != nil {
- jw.deactivate(rq)
- requestPool.Put(rq)
- }
+func (rq *Request) clearLocked() *Request {
+ rq.JawsKey = 0
+ rq.connectFn = nil
+ rq.Created = time.Time{}
+ rq.Initial = nil
+ rq.claimed = false
+ rq.running = false
+ rq.ctx, rq.cancelFn = context.WithCancelCause(context.Background())
+ rq.todoDirt = rq.todoDirt[:0]
+ rq.remoteIP = netip.Addr{}
+ rq.elems = rq.elems[:0]
+ rq.killSessionLocked()
+ clear(rq.tagMap)
+ return rq
}
// HeadHTML returns the HTML code needed to write in the HTML page's HEAD section.
@@ -194,17 +169,31 @@ func (rq *Request) Context() (ctx context.Context) {
return
}
-func (rq *Request) cancel(err error) {
- if rq != nil {
- rq.mu.RLock()
- cancelFn := rq.cancelFn
- rq.mu.RUnlock()
- if cancelFn != nil {
- cancelFn(err)
+func (rq *Request) maintenance(deadline time.Time) (doRecycle bool) {
+ rq.mu.Lock()
+ defer rq.mu.Unlock()
+ if doRecycle = (!rq.running && rq.Created.Before(deadline)); doRecycle {
+ rq.cancelLocked(newErrNoWebSocketRequest(rq))
+ }
+ return
+}
+
+func (rq *Request) cancelLocked(err error) {
+ if rq.JawsKey != 0 && rq.ctx.Err() == nil {
+ if !rq.running {
+ err = newErrPendingCancelled(rq, err)
}
+ rq.cancelFn(rq.Jaws.Log(err))
+ rq.killSessionLocked()
}
}
+func (rq *Request) cancel(err error) {
+ rq.mu.Lock()
+ defer rq.mu.Unlock()
+ rq.cancelLocked(err)
+}
+
// Alert attempts to show an alert message on the current request webpage if it has an HTML element with the id 'jaws-alert'.
// The lvl argument should be one of Bootstraps alert levels: primary, secondary, success, danger, warning, info, light or dark.
//
@@ -275,7 +264,6 @@ func (rq *Request) Register(tagitem interface{}, params ...interface{}) jid.Jid
elem := rq.NewElement(uib)
uib.parseGetter(elem, tagitem)
uib.parseParams(elem, params)
- rq.Dirty(uib.Tag)
return elem.jid
}
@@ -305,9 +293,9 @@ var nextJid Jid
func (rq *Request) newElementLocked(ui UI) (elem *Element) {
elem = &Element{
- jid: Jid(atomic.AddInt64((*int64)(&nextJid), 1)),
- ui: ui,
- Request: rq,
+ jid: Jid(atomic.AddInt64((*int64)(&nextJid), 1)),
+ ui: ui,
+ rq: rq,
}
rq.elems = append(rq.elems, elem)
return
@@ -371,8 +359,8 @@ func (rq *Request) tagExpanded(elem *Element, expandedtags []interface{}) {
// Tag adds the given tags to the given Element.
func (rq *Request) Tag(elem *Element, tags ...interface{}) {
- if elem != nil && len(tags) > 0 && elem.Request == rq {
- rq.tagExpanded(elem, MustTagExpand(elem.Request, tags))
+ if elem != nil && len(tags) > 0 && elem.rq == rq {
+ rq.tagExpanded(elem, MustTagExpand(elem.rq, tags))
}
}
@@ -413,7 +401,6 @@ func (rq *Request) process(broadcastMsgCh chan Message, incomingMsgCh <-chan wsM
defer func() {
rq.killSession()
- rq.Jaws.deactivate(rq)
rq.Jaws.unsubscribe(broadcastMsgCh)
close(eventCallCh)
for {
@@ -430,6 +417,7 @@ func (rq *Request) process(broadcastMsgCh chan Message, incomingMsgCh <-chan wsM
}
rq.Jaws.MustLog(err)
}
+ rq.Jaws.recycle(rq)
return
}
}
@@ -643,7 +631,7 @@ func (rq *Request) sendQueue(outboundCh chan<- string, wsQueue []wsMsg) []wsMsg
}
func (rq *Request) deleteElementLocked(e *Element) {
- e.Request = nil
+ e.rq = nil
rq.elems = slices.DeleteFunc(rq.elems, func(elem *Element) bool { return elem == e })
for k := range rq.tagMap {
rq.tagMap[k] = slices.DeleteFunc(rq.tagMap[k], func(elem *Element) bool { return elem == e })
@@ -701,3 +689,7 @@ func (rq *Request) onConnect() (err error) {
}
return
}
+
+func (rq *Request) Writer(w io.Writer) RequestWriter {
+ return RequestWriter{Request: rq, Writer: w}
+}
diff --git a/request_test.go b/request_test.go
index 755a673..5c1a1e8 100644
--- a/request_test.go
+++ b/request_test.go
@@ -29,7 +29,7 @@ func fillWsCh(ch chan string) {
}
func TestRequest_Registrations(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
@@ -49,28 +49,12 @@ func TestRequest_Registrations(t *testing.T) {
is.True(jid != jid2)
}
-func TestRequest_SendPanicsAfterRecycle(t *testing.T) {
- is := testHelper{t}
- defer func() {
- e := recover()
- if e == nil {
- is.Fail()
- }
- t.Log(e)
- }()
- jw := New()
- defer jw.Close()
- rq := jw.NewRequest(nil)
- rq.recycle()
- rq.Jaws.Broadcast(Message{})
-}
-
func TestRequest_HeadHTML(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
jw := New()
defer jw.Close()
rq := jw.NewRequest(nil)
- defer rq.recycle()
+ defer jw.recycle(rq)
txt := rq.HeadHTML()
is.Equal(strings.Contains(string(txt), rq.JawsKeyString()), true)
@@ -78,7 +62,7 @@ func TestRequest_HeadHTML(t *testing.T) {
}
func TestRequest_SendArrivesOk(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
jid := rq.Register("foo")
@@ -96,9 +80,7 @@ func TestRequest_SendArrivesOk(t *testing.T) {
}
func TestRequest_OutboundRespectsJawsClosed(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
- is := testHelper{t}
+ th := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
jw := rq.jw
@@ -107,24 +89,24 @@ func TestRequest_OutboundRespectsJawsClosed(t *testing.T) {
rq.ctx = context.Background()
rq.Register(tag, func(e *Element, evt what.What, val string) error {
atomic.AddInt32(&callCount, 1)
- is.Equal(1, jw.RequestCount())
+ th.Equal(1, jw.RequestCount())
jw.Close()
return nil
})
fillWsCh(rq.outCh)
jw.Broadcast(Message{Dest: Tag("foo"), What: what.Hook, Data: "bar"})
select {
- case <-tmr.C:
- is.Equal(int(atomic.LoadInt32(&callCount)), 0)
- is.Fail()
+ case <-th.C:
+ th.Equal(int(atomic.LoadInt32(&callCount)), 0)
+ th.Timeout()
case <-rq.Done():
- is.Error("request ctx cancelled too soon")
+ th.Error("request ctx cancelled too soon")
case <-jw.Done():
}
- is.Equal(int(atomic.LoadInt32(&callCount)), 1)
+ th.Equal(int(atomic.LoadInt32(&callCount)), 1)
select {
case <-rq.Done():
- is.Error("request ctx cancelled too soon")
+ th.Error("request ctx cancelled too soon")
default:
}
if jw.log.Len() != 0 {
@@ -133,9 +115,7 @@ func TestRequest_OutboundRespectsJawsClosed(t *testing.T) {
}
func TestRequest_OutboundRespectsContextDone(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
- is := testHelper{t}
+ th := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
var callCount int32
@@ -149,25 +129,25 @@ func TestRequest_OutboundRespectsContextDone(t *testing.T) {
rq.jw.Broadcast(Message{Dest: Tag("foo"), What: what.Hook, Data: "bar"})
select {
- case <-tmr.C:
- is.Equal(int(atomic.LoadInt32(&callCount)), 0)
- is.Fatal(int(atomic.LoadInt32(&callCount)))
+ case <-th.C:
+ th.Equal(int(atomic.LoadInt32(&callCount)), 0)
+ th.Timeout()
case <-rq.jw.Done():
- is.Fatal("jaws done too soon")
+ th.Fatal("jaws done too soon")
case <-rq.ctx.Done():
}
- is.Equal(int(atomic.LoadInt32(&callCount)), 1)
+ th.Equal(int(atomic.LoadInt32(&callCount)), 1)
select {
case <-rq.jw.Done():
- is.Fatal("jaws done too soon")
+ th.Fatal("jaws done too soon")
default:
}
}
func TestRequest_OutboundOverflowPanicsWithNoLogger(t *testing.T) {
- is := testHelper{t}
+ th := newTestHelper(t)
rq := newTestRequest()
rq.expectPanic = true
rq.jw.Logger = nil
@@ -176,16 +156,16 @@ func TestRequest_OutboundOverflowPanicsWithNoLogger(t *testing.T) {
fillWsCh(rq.outCh)
rq.Jaws.Broadcast(Message{Dest: Tag("foo"), What: what.Inner, Data: "bar"})
select {
- case <-time.NewTimer(testTimeout).C:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
case <-rq.doneCh:
- is.Equal(len(rq.outCh), cap(rq.outCh))
- is.True(rq.panicked)
+ th.Equal(len(rq.outCh), cap(rq.outCh))
+ th.True(rq.panicked)
}
}
func TestRequest_Trigger(t *testing.T) {
- is := testHelper{t}
+ th := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
gotFooCall := make(chan struct{})
@@ -207,32 +187,32 @@ func TestRequest_Trigger(t *testing.T) {
// rq.Broadcast(Message{Dest: Tag("err"), What: what.Input, Data: "baz"})
rq.jw.Broadcast(Message{Dest: Tag("end"), What: what.Input, Data: ""}) // to know when to stop
select {
- case <-time.NewTimer(testTimeout).C:
- is.Fail()
- case <-rq.outCh:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
+ case s := <-rq.outCh:
+ th.Fatal(s)
case <-gotFooCall:
- is.Fail()
+ th.Fatal("gotFooCall")
case <-gotEndCall:
}
// global broadcast should invoke fn
rq.jw.Broadcast(Message{Dest: Tag("foo"), What: what.Input, Data: "bar"})
select {
- case <-time.NewTimer(testTimeout).C:
- is.Fail()
- case <-rq.outCh:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
+ case s := <-rq.outCh:
+ th.Fatal(s)
case <-gotFooCall:
}
// fn returning error should send an danger alert message
rq.jw.Broadcast(Message{Dest: Tag("err"), What: what.Input, Data: "omg"})
select {
- case <-time.NewTimer(testTimeout).C:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
case msg := <-rq.outCh:
- is.Equal(msg, (&wsMsg{
+ th.Equal(msg, (&wsMsg{
Data: "danger\nomg",
Jid: jid.Jid(0),
What: what.Alert,
@@ -241,7 +221,7 @@ func TestRequest_Trigger(t *testing.T) {
}
func TestRequest_EventFnQueue(t *testing.T) {
- is := testHelper{t}
+ th := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
@@ -253,7 +233,7 @@ func TestRequest_EventFnQueue(t *testing.T) {
count := int(atomic.AddInt32(&callCount, 1))
if val != strconv.Itoa(count) {
t.Logf("val=%s, count=%d, cap=%d", val, count, cap(rq.outCh))
- is.Fail()
+ th.Fail()
}
if count == 1 {
close(firstDoneCh)
@@ -269,33 +249,32 @@ func TestRequest_EventFnQueue(t *testing.T) {
}
select {
- case <-time.NewTimer(testTimeout * 100).C:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
case <-rq.doneCh:
- is.Fail()
+ th.Fatal("doneCh")
case <-firstDoneCh:
}
- is.Equal(atomic.LoadInt32(&callCount), int32(1))
+ th.Equal(atomic.LoadInt32(&callCount), int32(1))
atomic.StoreInt32(&sleepDone, 1)
- is.Equal(rq.panicVal, nil)
+ th.Equal(rq.panicVal, nil)
- tmr := time.NewTimer(testTimeout * 100)
for int(atomic.LoadInt32(&callCount)) < cap(rq.outCh) {
select {
- case <-tmr.C:
+ case <-th.C:
t.Logf("callCount=%d, cap=%d", atomic.LoadInt32(&callCount), cap(rq.outCh))
- is.Equal(rq.panicVal, nil)
- is.Fail()
+ th.Equal(rq.panicVal, nil)
+ th.Timeout()
default:
time.Sleep(time.Millisecond)
}
}
- is.Equal(atomic.LoadInt32(&callCount), int32(cap(rq.outCh)))
+ th.Equal(atomic.LoadInt32(&callCount), int32(cap(rq.outCh)))
}
func TestRequest_EventFnQueueOverflowPanicsWithNoLogger(t *testing.T) {
- is := testHelper{t}
+ th := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
@@ -308,26 +287,23 @@ func TestRequest_EventFnQueueOverflowPanicsWithNoLogger(t *testing.T) {
rq.expectPanic = true
rq.jw.Logger = nil
- tmr := time.NewTimer(testTimeout)
-
jid := jidForTag(rq.Request, Tag("bomb"))
- defer tmr.Stop()
for {
select {
case <-rq.doneCh:
- is.True(rq.panicked)
- is.True(strings.Contains(rq.panicVal.(error).Error(), "eventCallCh is full sending"))
+ th.True(rq.panicked)
+ th.True(strings.Contains(rq.panicVal.(error).Error(), "eventCallCh is full sending"))
return
- case <-tmr.C:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
case rq.inCh <- wsMsg{Jid: jid, What: what.Input}:
}
}
}
func TestRequest_IgnoresIncomingMsgsDuringShutdown(t *testing.T) {
- is := testHelper{t}
+ th := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
@@ -355,39 +331,38 @@ func TestRequest_IgnoresIncomingMsgsDuringShutdown(t *testing.T) {
time.Sleep(time.Millisecond)
waited++
}
- is.Equal(atomic.LoadInt32(&spewState), int32(1))
- is.Equal(cap(rq.outCh), len(rq.outCh))
- is.True(waited < 1000)
+ th.Equal(atomic.LoadInt32(&spewState), int32(1))
+ th.Equal(cap(rq.outCh), len(rq.outCh))
+ th.True(waited < 1000)
// sending a message will now fail the rq since the
// outbound channel is full, but with the
// event fn holding it won't be able to end
select {
case rq.bcastCh <- Message{Dest: Tag("foo"), What: what.Inner, Data: ""}:
- case <-time.NewTimer(testTimeout).C:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
case <-rq.doneCh:
- is.Fail()
+ th.Fatal()
}
// rq should now be in shutdown phase draining channels
// while waiting for the event fn to return
- tmr := time.NewTimer(testTimeout)
for i := 0; i < cap(rq.outCh)*2; i++ {
select {
case <-rq.doneCh:
- is.Fail()
- case <-tmr.C:
- is.Fail()
+ th.Fatal()
+ case <-th.C:
+ th.Timeout()
default:
rq.Jaws.Broadcast(Message{Dest: rq})
}
select {
case rq.inCh <- wsMsg{}:
case <-rq.doneCh:
- is.Fail()
- case <-tmr.C:
- is.Fail()
+ th.Fatal()
+ case <-th.C:
+ th.Timeout()
}
}
@@ -396,18 +371,17 @@ func TestRequest_IgnoresIncomingMsgsDuringShutdown(t *testing.T) {
select {
case <-rq.doneCh:
- is.True(atomic.LoadInt32(&callCount) > 1)
- case <-time.NewTimer(testTimeout).C:
- is.Fail()
+ th.True(atomic.LoadInt32(&callCount) > 1)
+ case <-th.C:
+ th.Timeout()
}
// log data should contain message that we were unable to deliver error
- is.True(strings.Contains(rq.jw.log.String(), "outboundMsgCh full sending event"))
+ th.True(strings.Contains(rq.jw.log.String(), "outboundMsgCh full sending event"))
}
func TestRequest_Alert(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
tj := newTestJaws()
defer tj.Close()
rq1 := tj.newRequest(nil)
@@ -415,8 +389,8 @@ func TestRequest_Alert(t *testing.T) {
rq1.Alert("info", "\nnot\tescaped")
select {
- case <-tmr.C:
- t.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-rq1.outCh:
if s != "Alert\t\t\"info\\n\\nnot\\tescaped\"\n" {
t.Errorf("%q", s)
@@ -430,8 +404,7 @@ func TestRequest_Alert(t *testing.T) {
}
func TestRequest_Redirect(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
tj := newTestJaws()
defer tj.Close()
rq1 := tj.newRequest(nil)
@@ -439,8 +412,8 @@ func TestRequest_Redirect(t *testing.T) {
rq1.Redirect("some-url")
select {
- case <-tmr.C:
- t.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-rq1.outCh:
if s != "Redirect\t\t\"some-url\"\n" {
t.Errorf("%q", s)
@@ -454,15 +427,14 @@ func TestRequest_Redirect(t *testing.T) {
}
func TestRequest_AlertError(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
tj := newTestJaws()
defer tj.Close()
rq := tj.newRequest(nil)
rq.AlertError(errors.New("\nshould-be-escaped"))
select {
- case <-tmr.C:
- t.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "Alert\t\t\"danger\\n<html>\\nshould-be-escaped\"\n" {
t.Errorf("%q", s)
@@ -471,49 +443,47 @@ func TestRequest_AlertError(t *testing.T) {
}
func TestRequest_DeleteByTag(t *testing.T) {
- is := testHelper{t}
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
tj := newTestJaws()
defer tj.Close()
nextJid = 0
rq1 := tj.newRequest(nil)
ui1 := &testUi{}
e11 := rq1.NewElement(ui1)
- is.Equal(e11.jid, Jid(1))
+ th.Equal(e11.jid, Jid(1))
e11.Tag(Tag("e11"), Tag("foo"))
e12 := rq1.NewElement(ui1)
- is.Equal(e12.jid, Jid(2))
+ th.Equal(e12.jid, Jid(2))
e12.Tag(Tag("e12"))
e13 := rq1.NewElement(ui1)
- is.Equal(e13.jid, Jid(3))
+ th.Equal(e13.jid, Jid(3))
e13.Tag(Tag("e13"), Tag("bar"))
rq2 := tj.newRequest(nil)
ui2 := &testUi{}
e21 := rq2.NewElement(ui2)
- is.Equal(e21.jid, Jid(4))
+ th.Equal(e21.jid, Jid(4))
e21.Tag(Tag("e21"), Tag("foo"))
e22 := rq2.NewElement(ui2)
- is.Equal(e22.jid, Jid(5))
+ th.Equal(e22.jid, Jid(5))
e22.Tag(Tag("e22"))
e23 := rq2.NewElement(ui2)
- is.Equal(e23.jid, Jid(6))
+ th.Equal(e23.jid, Jid(6))
e23.Tag(Tag("e23"))
tj.Delete([]any{Tag("foo"), Tag("bar"), Tag("nothere"), Tag("e23")})
select {
- case <-tmr.C:
- t.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-rq1.outCh:
if s != "Delete\tJid.1\t\"\"\nDelete\tJid.3\t\"\"\n" {
t.Errorf("%q", s)
}
}
select {
- case <-tmr.C:
- t.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-rq2.outCh:
if s != "Delete\tJid.4\t\"\"\nDelete\tJid.6\t\"\"\n" {
t.Errorf("%q", s)
@@ -522,8 +492,7 @@ func TestRequest_DeleteByTag(t *testing.T) {
}
func TestRequest_HtmlIdBroadcast(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
tj := newTestJaws()
defer tj.Close()
rq1 := tj.newRequest(nil)
@@ -535,16 +504,16 @@ func TestRequest_HtmlIdBroadcast(t *testing.T) {
Data: "inner",
})
select {
- case <-tmr.C:
- t.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-rq1.outCh:
if s != "Inner\tfooId\t\"inner\"\n" {
t.Errorf("%q", s)
}
}
select {
- case <-tmr.C:
- t.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-rq2.outCh:
if s != "Inner\tfooId\t\"inner\"\n" {
t.Errorf("%q", s)
@@ -560,23 +529,23 @@ func jidForTag(rq *Request, tag interface{}) jid.Jid {
}
func TestRequest_ConnectFn(t *testing.T) {
- is := testHelper{t}
+ th := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
- is.Equal(rq.GetConnectFn(), nil)
- is.NoErr(rq.onConnect())
+ th.Equal(rq.GetConnectFn(), nil)
+ th.NoErr(rq.onConnect())
wantErr := errors.New("getouttahere")
fn := func(rq *Request) error {
return wantErr
}
rq.SetConnectFn(fn)
- is.Equal(rq.onConnect(), wantErr)
+ th.Equal(rq.onConnect(), wantErr)
}
func TestRequest_WsQueueOverflowCancels(t *testing.T) {
- is := testHelper{t}
+ th := newTestHelper(t)
jw := New()
defer jw.Close()
hr := httptest.NewRequest(http.MethodGet, "/", nil)
@@ -588,29 +557,28 @@ func TestRequest_WsQueueOverflowCancels(t *testing.T) {
}
}()
select {
- case <-time.NewTimer(testTimeout).C:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
case <-rq.Done():
}
- is.Equal(context.Cause(rq.Context()), ErrWebsocketQueueOverflow)
+ th.True(errors.Is(context.Cause(rq.Context()), ErrWebsocketQueueOverflow))
}
func TestRequest_Dirty(t *testing.T) {
- is := testHelper{t}
+ th := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
tss := &testUi{s: "foo"}
- h := rq.UI(NewUiText(tss))
- is.Equal(tss.getCalled, int32(1))
- is.True(strings.Contains(string(h), "foo"))
+ rq.UI(NewUiText(tss))
+ th.Equal(tss.getCalled, int32(1))
+ th.True(strings.Contains(string(rq.BodyString()), "foo"))
rq.Dirty(tss)
- tmr := time.NewTimer(testTimeout)
for atomic.LoadInt32(&tss.getCalled) < 2 {
select {
- case <-tmr.C:
- is.Fail()
+ case <-th.C:
+ th.Timeout()
default:
time.Sleep(time.Millisecond)
}
@@ -618,8 +586,7 @@ func TestRequest_Dirty(t *testing.T) {
}
func TestRequest_UpdatePanicLogs(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
nextJid = 0
rq := newTestRequest()
defer rq.Close()
@@ -631,8 +598,8 @@ func TestRequest_UpdatePanicLogs(t *testing.T) {
rq.UI(tss)
rq.Dirty(tss)
select {
- case <-tmr.C:
- t.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
case <-rq.doneCh:
}
if s := rq.jw.log.String(); !strings.Contains(s, "wildpanic") {
@@ -641,8 +608,7 @@ func TestRequest_UpdatePanicLogs(t *testing.T) {
}
func TestRequest_IncomingRemove(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
nextJid = 0
rq := newTestRequest()
defer rq.Close()
@@ -651,16 +617,16 @@ func TestRequest_IncomingRemove(t *testing.T) {
rq.UI(NewUiText(tss))
select {
- case <-tmr.C:
- t.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
case rq.inCh <- wsMsg{What: what.Remove, Data: "Jid.1"}:
}
elem := rq.getElementByJid(1)
for elem != nil {
select {
- case <-tmr.C:
- t.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
default:
time.Sleep(time.Millisecond)
elem = rq.getElementByJid(1)
@@ -669,8 +635,7 @@ func TestRequest_IncomingRemove(t *testing.T) {
}
func TestRequest_IncomingClick(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
nextJid = 0
rq := newTestRequest()
defer rq.Close()
@@ -689,14 +654,14 @@ func TestRequest_IncomingClick(t *testing.T) {
rq.Div("2", tjc2)
select {
- case <-tmr.C:
- t.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
case rq.inCh <- wsMsg{What: what.Click, Data: "name\tJid.1\tJid.2"}:
}
select {
- case <-tmr.C:
- t.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-tjc2.clickCh:
if s != "name" {
t.Error(s)
@@ -710,16 +675,16 @@ func TestRequest_IncomingClick(t *testing.T) {
}
func TestRequest_CustomErrors(t *testing.T) {
- is := testHelper{t}
+ th := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
cause := newErrNoWebSocketRequest(rq.Request)
err := newErrPendingCancelled(rq.Request, cause)
- is.True(errors.Is(err, ErrPendingCancelled{}))
- is.True(errors.Is(err, ErrNoWebSocketRequest{}))
- is.Equal(errors.Is(cause, ErrPendingCancelled{}), false)
+ th.True(errors.Is(err, ErrPendingCancelled{}))
+ th.True(errors.Is(err, ErrNoWebSocketRequest{}))
+ th.Equal(errors.Is(cause, ErrPendingCancelled{}), false)
var target1 ErrNoWebSocketRequest
- is.True(errors.As(err, &target1))
+ th.True(errors.As(err, &target1))
var target2 ErrPendingCancelled
- is.Equal(errors.As(cause, &target2), false)
+ th.Equal(errors.As(cause, &target2), false)
}
diff --git a/requestwriter.go b/requestwriter.go
new file mode 100644
index 0000000..351271d
--- /dev/null
+++ b/requestwriter.go
@@ -0,0 +1,12 @@
+package jaws
+
+import "io"
+
+type RequestWriter struct {
+ *Request
+ io.Writer
+}
+
+func (rw RequestWriter) UI(ui UI, params ...interface{}) error {
+ return rw.JawsRender(rw.NewElement(ui), rw.Writer, params)
+}
diff --git a/servehttp_test.go b/servehttp_test.go
index 536be28..ae35b1d 100644
--- a/servehttp_test.go
+++ b/servehttp_test.go
@@ -11,7 +11,7 @@ func TestServeHTTP_GetJavascript(t *testing.T) {
go jw.Serve()
defer jw.Close()
- is := testHelper{t}
+ is := newTestHelper(t)
mux := http.NewServeMux()
mux.Handle("/jaws/", jw)
@@ -51,7 +51,7 @@ func TestServeHTTP_GetJavascript(t *testing.T) {
}
func TestServeHTTP_GetPing(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
jw := New()
go jw.Serve()
defer jw.Close()
@@ -92,7 +92,7 @@ func TestServeHTTP_GetPing(t *testing.T) {
}
func TestServeHTTP_GetKey(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
jw := New()
go jw.Serve()
defer jw.Close()
@@ -109,9 +109,9 @@ func TestServeHTTP_GetKey(t *testing.T) {
is.Equal(w.Code, http.StatusNotFound)
is.Equal(w.Header()["Cache-Control"], nil)
+ w = httptest.NewRecorder()
rq := jw.NewRequest(req)
req = httptest.NewRequest("", "/jaws/"+rq.JawsKeyString(), nil)
- w = httptest.NewRecorder()
jw.ServeHTTP(w, req)
is.Equal(w.Code, http.StatusUpgradeRequired)
is.Equal(w.Header()["Cache-Control"], nil)
diff --git a/session_test.go b/session_test.go
index 48fc5c0..8edd8e8 100644
--- a/session_test.go
+++ b/session_test.go
@@ -212,8 +212,7 @@ func TestSession_Requests(t *testing.T) {
}
func TestSession_Delete(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
ts := newTestServer()
defer ts.Close()
go ts.jw.ServeWithTimeout(time.Second)
@@ -277,7 +276,7 @@ func TestSession_Delete(t *testing.T) {
}
ts.rq.Register("byebye", func(e *Element, evt what.What, val string) error {
- sess2 := ts.jw.GetSession(e.Request.Initial)
+ sess2 := ts.jw.GetSession(e.Request().Initial)
if x := sess2; x != ts.sess {
t.Error(x)
}
@@ -348,8 +347,8 @@ func TestSession_Delete(t *testing.T) {
}
select {
- case <-tmr.C:
- t.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
case rr, ok := <-resultChan:
if ok {
if x := rr.err; x != nil {
@@ -402,7 +401,7 @@ func TestSession_Cleanup(t *testing.T) {
t.Error(x)
}
- r1.recycle()
+ jw.recycle(r1)
r1 = nil
sess.deadline = time.Now()
if x := jw.SessionCount(); x != 1 {
@@ -425,7 +424,7 @@ func TestSession_ReplacesOld(t *testing.T) {
defer jw.Close()
go jw.ServeWithTimeout(time.Second)
- is := testHelper{t}
+ is := newTestHelper(t)
is.Equal(jw.SessionCount(), 0)
@@ -480,7 +479,7 @@ func TestSession_ReplacesOld(t *testing.T) {
is.Equal(jw.GetSession(h4), s1)
is.Equal(len(w4.Result().Cookies()), 0)
- r4.recycle()
+ jw.recycle(r4)
w3 := httptest.NewRecorder()
h3 := httptest.NewRequest("GET", "/", nil)
diff --git a/template.go b/template.go
index d5e6a8c..d0d25c1 100644
--- a/template.go
+++ b/template.go
@@ -39,16 +39,22 @@ func (t Template) String() string {
return fmt.Sprintf("{%q, %s}", t.Template.Name(), TagString(t.Dot))
}
-func (t Template) JawsRender(e *Element, w io.Writer, params []interface{}) {
- if expandedtags, err := TagExpand(e.Request, t.Dot); err != ErrIllegalTagType {
- e.Request.tagExpanded(e, expandedtags)
+func (t Template) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ if expandedtags, err := TagExpand(e.Request(), t.Dot); err != ErrIllegalTagType {
+ e.Request().tagExpanded(e, expandedtags)
}
var sb strings.Builder
for _, s := range parseParams(e, params) {
sb.WriteByte(' ')
sb.WriteString(s)
}
- maybePanic(t.Execute(w, With{Element: e, Dot: t.Dot, Attrs: template.HTMLAttr(sb.String())})) // #nosec G203
+ attrs := template.HTMLAttr(sb.String()) // #nosec G203
+ return t.Execute(w, With{
+ Element: e,
+ RequestWriter: e.Request().Writer(w),
+ Dot: t.Dot,
+ Attrs: attrs,
+ })
}
func (t Template) JawsUpdate(e *Element) {
diff --git a/template_test.go b/template_test.go
index 2d5571f..88cfd1c 100644
--- a/template_test.go
+++ b/template_test.go
@@ -7,7 +7,7 @@ import (
)
func TestTemplate_String(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
diff --git a/testhelper_test.go b/testhelper_test.go
index fa528af..9bbd19d 100644
--- a/testhelper_test.go
+++ b/testhelper_test.go
@@ -3,52 +3,53 @@ package jaws
import (
"reflect"
"testing"
+ "time"
)
-type testHelper struct{ *testing.T }
+type testHelper struct {
+ *time.Timer
+ *testing.T
+}
-func testNil(object any) (bool, reflect.Type) {
- if object == nil {
- return true, nil
+func newTestHelper(t *testing.T) (th *testHelper) {
+ th = &testHelper{
+ T: t,
+ Timer: time.NewTimer(time.Second * 3),
}
- value := reflect.ValueOf(object)
- kind := value.Kind()
- return kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil(), value.Type()
+ t.Cleanup(th.Cleanup)
+ return
}
-func testEqual(a, b any) bool {
- if reflect.DeepEqual(a, b) {
- return true
- }
- aIsNil, aType := testNil(a)
- bIsNil, bType := testNil(b)
- if !(aIsNil && bIsNil) {
- return false
- }
- return aType == nil || bType == nil || (aType == bType)
+func (th *testHelper) Cleanup() {
+ th.Timer.Stop()
}
-func (th testHelper) Equal(a, b any) {
+func (th *testHelper) Equal(a, b any) {
if !testEqual(a, b) {
th.Helper()
th.Errorf("%T(%v) != %T(%v)", a, a, b, b)
}
}
-func (th testHelper) True(a bool) {
+func (th *testHelper) True(a bool) {
if !a {
th.Helper()
th.Error("not true")
}
}
-func (th testHelper) NoErr(err error) {
+func (th *testHelper) NoErr(err error) {
if err != nil {
th.Helper()
th.Error(err)
}
}
+func (th *testHelper) Timeout() {
+ th.Helper()
+ th.Fatal("timeout")
+}
+
func Test_testHelper(t *testing.T) {
mustEqual := func(a, b any) {
if !testEqual(a, b) {
@@ -74,3 +75,24 @@ func Test_testHelper(t *testing.T) {
mustNotEqual((*testing.T)(nil), (*testHelper)(nil))
mustNotEqual(int(1), int32(1))
}
+
+func testNil(object any) (bool, reflect.Type) {
+ if object == nil {
+ return true, nil
+ }
+ value := reflect.ValueOf(object)
+ kind := value.Kind()
+ return kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil(), value.Type()
+}
+
+func testEqual(a, b any) bool {
+ if reflect.DeepEqual(a, b) {
+ return true
+ }
+ aIsNil, aType := testNil(a)
+ bIsNil, bType := testNil(b)
+ if !(aIsNil && bIsNil) {
+ return false
+ }
+ return aType == nil || bType == nil || (aType == bType)
+}
diff --git a/testjaws_test.go b/testjaws_test.go
index de3c63e..a396a1f 100644
--- a/testjaws_test.go
+++ b/testjaws_test.go
@@ -28,6 +28,7 @@ func newTestJaws() (tj *testJaws) {
type testRequest struct {
hr *http.Request
+ rr *httptest.ResponseRecorder
jw *testJaws
readyCh chan struct{}
doneCh chan struct{}
@@ -40,6 +41,7 @@ type testRequest struct {
panicked bool
panicVal any
*Request
+ RequestWriter
}
func (tj *testJaws) newRequest(hr *http.Request) (tr *testRequest) {
@@ -48,6 +50,8 @@ func (tj *testJaws) newRequest(hr *http.Request) (tr *testRequest) {
}
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
hr = hr.WithContext(ctx)
+ rr := httptest.NewRecorder()
+ rr.Body = &bytes.Buffer{}
rq := tj.NewRequest(hr)
if rq == nil || tj.UseRequest(rq.JawsKey, hr) != rq {
panic("failed to create or use jaws.Request")
@@ -58,16 +62,18 @@ func (tj *testJaws) newRequest(hr *http.Request) (tr *testRequest) {
}
tr = &testRequest{
- hr: hr,
- jw: tj,
- readyCh: make(chan struct{}),
- doneCh: make(chan struct{}),
- inCh: make(chan wsMsg),
- outCh: make(chan string, cap(bcastCh)),
- bcastCh: bcastCh,
- ctx: ctx,
- cancel: cancel,
- Request: rq,
+ hr: hr,
+ rr: rr,
+ jw: tj,
+ readyCh: make(chan struct{}),
+ doneCh: make(chan struct{}),
+ inCh: make(chan wsMsg),
+ outCh: make(chan string, cap(bcastCh)),
+ bcastCh: bcastCh,
+ ctx: ctx,
+ cancel: cancel,
+ Request: rq,
+ RequestWriter: rq.Writer(rr),
}
go func() {
@@ -81,17 +87,29 @@ func (tj *testJaws) newRequest(hr *http.Request) (tr *testRequest) {
}()
close(tr.readyCh)
tr.process(tr.bcastCh, tr.inCh, tr.outCh) // usubs from bcase, closes outCh
- tr.recycle()
+ tr.jw.recycle(tr.Request)
}()
return
}
+func (tr *testRequest) BodyString() string {
+ return tr.rr.Body.String()
+}
+
+func (tr *testRequest) BodyHtml() template.HTML {
+ return template.HTML(tr.BodyString())
+}
+
func (tr *testRequest) Close() {
tr.cancel()
tr.jw.Close()
}
+func (tr *testRequest) Write(buf []byte) (int, error) {
+ return tr.rr.Write(buf)
+}
+
func newTestRequest() (tr *testRequest) {
tj := newTestJaws()
return tj.newRequest(nil)
diff --git a/ui.go b/ui.go
index aa5b2d7..81cc575 100644
--- a/ui.go
+++ b/ui.go
@@ -2,7 +2,6 @@ package jaws
import (
"fmt"
- "html/template"
"io"
"strings"
)
@@ -10,28 +9,24 @@ import (
// If any of these functions panic, the Request will be closed and the panic logged.
// Optionally you may also implement ClickHandler and/or EventHandler.
type UI interface {
- JawsRender(e *Element, w io.Writer, params []interface{})
+ JawsRender(e *Element, w io.Writer, params []interface{}) error
JawsUpdate(e *Element)
}
-func (rq *Request) UI(ui UI, params ...interface{}) template.HTML {
- var sb strings.Builder
- rq.JawsRender(rq.NewElement(ui), &sb, params)
- return template.HTML(sb.String()) // #nosec G203
-}
-
-func (rq *Request) JawsRender(elem *Element, w io.Writer, params []interface{}) {
- elem.ui.JawsRender(elem, w, params)
- if rq.Jaws.Debug {
- var sb strings.Builder
- _, _ = fmt.Fprintf(&sb, "", "==>") + " -->"))
}
- sb.WriteByte(']')
- _, _ = w.Write([]byte(strings.ReplaceAll(sb.String(), "-->", "==>") + " -->"))
}
+ return
}
diff --git a/ui_test.go b/ui_test.go
index ee2c7f4..de69da3 100644
--- a/ui_test.go
+++ b/ui_test.go
@@ -1,9 +1,14 @@
package jaws
import (
+ "bytes"
+ "html/template"
"io"
+ "net/http"
+ "net/http/httptest"
"strings"
"testing"
+ "time"
)
type testStringer struct{}
@@ -14,17 +19,115 @@ func (s testStringer) String() string {
func TestRequest_JawsRender_DebugOutput(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
rq.Jaws.Debug = true
- h := string(rq.UI(&testUi{renderFn: func(e *Element, w io.Writer, params []any) {
+ rq.UI(&testUi{renderFn: func(e *Element, w io.Writer, params []any) error {
e.Tag(Tag("footag"))
- e.Tag(e.Request)
+ e.Tag(e.Request())
e.Tag(testStringer{})
- }}))
+ return nil
+ }})
+ h := rq.BodyString()
t.Log(h)
is.True(strings.Contains(h, "footag"))
is.True(strings.Contains(h, "*jaws.testUi"))
is.True(strings.Contains(h, testStringer{}.String()))
}
+
+func TestRequest_InsideTemplate(t *testing.T) {
+ jw := New()
+ defer jw.Close()
+ nextJid = 4
+
+ const tmplText = "(" +
+ "{{$.Initial.URL.Path}}" +
+ "{{$.A `a`}}" +
+ "{{$.Button `button`}}" +
+ "{{$.Checkbox .TheBool `checkbox`}}" +
+ "{{$.Container `container` .TheContainer}}" +
+ "{{$.Date .TheTime `date`}}" +
+ "{{$.Div `div`}}" +
+ "{{$.Img `img`}}" +
+ "{{$.Label `label`}}" +
+ "{{$.Li `li`}}" +
+ "{{$.Number .TheNumber}}" +
+ "{{$.Password .TheString}}" +
+ "{{$.Radio .TheBool}}" +
+ "{{$.Range .TheNumber}}" +
+ "{{$.Select .TheSelector}}" +
+ "{{$.Span `span`}}" +
+ "{{$.Tbody .TheContainer}}" +
+ "{{$.Td `td`}}" +
+ "{{$.Template `nested` .TheDot}}" +
+ "{{$.Text .TheString}}" +
+ "{{$.Textarea .TheString}}" +
+ "{{$.Tr `tr`}}" +
+ ")"
+ const nestedTmplText = "" +
+ "{{$.Initial.URL.Path}}" +
+ "{{with .Dot}}{{.}}{{$.Span `span2`}}{{end}}" +
+ ""
+ const want = "(" +
+ "/path" +
+ "a" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "div
" +
+ "" +
+ "" +
+ "li" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "span" +
+ "" +
+ "td | " +
+ "/pathdotspan2" +
+ "" +
+ "" +
+ "tr
" +
+ ")"
+
+ jw.Template = template.Must(template.New("nested").Parse(nestedTmplText))
+ tmpl := template.Must(template.New("normal").Parse(tmplText))
+ w := httptest.NewRecorder()
+ w.Body = &bytes.Buffer{}
+ hr := httptest.NewRequest(http.MethodGet, "/path", nil)
+ rq := jw.NewRequest(hr)
+ testDate, _ := time.Parse(ISO8601, "1901-02-03")
+ dot := struct {
+ RequestWriter
+ TheBool BoolSetter
+ TheContainer Container
+ TheTime TimeSetter
+ TheNumber FloatSetter
+ TheString StringSetter
+ TheSelector SelectHandler
+ TheDot any
+ }{
+ RequestWriter: RequestWriter{rq, w},
+ TheBool: newTestSetter(true),
+ TheContainer: &testContainer{},
+ TheTime: newTestSetter(testDate),
+ TheNumber: newTestSetter(float64(1.2)),
+ TheString: newTestSetter("bar"),
+ TheSelector: &testNamedBoolArray{
+ setCalled: make(chan struct{}),
+ NamedBoolArray: NewNamedBoolArray(),
+ },
+ TheDot: "dot",
+ }
+ if err := tmpl.Execute(w, dot); err != nil {
+ t.Fatal(err)
+ }
+ w.Flush()
+ if x := w.Body.String(); x != want {
+ t.Errorf("%q", x)
+ }
+}
diff --git a/uia.go b/uia.go
index 6da006b..6285c9a 100644
--- a/uia.go
+++ b/uia.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -9,8 +8,8 @@ type UiA struct {
UiHtmlInner
}
-func (ui *UiA) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderInner(e, w, "a", "", params)
+func (ui *UiA) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderInner(e, w, "a", "", params)
}
func NewUiA(innerHtml HtmlGetter) *UiA {
@@ -21,6 +20,6 @@ func NewUiA(innerHtml HtmlGetter) *UiA {
}
}
-func (rq *Request) A(innerHtml interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) A(innerHtml interface{}, params ...interface{}) error {
return rq.UI(NewUiA(makeHtmlGetter(innerHtml)), params...)
}
diff --git a/uia_test.go b/uia_test.go
index 90001f9..7e8407b 100644
--- a/uia_test.go
+++ b/uia_test.go
@@ -54,7 +54,8 @@ func TestRequest_A(t *testing.T) {
nextJid = 0
rq := newTestRequest()
defer rq.Close()
- if got := rq.A(tt.args.innerHtml, tt.args.params...); !reflect.DeepEqual(got, tt.want) {
+ rq.A(tt.args.innerHtml, tt.args.params...)
+ if got := rq.BodyHtml(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Request.A() = %v, want %v", got, tt.want)
}
})
diff --git a/uibutton.go b/uibutton.go
index 604c303..7f5763e 100644
--- a/uibutton.go
+++ b/uibutton.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -9,8 +8,8 @@ type UiButton struct {
UiHtmlInner
}
-func (ui *UiButton) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderInner(e, w, "button", "button", params)
+func (ui *UiButton) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderInner(e, w, "button", "button", params)
}
func NewUiButton(innerHtml HtmlGetter) *UiButton {
@@ -21,6 +20,6 @@ func NewUiButton(innerHtml HtmlGetter) *UiButton {
}
}
-func (rq *Request) Button(innerHtml interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Button(innerHtml interface{}, params ...interface{}) error {
return rq.UI(NewUiButton(makeHtmlGetter(innerHtml)), params...)
}
diff --git a/uibutton_test.go b/uibutton_test.go
index fd759fc..594cc29 100644
--- a/uibutton_test.go
+++ b/uibutton_test.go
@@ -9,7 +9,8 @@ func TestRequest_Button(t *testing.T) {
rq := newTestRequest()
defer rq.Close()
want := ``
- if got := string(rq.Button("inner")); got != want {
+ rq.Button("inner")
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Button() = %q, want %q", got, want)
}
}
diff --git a/uicheckbox.go b/uicheckbox.go
index 23d59b1..17c32c6 100644
--- a/uicheckbox.go
+++ b/uicheckbox.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -9,8 +8,8 @@ type UiCheckbox struct {
UiInputBool
}
-func (ui *UiCheckbox) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderBoolInput(e, w, "checkbox", params...)
+func (ui *UiCheckbox) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderBoolInput(e, w, "checkbox", params...)
}
func NewUiCheckbox(g BoolSetter) *UiCheckbox {
@@ -21,6 +20,6 @@ func NewUiCheckbox(g BoolSetter) *UiCheckbox {
}
}
-func (rq *Request) Checkbox(value interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Checkbox(value interface{}, params ...interface{}) error {
return rq.UI(NewUiCheckbox(makeBoolSetter(value)), params...)
}
diff --git a/uicheckbox_test.go b/uicheckbox_test.go
index ec89cc1..62eadb9 100644
--- a/uicheckbox_test.go
+++ b/uicheckbox_test.go
@@ -3,29 +3,28 @@ package jaws
import (
"errors"
"testing"
- "time"
"github.com/linkdata/jaws/what"
)
func TestRequest_Checkbox(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
nextJid = 0
rq := newTestRequest()
defer rq.Close()
ts := newTestSetter(true)
want := ``
- if got := string(rq.Checkbox(ts)); got != want {
+ rq.Checkbox(ts)
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Checkbox() = %q, want %q", got, want)
}
val := false
rq.inCh <- wsMsg{Data: "false", Jid: 1, What: what.Input}
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case <-ts.setCalled:
}
if ts.Get() != val {
@@ -41,8 +40,8 @@ func TestRequest_Checkbox(t *testing.T) {
ts.Set(val)
rq.Dirty(ts)
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "Value\tJid.1\t\"true\"\n" {
t.Errorf("%q", s)
@@ -57,8 +56,8 @@ func TestRequest_Checkbox(t *testing.T) {
rq.inCh <- wsMsg{Data: "omg", Jid: 1, What: what.Input}
select {
- case <-tmr.C:
- t.Error("timeout waiting for Alert")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "Alert\t\t\"danger\\nstrconv.ParseBool: parsing "omg": invalid syntax\"\n" {
t.Errorf("wrong Alert: %q", s)
@@ -68,8 +67,8 @@ func TestRequest_Checkbox(t *testing.T) {
ts.err = errors.New("meh")
rq.inCh <- wsMsg{Data: "true", Jid: 1, What: what.Input}
select {
- case <-tmr.C:
- t.Error("timeout waiting for Alert")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "Alert\t\t\"danger\\nmeh\"\n" {
t.Errorf("wrong Alert: %q", s)
diff --git a/uicontainer.go b/uicontainer.go
index 605fb0a..7a2d6ac 100644
--- a/uicontainer.go
+++ b/uicontainer.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -19,10 +18,10 @@ func NewUiContainer(outerHtmlTag string, c Container) *UiContainer {
}
}
-func (ui *UiContainer) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderContainer(e, w, ui.OuterHtmlTag, params)
+func (ui *UiContainer) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderContainer(e, w, ui.OuterHtmlTag, params)
}
-func (rq *Request) Container(outerHtmlTag string, c Container, params ...interface{}) template.HTML {
+func (rq RequestWriter) Container(outerHtmlTag string, c Container, params ...interface{}) error {
return rq.UI(NewUiContainer(outerHtmlTag, c), params...)
}
diff --git a/uicontainer_test.go b/uicontainer_test.go
index caf97b2..84c179e 100644
--- a/uicontainer_test.go
+++ b/uicontainer_test.go
@@ -60,7 +60,8 @@ func TestRequest_Container(t *testing.T) {
nextJid = 0
rq := newTestRequest()
defer rq.Close()
- if got := rq.Container("div", tt.args.c, tt.args.params...); !reflect.DeepEqual(got, tt.want) {
+ rq.Container("div", tt.args.c, tt.args.params...)
+ if got := rq.BodyHtml(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Request.Container() = %v, want %v", got, tt.want)
}
})
@@ -171,7 +172,9 @@ func TestRequest_Container_Alteration(t *testing.T) {
ui := NewUiContainer("div", tt.c)
elem := rq.NewElement(ui)
var sb strings.Builder
- ui.JawsRender(elem, &sb, nil)
+ if err := ui.JawsRender(elem, &sb, nil); err != nil {
+ t.Fatal(err)
+ }
tt.c.contents = tt.l
ui.JawsUpdate(elem)
if !slices.Equal(elem.wsQueue, tt.want) {
diff --git a/uidate.go b/uidate.go
index 98180e7..54ddf77 100644
--- a/uidate.go
+++ b/uidate.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -11,8 +10,8 @@ type UiDate struct {
UiInputDate
}
-func (ui *UiDate) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderDateInput(e, w, e.Jid(), "date", params...)
+func (ui *UiDate) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderDateInput(e, w, e.Jid(), "date", params...)
}
func NewUiDate(g TimeSetter) *UiDate {
@@ -23,6 +22,6 @@ func NewUiDate(g TimeSetter) *UiDate {
}
}
-func (rq *Request) Date(value interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Date(value interface{}, params ...interface{}) error {
return rq.UI(NewUiDate(makeTimeSetter(value)), params...)
}
diff --git a/uidate_test.go b/uidate_test.go
index eba6cae..5b05e9e 100644
--- a/uidate_test.go
+++ b/uidate_test.go
@@ -10,13 +10,15 @@ import (
)
func TestRequest_Date(t *testing.T) {
+ th := newTestHelper(t)
nextJid = 0
rq := newTestRequest()
defer rq.Close()
ts := newTestSetter(time.Now())
want := fmt.Sprintf(``, ts.Get().Format(ISO8601))
- if got := string(rq.Date(ts)); got != want {
+ rq.Date(ts)
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Date() = %q, want %q", got, want)
}
@@ -25,8 +27,8 @@ func TestRequest_Date(t *testing.T) {
tmr := time.NewTimer(testTimeout)
defer tmr.Stop()
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case <-ts.setCalled:
}
if ts.Get() != val {
@@ -42,8 +44,8 @@ func TestRequest_Date(t *testing.T) {
ts.Set(val)
rq.Dirty(ts)
select {
- case <-tmr.C:
- t.Error("timeout waiting for Value")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != fmt.Sprintf("Value\tJid.1\t\"%s\"\n", val.Format(ISO8601)) {
t.Error("wrong Value")
@@ -58,8 +60,8 @@ func TestRequest_Date(t *testing.T) {
rq.inCh <- wsMsg{Data: "omg", Jid: 1, What: what.Input}
select {
- case <-tmr.C:
- t.Error("timeout waiting for Alert")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "Alert\t\t\"danger\\nparsing time "omg" as "2006-01-02": cannot parse "omg" as "2006"\"\n" {
t.Errorf("wrong Alert: %q", s)
@@ -69,8 +71,8 @@ func TestRequest_Date(t *testing.T) {
ts.err = errors.New("meh")
rq.inCh <- wsMsg{Data: val.Format(ISO8601), Jid: 1, What: what.Input}
select {
- case <-tmr.C:
- t.Error("timeout waiting for Alert")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "Alert\t\t\"danger\\nmeh\"\n" {
t.Errorf("wrong Alert: %q", s)
diff --git a/uidiv.go b/uidiv.go
index a12046f..ededbf0 100644
--- a/uidiv.go
+++ b/uidiv.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -9,8 +8,8 @@ type UiDiv struct {
UiHtmlInner
}
-func (ui *UiDiv) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderInner(e, w, "div", "", params)
+func (ui *UiDiv) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderInner(e, w, "div", "", params)
}
func NewUiDiv(innerHtml HtmlGetter) *UiDiv {
@@ -21,6 +20,6 @@ func NewUiDiv(innerHtml HtmlGetter) *UiDiv {
}
}
-func (rq *Request) Div(innerHtml interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Div(innerHtml interface{}, params ...interface{}) error {
return rq.UI(NewUiDiv(makeHtmlGetter(innerHtml)), params...)
}
diff --git a/uidiv_test.go b/uidiv_test.go
index ba80c32..48c4fb7 100644
--- a/uidiv_test.go
+++ b/uidiv_test.go
@@ -9,7 +9,8 @@ func TestRequest_Div(t *testing.T) {
rq := newTestRequest()
defer rq.Close()
want := `inner
`
- if got := string(rq.Div("inner")); got != want {
+ rq.Div("inner")
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Div() = %q, want %q", got, want)
}
}
diff --git a/uihtml.go b/uihtml.go
index 0457da8..8bbad2d 100644
--- a/uihtml.go
+++ b/uihtml.go
@@ -14,7 +14,7 @@ type UiHtml struct {
func (ui *UiHtml) parseGetter(e *Element, getter interface{}) {
if getter != nil {
if tagger, ok := getter.(TagGetter); ok {
- ui.Tag = tagger.JawsGetTag(e.Request)
+ ui.Tag = tagger.JawsGetTag(e.Request())
if ch, ok := getter.(ClickHandler); ok {
e.handlers = append(e.handlers, clickHandlerWapper{ch})
}
@@ -63,10 +63,11 @@ func (ui *UiHtml) parseParams(elem *Element, params []interface{}) (attrs []stri
return
}
-func (ui *UiHtml) JawsRender(e *Element, w io.Writer, params []interface{}) {
+func (ui *UiHtml) JawsRender(e *Element, w io.Writer, params []interface{}) (err error) {
if h, ok := ui.Tag.(UI); ok {
- h.JawsRender(e, w, params)
+ err = h.JawsRender(e, w, params)
}
+ return
}
func (ui *UiHtml) JawsUpdate(e *Element) {
diff --git a/uihtml_test.go b/uihtml_test.go
index 4dc4543..4816cde 100644
--- a/uihtml_test.go
+++ b/uihtml_test.go
@@ -6,7 +6,6 @@ import (
"io"
"strings"
"testing"
- "time"
"github.com/linkdata/jaws/what"
)
@@ -27,16 +26,20 @@ func (tje *testJawsEvent) JawsEvent(e *Element, wht what.What, val string) (err
}
func (tje *testJawsEvent) JawsGetTag(*Request) (tag any) {
- return tje.tag
+ if tje.tag != nil {
+ return tje.tag
+ }
+ return nil
}
-func (tje *testJawsEvent) JawsRender(e *Element, w io.Writer, params []any) {
+func (tje *testJawsEvent) JawsRender(e *Element, w io.Writer, params []any) error {
w.Write([]byte(fmt.Sprint(params)))
- tje.msgCh <- "JawsRender"
+ tje.msgCh <- fmt.Sprintf("JawsRender(%d)", e.jid)
+ return nil
}
func (tje *testJawsEvent) JawsUpdate(e *Element) {
- tje.msgCh <- "JawsUpdate"
+ tje.msgCh <- fmt.Sprintf("JawsUpdate(%d)", e.jid)
}
var _ ClickHandler = (*testJawsEvent)(nil)
@@ -45,8 +48,7 @@ var _ TagGetter = (*testJawsEvent)(nil)
var _ UI = (*testJawsEvent)(nil)
func TestUiHtml_JawsEvent(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
nextJid = 0
rq := newTestRequest()
defer rq.Close()
@@ -59,8 +61,8 @@ func TestUiHtml_JawsEvent(t *testing.T) {
rq.inCh <- wsMsg{Data: "text", Jid: id, What: what.Input}
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-tje.msgCh:
if s != "JawsEvent: Input \"text\"" {
t.Error(s)
@@ -69,8 +71,8 @@ func TestUiHtml_JawsEvent(t *testing.T) {
rq.inCh <- wsMsg{Data: "name", Jid: id, What: what.Click}
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-msgCh:
if s != "JawsClick: \"name\"" {
t.Error(s)
@@ -79,21 +81,27 @@ func TestUiHtml_JawsEvent(t *testing.T) {
tje.tag = tje
id2 := rq.Register(tje)
+ th.Equal(id2, Jid(2))
rq.inCh <- wsMsg{Data: "text2", Jid: id2, What: what.Input}
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-tje.msgCh:
if s != "JawsEvent: Input \"text2\"" {
t.Error(s)
}
}
+ // nothing should be marked dirty,
+ // but if it is, this ensures the
+ // test fails reliably
+ rq.jw.distributeDirt()
+
rq.inCh <- wsMsg{Data: "name2", Jid: id2, What: what.Click}
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-msgCh:
if s != "JawsClick: \"name2\"" {
t.Error(s)
@@ -102,22 +110,24 @@ func TestUiHtml_JawsEvent(t *testing.T) {
rq.Dirty(tje)
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-msgCh:
- if s != "JawsUpdate" {
+ if s != "JawsUpdate(2)" {
t.Error(s)
}
}
elem := rq.getElementByJid(id2)
var sb strings.Builder
- elem.ui.JawsRender(elem, &sb, []any{"attr"})
+ if err := elem.ui.JawsRender(elem, &sb, []any{"attr"}); err != nil {
+ t.Fatal(err)
+ }
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-msgCh:
- if s != "JawsRender" {
+ if s != "JawsRender(2)" {
t.Error(s)
}
}
diff --git a/uihtmlinner.go b/uihtmlinner.go
index 42e06bb..955e5ae 100644
--- a/uihtmlinner.go
+++ b/uihtmlinner.go
@@ -9,10 +9,10 @@ type UiHtmlInner struct {
HtmlGetter
}
-func (ui *UiHtmlInner) renderInner(e *Element, w io.Writer, htmltag, htmltype string, params []interface{}) {
+func (ui *UiHtmlInner) renderInner(e *Element, w io.Writer, htmltag, htmltype string, params []interface{}) error {
ui.parseGetter(e, ui.HtmlGetter)
attrs := ui.parseParams(e, params)
- maybePanic(WriteHtmlInner(w, e.Jid(), htmltag, htmltype, ui.JawsGetHtml(e), attrs...))
+ return WriteHtmlInner(w, e.Jid(), htmltag, htmltype, ui.JawsGetHtml(e), attrs...)
}
func (ui *UiHtmlInner) JawsUpdate(e *Element) {
diff --git a/uihtmlinner_test.go b/uihtmlinner_test.go
index d11a9ef..958de8f 100644
--- a/uihtmlinner_test.go
+++ b/uihtmlinner_test.go
@@ -20,7 +20,9 @@ func TestUiHtmlInner_JawsUpdate(t *testing.T) {
ui := NewUiDiv(ts)
elem := rq.NewElement(ui)
var sb strings.Builder
- ui.JawsRender(elem, &sb, nil)
+ if err := ui.JawsRender(elem, &sb, nil); err != nil {
+ t.Fatal(err)
+ }
wantHtml := "first
"
if sb.String() != wantHtml {
t.Errorf("got %q, want %q", sb.String(), wantHtml)
diff --git a/uiimg.go b/uiimg.go
index 9cded64..bdc74d6 100644
--- a/uiimg.go
+++ b/uiimg.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
"strconv"
)
@@ -19,10 +18,10 @@ func (ui *UiImg) SrcAttr(e *Element) string {
return src
}
-func (ui *UiImg) JawsRender(e *Element, w io.Writer, params []interface{}) {
+func (ui *UiImg) JawsRender(e *Element, w io.Writer, params []interface{}) error {
ui.parseGetter(e, ui.StringSetter)
attrs := append(ui.parseParams(e, params), "src="+ui.SrcAttr(e))
- maybePanic(WriteHtmlInner(w, e.Jid(), "img", "", "", attrs...))
+ return WriteHtmlInner(w, e.Jid(), "img", "", "", attrs...)
}
func (ui *UiImg) JawsUpdate(u *Element) {
@@ -35,6 +34,6 @@ func NewUiImg(g StringSetter) *UiImg {
}
}
-func (rq *Request) Img(imageSrc interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Img(imageSrc interface{}, params ...interface{}) error {
return rq.UI(NewUiImg(makeStringSetter(imageSrc)), params...)
}
diff --git a/uiimg_test.go b/uiimg_test.go
index 59fcbf4..46da5e1 100644
--- a/uiimg_test.go
+++ b/uiimg_test.go
@@ -3,10 +3,10 @@ package jaws
import (
"strconv"
"testing"
- "time"
)
func TestRequest_Img(t *testing.T) {
+ th := newTestHelper(t)
nextJid = 0
rq := newTestRequest()
defer rq.Close()
@@ -14,17 +14,17 @@ func TestRequest_Img(t *testing.T) {
ts := newTestSetter("\"quoted.png\"")
wantHtml := ""
- if gotHtml := string(rq.Img(ts, "hidden")); gotHtml != wantHtml {
+ rq.Img(ts, "hidden")
+ if gotHtml := rq.BodyString(); gotHtml != wantHtml {
t.Errorf("Request.Img() = %q, want %q", gotHtml, wantHtml)
}
- tmr := time.NewTimer(testTimeout)
ts.Set("unquoted.jpg")
rq.Dirty(ts)
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "SAttr\tJid.1\t\"src\\n\\\"unquoted.jpg\\\"\"\n" {
t.Error(strconv.Quote(s))
diff --git a/uiinputbool.go b/uiinputbool.go
index be8110c..43114a9 100644
--- a/uiinputbool.go
+++ b/uiinputbool.go
@@ -12,7 +12,7 @@ type UiInputBool struct {
BoolSetter
}
-func (ui *UiInputBool) renderBoolInput(e *Element, w io.Writer, htmltype string, params ...interface{}) {
+func (ui *UiInputBool) renderBoolInput(e *Element, w io.Writer, htmltype string, params ...interface{}) error {
ui.parseGetter(e, ui.BoolSetter)
attrs := ui.parseParams(e, params)
v := ui.JawsGetBool(e)
@@ -20,7 +20,7 @@ func (ui *UiInputBool) renderBoolInput(e *Element, w io.Writer, htmltype string,
if v {
attrs = append(attrs, "checked")
}
- maybePanic(WriteHtmlInput(w, e.Jid(), htmltype, "", attrs...))
+ return WriteHtmlInput(w, e.Jid(), htmltype, "", attrs...)
}
func (ui *UiInputBool) JawsUpdate(e *Element) {
diff --git a/uiinputdate.go b/uiinputdate.go
index 4b6ddcd..86bc687 100644
--- a/uiinputdate.go
+++ b/uiinputdate.go
@@ -16,11 +16,11 @@ func (ui *UiInputDate) str() string {
return ui.Last.Load().(time.Time).Format(ISO8601)
}
-func (ui *UiInputDate) renderDateInput(e *Element, w io.Writer, jid Jid, htmltype string, params ...interface{}) {
+func (ui *UiInputDate) renderDateInput(e *Element, w io.Writer, jid Jid, htmltype string, params ...interface{}) error {
ui.parseGetter(e, ui.TimeSetter)
attrs := ui.parseParams(e, params)
ui.Last.Store(ui.JawsGetTime(e))
- maybePanic(WriteHtmlInput(w, e.Jid(), htmltype, ui.str(), attrs...))
+ return WriteHtmlInput(w, e.Jid(), htmltype, ui.str(), attrs...)
}
func (ui *UiInputDate) JawsUpdate(e *Element) {
diff --git a/uiinputfloat.go b/uiinputfloat.go
index d596c2f..a3d3aee 100644
--- a/uiinputfloat.go
+++ b/uiinputfloat.go
@@ -16,11 +16,11 @@ func (ui *UiInputFloat) str() string {
return strconv.FormatFloat(ui.Last.Load().(float64), 'f', -1, 64)
}
-func (ui *UiInputFloat) renderFloatInput(e *Element, w io.Writer, htmltype string, params ...interface{}) {
+func (ui *UiInputFloat) renderFloatInput(e *Element, w io.Writer, htmltype string, params ...interface{}) error {
ui.parseGetter(e, ui.FloatSetter)
attrs := ui.parseParams(e, params)
ui.Last.Store(ui.JawsGetFloat(e))
- maybePanic(WriteHtmlInput(w, e.Jid(), htmltype, ui.str(), attrs...))
+ return WriteHtmlInput(w, e.Jid(), htmltype, ui.str(), attrs...)
}
func (ui *UiInputFloat) JawsUpdate(e *Element) {
diff --git a/uiinputtext.go b/uiinputtext.go
index d0f085e..3f13de7 100644
--- a/uiinputtext.go
+++ b/uiinputtext.go
@@ -11,12 +11,12 @@ type UiInputText struct {
StringSetter
}
-func (ui *UiInputText) renderStringInput(e *Element, w io.Writer, htmltype string, params ...interface{}) {
+func (ui *UiInputText) renderStringInput(e *Element, w io.Writer, htmltype string, params ...interface{}) error {
ui.parseGetter(e, ui.StringSetter)
attrs := ui.parseParams(e, params)
v := ui.JawsGetString(e)
ui.Last.Store(v)
- maybePanic(WriteHtmlInput(w, e.Jid(), htmltype, v, attrs...))
+ return WriteHtmlInput(w, e.Jid(), htmltype, v, attrs...)
}
func (ui *UiInputText) JawsUpdate(e *Element) {
diff --git a/uilabel.go b/uilabel.go
index feae95f..2577f3f 100644
--- a/uilabel.go
+++ b/uilabel.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -9,8 +8,8 @@ type UiLabel struct {
UiHtmlInner
}
-func (ui *UiLabel) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderInner(e, w, "label", "", params)
+func (ui *UiLabel) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderInner(e, w, "label", "", params)
}
func NewUiLabel(innerHtml HtmlGetter) *UiLabel {
@@ -21,6 +20,6 @@ func NewUiLabel(innerHtml HtmlGetter) *UiLabel {
}
}
-func (rq *Request) Label(innerHtml interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Label(innerHtml interface{}, params ...interface{}) error {
return rq.UI(NewUiLabel(makeHtmlGetter(innerHtml)), params...)
}
diff --git a/uilabel_test.go b/uilabel_test.go
index 252a74d..0bc0fa8 100644
--- a/uilabel_test.go
+++ b/uilabel_test.go
@@ -9,7 +9,8 @@ func TestRequest_Label(t *testing.T) {
rq := newTestRequest()
defer rq.Close()
want := ``
- if got := string(rq.Label("inner")); got != want {
+ rq.Label("inner")
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Label() = %q, want %q", got, want)
}
}
diff --git a/uili.go b/uili.go
index 1ec7a38..7800e7d 100644
--- a/uili.go
+++ b/uili.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -9,8 +8,8 @@ type UiLi struct {
UiHtmlInner
}
-func (ui *UiLi) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderInner(e, w, "li", "", params)
+func (ui *UiLi) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderInner(e, w, "li", "", params)
}
func NewUiLi(innerHtml HtmlGetter) *UiLi {
@@ -21,6 +20,6 @@ func NewUiLi(innerHtml HtmlGetter) *UiLi {
}
}
-func (rq *Request) Li(innerHtml interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Li(innerHtml interface{}, params ...interface{}) error {
return rq.UI(NewUiLi(makeHtmlGetter(innerHtml)), params...)
}
diff --git a/uili_test.go b/uili_test.go
index 24f35e4..0c1c675 100644
--- a/uili_test.go
+++ b/uili_test.go
@@ -9,7 +9,8 @@ func TestRequest_Li(t *testing.T) {
rq := newTestRequest()
defer rq.Close()
want := `inner`
- if got := string(rq.Li("inner")); got != want {
+ rq.Li("inner")
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Li() = %q, want %q", got, want)
}
}
diff --git a/uinumber.go b/uinumber.go
index 46c4351..3aaffad 100644
--- a/uinumber.go
+++ b/uinumber.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -9,8 +8,8 @@ type UiNumber struct {
UiInputFloat
}
-func (ui *UiNumber) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderFloatInput(e, w, "number", params...)
+func (ui *UiNumber) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderFloatInput(e, w, "number", params...)
}
func NewUiNumber(g FloatSetter) *UiNumber {
@@ -21,6 +20,6 @@ func NewUiNumber(g FloatSetter) *UiNumber {
}
}
-func (rq *Request) Number(value interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Number(value interface{}, params ...interface{}) error {
return rq.UI(NewUiNumber(makeFloatSetter(value)), params...)
}
diff --git a/uinumber_test.go b/uinumber_test.go
index c9a482f..3d3b1ff 100644
--- a/uinumber_test.go
+++ b/uinumber_test.go
@@ -4,29 +4,28 @@ import (
"errors"
"fmt"
"testing"
- "time"
"github.com/linkdata/jaws/what"
)
func TestRequest_Number(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
nextJid = 0
rq := newTestRequest()
defer rq.Close()
ts := newTestSetter(float64(1.2))
want := fmt.Sprintf(``, ts.Get())
- if got := string(rq.Number(ts)); got != want {
+ rq.Number(ts)
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Number() = %q, want %q", got, want)
}
val := float64(2.3)
rq.inCh <- wsMsg{Data: fmt.Sprint(val), Jid: 1, What: what.Input}
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case <-ts.setCalled:
}
if ts.Get() != val {
@@ -42,8 +41,8 @@ func TestRequest_Number(t *testing.T) {
ts.Set(val)
rq.Dirty(ts)
select {
- case <-tmr.C:
- t.Error("timeout waiting for Value")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != fmt.Sprintf("Value\tJid.1\t\"%v\"\n", val) {
t.Error("wrong Value")
@@ -58,8 +57,8 @@ func TestRequest_Number(t *testing.T) {
rq.inCh <- wsMsg{Data: "omg", Jid: 1, What: what.Input}
select {
- case <-tmr.C:
- t.Error("timeout waiting for Alert")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "Alert\t\t\"danger\\nstrconv.ParseFloat: parsing "omg": invalid syntax\"\n" {
t.Errorf("wrong Alert: %q", s)
@@ -69,8 +68,8 @@ func TestRequest_Number(t *testing.T) {
ts.err = errors.New("meh")
rq.inCh <- wsMsg{Data: fmt.Sprint(val), Jid: 1, What: what.Input}
select {
- case <-tmr.C:
- t.Error("timeout waiting for Alert")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "Alert\t\t\"danger\\nmeh\"\n" {
t.Errorf("wrong Alert: %q", s)
diff --git a/uioption.go b/uioption.go
index 9921b47..d11bced 100644
--- a/uioption.go
+++ b/uioption.go
@@ -7,14 +7,14 @@ import (
type UiOption struct{ *NamedBool }
-func (ui UiOption) JawsRender(e *Element, w io.Writer, params []interface{}) {
+func (ui UiOption) JawsRender(e *Element, w io.Writer, params []interface{}) error {
e.Tag(ui.NamedBool)
attrs := parseParams(e, params)
attrs = append(attrs, `value="`+html.EscapeString(ui.JawsGetString(e))+`"`)
if ui.Checked() {
attrs = append(attrs, "selected")
}
- maybePanic(WriteHtmlInner(w, e.Jid(), "option", "", ui.JawsGetHtml(e), attrs...))
+ return WriteHtmlInner(w, e.Jid(), "option", "", ui.JawsGetHtml(e), attrs...)
}
func (ui UiOption) JawsUpdate(e *Element) {
diff --git a/uioption_test.go b/uioption_test.go
index a0bc792..4f68351 100644
--- a/uioption_test.go
+++ b/uioption_test.go
@@ -3,12 +3,10 @@ package jaws
import (
"strings"
"testing"
- "time"
)
func TestUiOption(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
nextJid = 0
rq := newTestRequest()
defer rq.Close()
@@ -19,7 +17,9 @@ func TestUiOption(t *testing.T) {
ui := UiOption{nb}
elem := rq.NewElement(ui)
var sb strings.Builder
- ui.JawsRender(elem, &sb, []any{"hidden"})
+ if err := ui.JawsRender(elem, &sb, []any{"hidden"}); err != nil {
+ t.Fatal(err)
+ }
wantHtml := ""
if gotHtml := sb.String(); gotHtml != wantHtml {
t.Errorf("got %q, want %q", gotHtml, wantHtml)
@@ -28,8 +28,8 @@ func TestUiOption(t *testing.T) {
nb.Set(false)
rq.Dirty(nb)
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "RAttr\tJid.1\t\"selected\"\n" {
t.Errorf("%q", s)
@@ -39,8 +39,8 @@ func TestUiOption(t *testing.T) {
nb.Set(true)
rq.Dirty(nb)
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "SAttr\tJid.1\t\"selected\\n\"\n" {
t.Errorf("%q", s)
diff --git a/uipassword.go b/uipassword.go
index c7ce90d..9a164a3 100644
--- a/uipassword.go
+++ b/uipassword.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -9,8 +8,8 @@ type UiPassword struct {
UiInputText
}
-func (ui *UiPassword) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderStringInput(e, w, "password", params...)
+func (ui *UiPassword) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderStringInput(e, w, "password", params...)
}
func NewUiPassword(g StringSetter) *UiPassword {
@@ -21,6 +20,6 @@ func NewUiPassword(g StringSetter) *UiPassword {
}
}
-func (rq *Request) Password(value interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Password(value interface{}, params ...interface{}) error {
return rq.UI(NewUiPassword(makeStringSetter(value)), params...)
}
diff --git a/uipassword_test.go b/uipassword_test.go
index 18c733a..f8b9ea3 100644
--- a/uipassword_test.go
+++ b/uipassword_test.go
@@ -10,7 +10,8 @@ func TestRequest_Password(t *testing.T) {
defer rq.Close()
ts := newTestSetter("")
want := ``
- if got := string(rq.Password(ts)); got != want {
+ rq.Password(ts)
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Password() = %q, want %q", got, want)
}
}
diff --git a/uiradio.go b/uiradio.go
index f144059..69a034a 100644
--- a/uiradio.go
+++ b/uiradio.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -9,8 +8,8 @@ type UiRadio struct {
UiInputBool
}
-func (ui *UiRadio) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderBoolInput(e, w, "radio", params...)
+func (ui *UiRadio) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderBoolInput(e, w, "radio", params...)
}
func NewUiRadio(vp BoolSetter) *UiRadio {
@@ -21,6 +20,6 @@ func NewUiRadio(vp BoolSetter) *UiRadio {
}
}
-func (rq *Request) Radio(value interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Radio(value interface{}, params ...interface{}) error {
return rq.UI(NewUiRadio(makeBoolSetter(value)), params...)
}
diff --git a/uiradio_test.go b/uiradio_test.go
index ddc6bed..1e0c715 100644
--- a/uiradio_test.go
+++ b/uiradio_test.go
@@ -11,7 +11,8 @@ func TestRequest_Radio(t *testing.T) {
ts := newTestSetter(true)
want := ``
- if got := string(rq.Radio(ts)); got != want {
+ rq.Radio(ts)
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Radio() = %q, want %q", got, want)
}
}
diff --git a/uiradiogroup.go b/uiradiogroup.go
index 561c170..114a735 100644
--- a/uiradiogroup.go
+++ b/uiradiogroup.go
@@ -29,7 +29,7 @@ func (rq *Request) RadioGroup(nba *NamedBoolArray) (rel []RadioElement) {
// Radio renders a HTML input element of type 'radio'.
func (re RadioElement) Radio(params ...interface{}) template.HTML {
var sb strings.Builder
- re.radio.Render(&sb, append(params, re.nameAttr))
+ maybePanic(re.radio.Render(&sb, append(params, re.nameAttr)))
return template.HTML(sb.String()) // #nosec G203
}
@@ -37,6 +37,6 @@ func (re RadioElement) Radio(params ...interface{}) template.HTML {
func (re *RadioElement) Label(params ...interface{}) template.HTML {
var sb strings.Builder
forAttr := string(re.radio.jid.AppendQuote([]byte("for=")))
- re.label.Render(&sb, append(params, forAttr))
+ maybePanic(re.label.Render(&sb, append(params, forAttr)))
return template.HTML(sb.String()) // #nosec G203
}
diff --git a/uirange.go b/uirange.go
index a0fe67f..ca759ce 100644
--- a/uirange.go
+++ b/uirange.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -9,8 +8,8 @@ type UiRange struct {
UiInputFloat
}
-func (ui *UiRange) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderFloatInput(e, w, "range", params...)
+func (ui *UiRange) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderFloatInput(e, w, "range", params...)
}
func NewUiRange(g FloatSetter) *UiRange {
@@ -21,6 +20,6 @@ func NewUiRange(g FloatSetter) *UiRange {
}
}
-func (rq *Request) Range(value interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Range(value interface{}, params ...interface{}) error {
return rq.UI(NewUiRange(makeFloatSetter(value)), params...)
}
diff --git a/uirange_test.go b/uirange_test.go
index 2422bd0..d62b54a 100644
--- a/uirange_test.go
+++ b/uirange_test.go
@@ -3,27 +3,26 @@ package jaws
import (
"errors"
"testing"
- "time"
"github.com/linkdata/jaws/what"
)
func TestRequest_Range(t *testing.T) {
+ th := newTestHelper(t)
nextJid = 0
rq := newTestRequest()
defer rq.Close()
ts := newTestSetter(float64(1))
want := ``
- if got := string(rq.Range(ts)); got != want {
+ rq.Range(ts)
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Range() = %q, want %q", got, want)
}
- tmr := time.NewTimer(testTimeout)
rq.inCh <- wsMsg{Data: "2.1", Jid: 1, What: what.Input}
- defer tmr.Stop()
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case <-ts.setCalled:
}
if ts.Get() != 2.1 {
@@ -37,8 +36,8 @@ func TestRequest_Range(t *testing.T) {
ts.Set(2.3)
rq.Dirty(ts)
select {
- case <-tmr.C:
- t.Error("timeout waiting for Value")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "Value\tJid.1\t\"2.3\"\n" {
t.Error(s)
@@ -54,8 +53,8 @@ func TestRequest_Range(t *testing.T) {
ts.err = errors.New("meh")
rq.inCh <- wsMsg{Data: "3.4", Jid: 1, What: what.Input}
select {
- case <-tmr.C:
- t.Error("timeout waiting for Alert")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "Alert\t\t\"danger\\nmeh\"\n" {
t.Errorf("wrong Alert: %q", s)
diff --git a/uiselect.go b/uiselect.go
index 08cc4de..dfeb0b6 100644
--- a/uiselect.go
+++ b/uiselect.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
"github.com/linkdata/jaws/what"
@@ -19,8 +18,8 @@ func NewUiSelect(sh SelectHandler) *UiSelect {
}
}
-func (ui *UiSelect) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderContainer(e, w, "select", params)
+func (ui *UiSelect) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderContainer(e, w, "select", params)
}
func (ui *UiSelect) JawsUpdate(e *Element) {
@@ -39,6 +38,6 @@ func (ui *UiSelect) JawsEvent(e *Element, wht what.What, val string) (err error)
return ui.UiHtml.JawsEvent(e, wht, val)
}
-func (rq *Request) Select(sh SelectHandler, params ...interface{}) template.HTML {
+func (rq RequestWriter) Select(sh SelectHandler, params ...interface{}) error {
return rq.UI(NewUiSelect(sh), params...)
}
diff --git a/uiselect_test.go b/uiselect_test.go
index 76e38dd..b3caf9d 100644
--- a/uiselect_test.go
+++ b/uiselect_test.go
@@ -3,7 +3,6 @@ package jaws
import (
"errors"
"testing"
- "time"
"github.com/linkdata/deadlock"
"github.com/linkdata/jaws/what"
@@ -31,6 +30,7 @@ func (ts *testNamedBoolArray) JawsSetString(e *Element, val string) (err error)
}
func TestRequest_Select(t *testing.T) {
+ th := newTestHelper(t)
nextJid = 0
rq := newTestRequest()
defer rq.Close()
@@ -44,7 +44,8 @@ func TestRequest_Select(t *testing.T) {
a.Set("1", true)
wantHtml := ""
- if gotHtml := string(rq.Select(a, "disabled")); gotHtml != wantHtml {
+ rq.Select(a, "disabled")
+ if gotHtml := rq.BodyString(); gotHtml != wantHtml {
t.Errorf("Request.Select() = %q, want %q", gotHtml, wantHtml)
}
@@ -55,12 +56,10 @@ func TestRequest_Select(t *testing.T) {
t.Error("2 is checked")
}
- tmr := time.NewTimer(testTimeout)
rq.inCh <- wsMsg{Data: "2", Jid: 1, What: what.Input}
- defer tmr.Stop()
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case <-a.setCalled:
}
@@ -81,8 +80,8 @@ func TestRequest_Select(t *testing.T) {
rq.Dirty(a)
select {
- case <-tmr.C:
- t.Error("timeout waiting for Value")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "Value\tJid.1\t\"\"\n" {
t.Error("wrong Value")
@@ -99,8 +98,8 @@ func TestRequest_Select(t *testing.T) {
a.err = errors.New("meh")
rq.inCh <- wsMsg{Data: "1", Jid: 1, What: what.Input}
select {
- case <-tmr.C:
- t.Error("timeout waiting for Alert")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "Alert\t\t\"danger\\nmeh\"\n" {
t.Errorf("wrong Alert: %q", s)
diff --git a/uispan.go b/uispan.go
index 093c6c4..538bce6 100644
--- a/uispan.go
+++ b/uispan.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -9,8 +8,8 @@ type UiSpan struct {
UiHtmlInner
}
-func (ui *UiSpan) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderInner(e, w, "span", "", params)
+func (ui *UiSpan) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderInner(e, w, "span", "", params)
}
func NewUiSpan(innerHtml HtmlGetter) *UiSpan {
@@ -21,6 +20,6 @@ func NewUiSpan(innerHtml HtmlGetter) *UiSpan {
}
}
-func (rq *Request) Span(innerHtml interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Span(innerHtml interface{}, params ...interface{}) error {
return rq.UI(NewUiSpan(makeHtmlGetter(innerHtml)), params...)
}
diff --git a/uispan_test.go b/uispan_test.go
index 64733e6..41d8ca6 100644
--- a/uispan_test.go
+++ b/uispan_test.go
@@ -9,7 +9,8 @@ func TestRequest_Span(t *testing.T) {
rq := newTestRequest()
defer rq.Close()
want := `inner`
- if got := string(rq.Span("inner")); got != want {
+ rq.Span("inner")
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Span() = %q, want %q", got, want)
}
}
diff --git a/uitbody.go b/uitbody.go
index 8631682..3953be7 100644
--- a/uitbody.go
+++ b/uitbody.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -17,10 +16,10 @@ func NewUiTbody(c Container) *UiTbody {
}
}
-func (ui *UiTbody) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderContainer(e, w, "tbody", params)
+func (ui *UiTbody) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderContainer(e, w, "tbody", params)
}
-func (rq *Request) Tbody(c Container, params ...interface{}) template.HTML {
+func (rq RequestWriter) Tbody(c Container, params ...interface{}) error {
return rq.UI(NewUiTbody(c), params...)
}
diff --git a/uitbody_test.go b/uitbody_test.go
index c94a73e..cbd2db7 100644
--- a/uitbody_test.go
+++ b/uitbody_test.go
@@ -9,7 +9,8 @@ func TestRequest_Tbody(t *testing.T) {
rq := newTestRequest()
defer rq.Close()
want := ``
- if got := string(rq.Tbody(&testContainer{})); got != want {
+ rq.Tbody(&testContainer{})
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Span() = %q, want %q", got, want)
}
}
diff --git a/uitd.go b/uitd.go
index b815566..69c6a95 100644
--- a/uitd.go
+++ b/uitd.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -9,8 +8,8 @@ type UiTd struct {
UiHtmlInner
}
-func (ui *UiTd) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderInner(e, w, "td", "", params)
+func (ui *UiTd) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderInner(e, w, "td", "", params)
}
func NewUiTd(innerHtml HtmlGetter) *UiTd {
@@ -21,6 +20,6 @@ func NewUiTd(innerHtml HtmlGetter) *UiTd {
}
}
-func (rq *Request) Td(innerHtml interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Td(innerHtml interface{}, params ...interface{}) error {
return rq.UI(NewUiTd(makeHtmlGetter(innerHtml)), params...)
}
diff --git a/uitd_test.go b/uitd_test.go
index 8daa651..0bc350b 100644
--- a/uitd_test.go
+++ b/uitd_test.go
@@ -9,7 +9,8 @@ func TestRequest_Td(t *testing.T) {
rq := newTestRequest()
defer rq.Close()
want := `inner | `
- if got := string(rq.Td("inner")); got != want {
+ rq.Td("inner")
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Td() = %q, want %q", got, want)
}
}
diff --git a/uitemplate.go b/uitemplate.go
index c887b0a..a07e2fe 100644
--- a/uitemplate.go
+++ b/uitemplate.go
@@ -1,9 +1,5 @@
package jaws
-import (
- "html/template"
-)
-
type UiTemplate struct {
Template
}
@@ -17,6 +13,6 @@ func NewUiTemplate(t Template) UiTemplate {
//
// The templ argument can either be a string, in which case Jaws.Template.Lookup() will
// be used to resolve it. Or it can be a *template.Template directly.
-func (rq *Request) Template(templ, dot interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Template(templ, dot interface{}, params ...interface{}) error {
return rq.UI(NewUiTemplate(rq.MakeTemplate(templ, dot)), params...)
}
diff --git a/uitemplate_test.go b/uitemplate_test.go
index 6a78984..1af920c 100644
--- a/uitemplate_test.go
+++ b/uitemplate_test.go
@@ -11,7 +11,7 @@ import (
)
func TestRequest_Template(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
type args struct {
templ interface{}
dot interface{}
@@ -64,13 +64,14 @@ func TestRequest_Template(t *testing.T) {
t.Fail()
}()
}
- got := rq.Template(tt.args.templ, tt.args.dot, tt.args.params...)
+ rq.Template(tt.args.templ, tt.args.dot, tt.args.params...)
+ got := rq.BodyHtml()
is.Equal(len(rq.elems), 1)
elem := rq.elems[0]
if tt.errtxt != "" {
t.Fail()
}
- gotTags := elem.TagsOf(elem)
+ gotTags := elem.Request().TagsOf(elem)
is.Equal(len(tt.tags), len(gotTags))
for _, tag := range tt.tags {
is.True(elem.HasTag(tag))
@@ -96,11 +97,11 @@ func (td *templateDot) JawsClick(e *Element, name string) error {
var _ ClickHandler = &templateDot{}
func TestRequest_Template_Event(t *testing.T) {
- is := testHelper{t}
+ is := newTestHelper(t)
rq := newTestRequest()
defer rq.Close()
dot := &templateDot{clickedCh: make(chan struct{})}
- _ = rq.Template("testtemplate", dot)
+ rq.Template("testtemplate", dot)
rq.jw.Broadcast(Message{
Dest: dot,
What: what.Update,
diff --git a/uitext.go b/uitext.go
index e654b33..54ac17c 100644
--- a/uitext.go
+++ b/uitext.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -9,8 +8,8 @@ type UiText struct {
UiInputText
}
-func (ui *UiText) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderStringInput(e, w, "text", params...)
+func (ui *UiText) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderStringInput(e, w, "text", params...)
}
func NewUiText(vp StringSetter) (ui *UiText) {
@@ -21,6 +20,6 @@ func NewUiText(vp StringSetter) (ui *UiText) {
}
}
-func (rq *Request) Text(value interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Text(value interface{}, params ...interface{}) error {
return rq.UI(NewUiText(makeStringSetter(value)), params...)
}
diff --git a/uitext_test.go b/uitext_test.go
index 629a2f4..ce886e7 100644
--- a/uitext_test.go
+++ b/uitext_test.go
@@ -3,27 +3,26 @@ package jaws
import (
"errors"
"testing"
- "time"
"github.com/linkdata/jaws/what"
)
func TestRequest_Text(t *testing.T) {
+ th := newTestHelper(t)
nextJid = 0
rq := newTestRequest()
defer rq.Close()
ss := newTestSetter("foo")
want := ``
- if got := string(rq.Text(ss)); got != want {
+ rq.Text(ss)
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Text() = %q, want %q", got, want)
}
- tmr := time.NewTimer(testTimeout)
rq.inCh <- wsMsg{Data: "bar", Jid: 1, What: what.Input}
- defer tmr.Stop()
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case <-ss.setCalled:
}
if ss.Get() != "bar" {
@@ -37,8 +36,8 @@ func TestRequest_Text(t *testing.T) {
ss.Set("quux")
rq.Dirty(ss)
select {
- case <-tmr.C:
- t.Error("timeout waiting for Value")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "Value\tJid.1\t\"quux\"\n" {
t.Error("wrong Value")
@@ -54,8 +53,8 @@ func TestRequest_Text(t *testing.T) {
ss.err = errors.New("meh")
rq.inCh <- wsMsg{Data: "omg", Jid: 1, What: what.Input}
select {
- case <-tmr.C:
- t.Error("timeout waiting for Alert")
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "Alert\t\t\"danger\\nmeh\"\n" {
t.Errorf("wrong Alert: %q", s)
diff --git a/uitextarea.go b/uitextarea.go
index d8cf02a..03538ea 100644
--- a/uitextarea.go
+++ b/uitextarea.go
@@ -9,10 +9,10 @@ type UiTextarea struct {
UiInputText
}
-func (ui *UiTextarea) JawsRender(e *Element, w io.Writer, params []interface{}) {
+func (ui *UiTextarea) JawsRender(e *Element, w io.Writer, params []interface{}) error {
ui.parseGetter(e, ui.StringSetter)
attrs := ui.parseParams(e, params)
- maybePanic(WriteHtmlInner(w, e.Jid(), "textarea", "", template.HTML(ui.JawsGetString(e)), attrs...)) // #nosec G203
+ return WriteHtmlInner(w, e.Jid(), "textarea", "", template.HTML(ui.JawsGetString(e)), attrs...) // #nosec G203
}
func (ui *UiTextarea) JawsUpdate(e *Element) {
@@ -27,6 +27,6 @@ func NewUiTextarea(g StringSetter) (ui *UiTextarea) {
}
}
-func (rq *Request) Textarea(value interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Textarea(value interface{}, params ...interface{}) error {
return rq.UI(NewUiTextarea(makeStringSetter(value)), params...)
}
diff --git a/uitextarea_test.go b/uitextarea_test.go
index 90254d3..f2b0af1 100644
--- a/uitextarea_test.go
+++ b/uitextarea_test.go
@@ -2,27 +2,26 @@ package jaws
import (
"testing"
- "time"
"github.com/linkdata/jaws/what"
)
func TestRequest_Textarea(t *testing.T) {
+ th := newTestHelper(t)
nextJid = 0
rq := newTestRequest()
defer rq.Close()
ss := newTestSetter("foo")
want := ``
- if got := string(rq.Textarea(ss)); got != want {
+ rq.Textarea(ss)
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Textarea() = %q, want %q", got, want)
}
rq.inCh <- wsMsg{Data: "bar", Jid: 1, What: what.Input}
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
select {
- case <-tmr.C:
- t.Fail()
+ case <-th.C:
+ th.Timeout()
case <-ss.setCalled:
}
if ss.Get() != "bar" {
@@ -36,8 +35,8 @@ func TestRequest_Textarea(t *testing.T) {
ss.Set("quux")
rq.Dirty(ss)
select {
- case <-tmr.C:
- t.Fail()
+ case <-th.C:
+ th.Timeout()
case s := <-rq.outCh:
if s != "Inner\tJid.1\t\"quux\"\n" {
t.Fail()
diff --git a/uitr.go b/uitr.go
index 86d78e1..e422903 100644
--- a/uitr.go
+++ b/uitr.go
@@ -1,7 +1,6 @@
package jaws
import (
- "html/template"
"io"
)
@@ -9,8 +8,8 @@ type UiTr struct {
UiHtmlInner
}
-func (ui *UiTr) JawsRender(e *Element, w io.Writer, params []interface{}) {
- ui.renderInner(e, w, "tr", "", params)
+func (ui *UiTr) JawsRender(e *Element, w io.Writer, params []interface{}) error {
+ return ui.renderInner(e, w, "tr", "", params)
}
func NewUiTr(innerHtml HtmlGetter) *UiTr {
@@ -21,6 +20,6 @@ func NewUiTr(innerHtml HtmlGetter) *UiTr {
}
}
-func (rq *Request) Tr(innerHtml interface{}, params ...interface{}) template.HTML {
+func (rq RequestWriter) Tr(innerHtml interface{}, params ...interface{}) error {
return rq.UI(NewUiTr(makeHtmlGetter(innerHtml)), params...)
}
diff --git a/uitr_test.go b/uitr_test.go
index 7fdfcf6..3e87fad 100644
--- a/uitr_test.go
+++ b/uitr_test.go
@@ -9,7 +9,8 @@ func TestRequest_Tr(t *testing.T) {
rq := newTestRequest()
defer rq.Close()
want := `inner
`
- if got := string(rq.Tr("inner")); got != want {
+ rq.Tr("inner")
+ if got := rq.BodyString(); got != want {
t.Errorf("Request.Tr() = %q, want %q", got, want)
}
}
diff --git a/uiwrapcontainer.go b/uiwrapcontainer.go
index 9ff04d5..7842d3c 100644
--- a/uiwrapcontainer.go
+++ b/uiwrapcontainer.go
@@ -16,7 +16,7 @@ type uiWrapContainer struct {
contents []*Element
}
-func (ui *uiWrapContainer) renderContainer(e *Element, w io.Writer, outerhtmltag string, params []interface{}) {
+func (ui *uiWrapContainer) renderContainer(e *Element, w io.Writer, outerhtmltag string, params []interface{}) error {
ui.parseGetter(e, ui.Container)
attrs := ui.parseParams(e, params)
b := e.jid.AppendStartTagAttr(nil, outerhtmltag)
@@ -27,18 +27,22 @@ func (ui *uiWrapContainer) renderContainer(e *Element, w io.Writer, outerhtmltag
b = append(b, '>')
_, err := w.Write(b)
if err == nil {
- for _, cui := range ui.Container.JawsContains(e.Request) {
- elem := e.Request.NewElement(cui)
- ui.contents = append(ui.contents, elem)
- elem.Render(w, nil)
+ for _, cui := range ui.Container.JawsContains(e.Request()) {
+ if err == nil {
+ elem := e.Request().NewElement(cui)
+ ui.contents = append(ui.contents, elem)
+ err = elem.Render(w, nil)
+ }
}
b = b[:0]
b = append(b, ""...)
b = append(b, outerhtmltag...)
b = append(b, '>')
- _, err = w.Write(b)
+ if err == nil {
+ _, err = w.Write(b)
+ }
}
- maybePanic(err)
+ return err
}
func (ui *uiWrapContainer) JawsUpdate(e *Element) {
@@ -47,7 +51,7 @@ func (ui *uiWrapContainer) JawsUpdate(e *Element) {
oldMap := make(map[UI]*Element)
newMap := make(map[UI]struct{})
- newContents := ui.Container.JawsContains(e.Request)
+ newContents := ui.Container.JawsContains(e.Request())
for _, t := range newContents {
newMap[t] = struct{}{}
}
@@ -65,7 +69,7 @@ func (ui *uiWrapContainer) JawsUpdate(e *Element) {
for _, cui := range newContents {
var elem *Element
if elem = oldMap[cui]; elem == nil {
- elem = e.Request.NewElement(cui)
+ elem = e.Request().NewElement(cui)
toAppend = append(toAppend, elem)
}
ui.contents = append(ui.contents, elem)
@@ -75,12 +79,12 @@ func (ui *uiWrapContainer) JawsUpdate(e *Element) {
for _, elem := range toRemove {
e.Remove(elem.jid.String())
- e.deleteElement(elem)
+ e.Request().deleteElement(elem)
}
for _, elem := range toAppend {
var sb strings.Builder
- elem.ui.JawsRender(elem, &sb, nil)
+ maybePanic(elem.ui.JawsRender(elem, &sb, nil))
e.Append(template.HTML(sb.String())) // #nosec G203
}
diff --git a/with.go b/with.go
index 39c92a9..c7681f7 100644
--- a/with.go
+++ b/with.go
@@ -6,6 +6,7 @@ import (
type With struct {
*Element
+ RequestWriter
Dot interface{}
Attrs template.HTMLAttr
}
diff --git a/ws.go b/ws.go
index ac4c59b..e0d821d 100644
--- a/ws.go
+++ b/ws.go
@@ -7,31 +7,46 @@ import (
"nhooyr.io/websocket"
)
+func (rq *Request) startServe() (ok bool) {
+ rq.mu.Lock()
+ if ok = !rq.running && rq.claimed; ok {
+ rq.running = true
+ }
+ rq.mu.Unlock()
+ return
+}
+
+func (rq *Request) stopServe() {
+ rq.mu.Lock()
+ rq.running = false
+ rq.mu.Unlock()
+}
+
// ServeHTTP implements http.HanderFunc.
//
-// Assumes UseRequest() have been successfully called for the Request.
+// Requires UseRequest() have been successfully called for the Request.
func (rq *Request) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- ws, err := websocket.Accept(w, r, nil)
- if err == nil {
- if err = rq.onConnect(); err == nil {
- incomingMsgCh := make(chan wsMsg)
- broadcastMsgCh := rq.Jaws.subscribe(rq, 1)
- outboundCh := make(chan string, cap(broadcastMsgCh))
- go wsReader(rq.ctx, rq.cancelFn, rq.Jaws.Done(), incomingMsgCh, ws) // closes incomingMsgCh
- go wsWriter(rq.ctx, rq.cancelFn, rq.Jaws.Done(), outboundCh, ws) // calls ws.Close()
- rq.process(broadcastMsgCh, incomingMsgCh, outboundCh) // unsubscribes broadcastMsgCh, closes outboundMsgCh
- } else {
- defer ws.Close(websocket.StatusNormalClosure, err.Error())
- var msg wsMsg
- msg.FillAlert(rq.Jaws.Log(err))
- _ = ws.Write(r.Context(), websocket.MessageText, msg.Append(nil))
+ if rq.startServe() {
+ defer rq.stopServe()
+ ws, err := websocket.Accept(w, r, nil)
+ if err == nil {
+ if err = rq.onConnect(); err == nil {
+ incomingMsgCh := make(chan wsMsg)
+ broadcastMsgCh := rq.Jaws.subscribe(rq, 4+len(rq.elems)*4)
+ outboundCh := make(chan string, cap(broadcastMsgCh))
+ go wsReader(rq.ctx, rq.cancelFn, rq.Jaws.Done(), incomingMsgCh, ws) // closes incomingMsgCh
+ go wsWriter(rq.ctx, rq.cancelFn, rq.Jaws.Done(), outboundCh, ws) // calls ws.Close()
+ rq.process(broadcastMsgCh, incomingMsgCh, outboundCh) // unsubscribes broadcastMsgCh, closes outboundMsgCh
+ } else {
+ defer ws.Close(websocket.StatusNormalClosure, err.Error())
+ var msg wsMsg
+ msg.FillAlert(rq.Jaws.Log(err))
+ _ = ws.Write(r.Context(), websocket.MessageText, msg.Append(nil))
+ }
}
- }
- if err != nil {
rq.cancel(err)
- _ = rq.Jaws.Log(err)
+ rq.Jaws.recycle(rq)
}
- rq.recycle()
}
// wsReader reads websocket text messages, parses them and sends them on incomingMsgCh.
diff --git a/ws_test.go b/ws_test.go
index c888939..e0576c9 100644
--- a/ws_test.go
+++ b/ws_test.go
@@ -21,6 +21,7 @@ type testServer struct {
ctx context.Context
cancel context.CancelFunc
hr *http.Request
+ rr *httptest.ResponseRecorder
rq *Request
sess *Session
srv *httptest.Server
@@ -30,8 +31,9 @@ type testServer struct {
func newTestServer() (ts *testServer) {
jw := New()
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
+ rr := httptest.NewRecorder()
hr := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
- sess := jw.NewSession(nil, hr)
+ sess := jw.NewSession(rr, hr)
rq := jw.NewRequest(hr)
if rq != jw.UseRequest(rq.JawsKey, hr) {
panic("UseRequest failed")
@@ -41,6 +43,7 @@ func newTestServer() (ts *testServer) {
ctx: ctx,
cancel: cancel,
hr: hr,
+ rr: rr,
rq: rq,
sess: sess,
connectedCh: make(chan struct{}),
@@ -85,10 +88,11 @@ func (ts *testServer) Close() {
func TestWS_UpgradeRequired(t *testing.T) {
jw := New()
defer jw.Close()
- rq := jw.NewRequest(nil)
-
- req := httptest.NewRequest("", "/jaws/"+rq.JawsKeyString(), nil)
w := httptest.NewRecorder()
+ hr := httptest.NewRequest("", "/", nil)
+ rq := jw.NewRequest(hr)
+ jw.UseRequest(rq.JawsKey, hr)
+ req := httptest.NewRequest("", "/jaws/"+rq.JawsKeyString(), nil)
rq.ServeHTTP(w, req)
if w.Code != http.StatusUpgradeRequired {
t.Error(w.Code)
@@ -124,8 +128,7 @@ func TestWS_ConnectFnFails(t *testing.T) {
}
func TestWS_NormalExchange(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
ts := newTestServer()
defer ts.Close()
@@ -156,8 +159,8 @@ func TestWS_NormalExchange(t *testing.T) {
t.Fatal(err)
}
select {
- case <-tmr.C:
- t.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
case <-gotCallCh:
}
@@ -176,8 +179,7 @@ func TestWS_NormalExchange(t *testing.T) {
}
func TestReader_RespectsContextDone(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
ts := newTestServer()
defer ts.Close()
@@ -206,15 +208,14 @@ func TestReader_RespectsContextDone(t *testing.T) {
ts.cancel()
select {
- case <-tmr.C:
- t.Error("did not unblock")
+ case <-th.C:
+ th.Timeout()
case <-doneCh:
}
}
func TestReader_RespectsJawsDone(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
ts := newTestServer()
defer ts.Close()
@@ -238,15 +239,14 @@ func TestReader_RespectsJawsDone(t *testing.T) {
}
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case <-doneCh:
}
}
func TestWriter_SendsThePayload(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
ts := newTestServer()
defer ts.Close()
@@ -268,14 +268,14 @@ func TestWriter_SendsThePayload(t *testing.T) {
msg := wsMsg{Jid: Jid(1234)}
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case outCh <- msg.Format():
}
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case <-doneCh:
}
@@ -290,15 +290,14 @@ func TestWriter_SendsThePayload(t *testing.T) {
}
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case <-client.CloseRead(ts.ctx).Done():
}
}
func TestWriter_RespectsContext(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
ts := newTestServer()
defer ts.Close()
@@ -316,16 +315,15 @@ func TestWriter_RespectsContext(t *testing.T) {
ts.cancel()
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case <-doneCh:
return
}
}
func TestWriter_RespectsJawsDone(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
ts := newTestServer()
defer ts.Close()
@@ -343,15 +341,14 @@ func TestWriter_RespectsJawsDone(t *testing.T) {
ts.jw.Close()
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case <-doneCh:
}
}
func TestWriter_RespectsOutboundClosed(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
ts := newTestServer()
defer ts.Close()
@@ -368,8 +365,8 @@ func TestWriter_RespectsOutboundClosed(t *testing.T) {
close(outCh)
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case <-doneCh:
}
@@ -379,8 +376,7 @@ func TestWriter_RespectsOutboundClosed(t *testing.T) {
}
func TestWriter_ReportsError(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
ts := newTestServer()
defer ts.Close()
@@ -397,14 +393,14 @@ func TestWriter_ReportsError(t *testing.T) {
msg := wsMsg{Jid: Jid(1234)}
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case outCh <- msg.Format():
}
select {
- case <-tmr.C:
- t.Error("timeout")
+ case <-th.C:
+ th.Timeout()
case <-doneCh:
}
@@ -415,8 +411,7 @@ func TestWriter_ReportsError(t *testing.T) {
}
func TestReader_ReportsError(t *testing.T) {
- tmr := time.NewTimer(testTimeout)
- defer tmr.Stop()
+ th := newTestHelper(t)
ts := newTestServer()
defer ts.Close()
@@ -438,8 +433,8 @@ func TestReader_ReportsError(t *testing.T) {
}
select {
- case <-tmr.C:
- t.Fatal("timeout")
+ case <-th.C:
+ th.Timeout()
case <-doneCh:
}