164 lines
4.8 KiB
Go
164 lines
4.8 KiB
Go
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())
|
|
}
|
|
}
|