Add landing page for kjol, documentation
This commit is contained in:
163
go/webui/forms_async_test.go
Normal file
163
go/webui/forms_async_test.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"kjol/vdom"
|
||||
)
|
||||
|
||||
// ---- input masks ----
|
||||
|
||||
func TestMaskTaxID(t *testing.T) {
|
||||
for _, tc := range []struct{ in, want string }{
|
||||
{"", ""},
|
||||
{"1", "1"},
|
||||
{"12", "12"},
|
||||
{"123", "12-3"},
|
||||
{"123456789", "12-3456789"},
|
||||
{"12-3456789", "12-3456789"}, // idempotent: re-masking its own output
|
||||
{"1234567890123", "12-3456789"}, // nine digits, no more
|
||||
{"ab12cd3456789xy", "12-3456789"},
|
||||
} {
|
||||
if got := MaskTaxID(tc.in); got != tc.want {
|
||||
t.Errorf("MaskTaxID(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaskRate(t *testing.T) {
|
||||
for _, tc := range []struct{ in, want string }{
|
||||
{"", ""},
|
||||
{"5", "5"},
|
||||
{"5.25", "5.25"},
|
||||
{"5.2555", "5.255"}, // three decimals, no more
|
||||
{"5.2.5", "5.25"}, // one decimal point, no more
|
||||
{"007", "7"}, // no leading zeros...
|
||||
{"0", "0"}, // ...but a lone zero is a number
|
||||
{"0.5", "0.5"}, // and so is a leading zero before a point
|
||||
{"$1,2a3.45", "123.45"},
|
||||
{"5.25", "5.25"}, // idempotent
|
||||
} {
|
||||
if got := MaskRate(tc.in); got != tc.want {
|
||||
t.Errorf("MaskRate(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A mask is applied to its own output on every keystroke, so anything that is not
|
||||
// idempotent corrupts the field as you type.
|
||||
func TestMasksAreIdempotent(t *testing.T) {
|
||||
for _, in := range []string{"123456789", "12-345", "1", ""} {
|
||||
once := MaskTaxID(in)
|
||||
if twice := MaskTaxID(once); twice != once {
|
||||
t.Errorf("MaskTaxID is not idempotent: %q -> %q -> %q", in, once, twice)
|
||||
}
|
||||
}
|
||||
for _, in := range []string{"5.255", "0.5", "007", "12"} {
|
||||
once := MaskRate(in)
|
||||
if twice := MaskRate(once); twice != once {
|
||||
t.Errorf("MaskRate is not idempotent: %q -> %q -> %q", in, once, twice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- async combobox ----
|
||||
|
||||
func asyncOpts(labels ...string) []FormSelectOption {
|
||||
out := make([]FormSelectOption, len(labels))
|
||||
for i, l := range labels {
|
||||
out[i] = FormSelectOption{Value: strings.ToLower(l), Label: l}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// The classic bug in a search-as-you-type box: a SLOW answer to an early query lands
|
||||
// after a FAST answer to the query the user actually finished typing, and overwrites it.
|
||||
// The user is then reading results for "ab" while the box says "abcdef".
|
||||
//
|
||||
// Every response carries the query it was for; one that no longer matches is dropped.
|
||||
func TestAsyncComboboxDropsStaleResponses(t *testing.T) {
|
||||
var pending []func([]FormSelectOption) // answers we have not given yet
|
||||
var queries []string
|
||||
|
||||
c := NewAsyncCombobox(AsyncComboboxOptions{
|
||||
MinChars: 2,
|
||||
Search: func(q string, done func([]FormSelectOption)) {
|
||||
queries = append(queries, q)
|
||||
pending = append(pending, done)
|
||||
},
|
||||
})
|
||||
|
||||
// Type "ab", then "abcdef". (run() is called directly: the debounce timer is a
|
||||
// browser timer, and this test has no browser.)
|
||||
c.query.Set("ab")
|
||||
c.run("ab")
|
||||
c.query.Set("abcdef")
|
||||
c.run("abcdef")
|
||||
|
||||
if len(pending) != 2 {
|
||||
t.Fatalf("expected 2 searches, got %d (%v)", len(pending), queries)
|
||||
}
|
||||
|
||||
// The answer to "abcdef" comes back first...
|
||||
pending[1](asyncOpts("Abcdef Ltd"))
|
||||
if got := c.results.Get(); len(got) != 1 || got[0].Label != "Abcdef Ltd" {
|
||||
t.Fatalf("the current query's results were not shown: %v", got)
|
||||
}
|
||||
|
||||
// ...and the slow answer to "ab" arrives afterwards. It must be DISCARDED.
|
||||
pending[0](asyncOpts("Ab Corp", "Abacus"))
|
||||
|
||||
got := c.results.Get()
|
||||
if len(got) != 1 || got[0].Label != "Abcdef Ltd" {
|
||||
t.Errorf("a stale response overwrote the current results: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Below MinChars there is no search at all — a one-character query against a big table
|
||||
// is a scan, and every user who ever types starts by typing one character.
|
||||
func TestAsyncComboboxRespectsMinChars(t *testing.T) {
|
||||
searched := 0
|
||||
c := NewAsyncCombobox(AsyncComboboxOptions{
|
||||
MinChars: 3,
|
||||
Search: func(string, func([]FormSelectOption)) { searched++ },
|
||||
})
|
||||
|
||||
c.onInput("a")
|
||||
c.onInput("ab")
|
||||
if searched != 0 {
|
||||
t.Errorf("searched %d times below MinChars", searched)
|
||||
}
|
||||
if c.IsOpen() {
|
||||
t.Error("the panel opened with nothing to show")
|
||||
}
|
||||
}
|
||||
|
||||
// Choosing an option closes the box and reports the whole option, not just its value —
|
||||
// the caller usually needs the label back to display it.
|
||||
func TestAsyncComboboxSelect(t *testing.T) {
|
||||
var picked FormSelectOption
|
||||
c := NewAsyncCombobox(AsyncComboboxOptions{
|
||||
Search: func(_ string, done func([]FormSelectOption)) { done(asyncOpts("Ada Lovelace")) },
|
||||
})
|
||||
|
||||
c.query.Set("ada")
|
||||
c.run("ada")
|
||||
c.f.Show()
|
||||
|
||||
node := c.Render(FormAsyncComboboxProps{OnSelect: func(o FormSelectOption) { picked = o }})
|
||||
if !findAndClick(node, "Ada Lovelace") {
|
||||
t.Fatalf("no result row to click:\n%s", vdom.RenderHTML(node))
|
||||
}
|
||||
|
||||
if picked.Value != "ada lovelace" {
|
||||
t.Errorf("OnSelect got %+v", picked)
|
||||
}
|
||||
if c.IsOpen() {
|
||||
t.Error("choosing an option should close the panel")
|
||||
}
|
||||
if c.query.Get() != "Ada Lovelace" {
|
||||
t.Errorf("the field should show the chosen label, got %q", c.query.Get())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user