package webui import ( "strings" "testing" "kjol/wasmruntime/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()) } }