Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions lib/ui/input_widgets.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ui

import (
"errors"
"html/template"
"io"
"strconv"
Expand Down Expand Up @@ -178,9 +179,10 @@ func (u *InputDate) JawsUpdate(elem *jaws.Element) {

// JawsInput stores a browser-side date input value.
//
// The browser sends a calendar date (YYYY-MM-DD), which [time.Parse] resolves
// to midnight UTC, so the stored [time.Time] drops any time-of-day and
// [time.Location] the previously bound value carried. In a non-UTC deployment
// An empty value maps to the zero [time.Time], which renders as "0001-01-01".
// Non-empty values are calendar dates (YYYY-MM-DD). [time.Parse]
// resolves them to midnight UTC, so the stored [time.Time] drops any time-of-day
// and [time.Location] the previously bound value carried. In a non-UTC deployment
// the stored instant therefore shifts by the zone offset, and because
// [time.Time] inequality includes the location, re-selecting the same date
// still reports a change and broadcasts it. Bind a date whose clock and zone are
Expand All @@ -193,15 +195,22 @@ func (u *InputDate) JawsUpdate(elem *jaws.Element) {
// parse error and leaves the last accepted value in place instead of updating the
// bound value. Keep bound years within 1..9999.
func (u *InputDate) JawsInput(elem *jaws.Element, value string) (err error) {
input := value
if value == "" {
value = "0001-01-01"
}
var v time.Time
// Parse errors are malformed client frames: jaws.js reads elem.value from
// browser date controls. Leave Last as the last accepted value.
if v, err = time.Parse(assets.ISO8601, value); err == nil {
u.Last.Store(u.str(v))
err = u.maybeDirty(elem, u.Setter.JawsSet(elem, v))
u.Last.Store(input)
err = u.Setter.JawsSet(elem, v)
if input == "" && u.tag != nil && errors.Is(err, jaws.ErrValueUnchanged) {
elem.Dirty(elem)
err = nil
return
}
err = u.maybeDirty(elem, err)
}
return
}
69 changes: 69 additions & 0 deletions lib/ui/input_widgets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"html/template"
"strings"
"testing"
"testing/synctest"
"time"

"github.com/linkdata/deadlock"
Expand Down Expand Up @@ -176,6 +177,74 @@ func TestInputDateWidget(t *testing.T) {
date.JawsUpdate(elem)
}

func TestInputDate_ClearReconcilesCanonicalZero(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
jw, err := jaws.New()
if err != nil {
t.Fatal(err)
}
go jw.Serve()
tr := jawstest.NewTestRequest(jw, nil)
<-tr.ReadyCh
defer func() {
tr.Close()
jw.Close()
synctest.Wait()
}()

source := newTestSetter(time.Date(2026, time.August, 15, 0, 0, 0, 0, time.UTC))
originElem, _ := renderUI(t, tr.Request, NewDate(source))
peerElem, _ := renderUI(t, tr.Request, NewDate(source))

waitForUpdate := func() {
synctest.Wait()
time.Sleep(jaws.DefaultUpdateInterval + time.Millisecond)
synctest.Wait()
}
readCorrection := func() (msg wire.WsMsg) {
t.Helper()
select {
case msg = <-tr.OutCh:
if msg.What != what.Value || msg.Data != "0001-01-01" {
t.Fatalf("date correction = %#v, want Value %q", msg, "0001-01-01")
}
default:
t.Fatal("missing date correction")
}
return
}

tr.InCh <- wire.WsMsg{Jid: originElem.Jid(), What: what.Input, Data: ""}
waitForUpdate()
if got := source.Get(); !got.IsZero() {
t.Fatalf("cleared date = %v, want zero time", got)
}
corrections := map[jaws.Jid]bool{
readCorrection().Jid: true,
readCorrection().Jid: true,
}
if len(corrections) != 2 || !corrections[originElem.Jid()] || !corrections[peerElem.Jid()] {
t.Fatalf("corrected elements = %v, want origin and peer", corrections)
}
select {
case msg := <-tr.OutCh:
t.Fatalf("unexpected additional date output: %#v", msg)
default:
}

tr.InCh <- wire.WsMsg{Jid: originElem.Jid(), What: what.Input, Data: ""}
waitForUpdate()
if msg := readCorrection(); msg.Jid != originElem.Jid() {
t.Fatalf("unchanged correction Jid = %v, want %v", msg.Jid, originElem.Jid())
}
select {
case msg := <-tr.OutCh:
t.Fatalf("unexpected additional date output: %#v", msg)
default:
}
})
}

// TestInputDate_BrowserEditNormalizesToMidnightUTC locks in the documented
// date-only behavior (issue #124): the control renders/reads a calendar date, so
// a browser edit resolves through time.Parse to midnight UTC and drops the bound
Expand Down
Loading