rename packages, refactor out tailwind compiler,
This commit is contained in:
221
go/webui/forms_dropdown_test.go
Normal file
221
go/webui/forms_dropdown_test.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"kjol/vdom"
|
||||
)
|
||||
|
||||
// fakeEvent is a vdom.Event with no DOM behind it — enough to drive Floating's
|
||||
// close handlers natively. Target() is nil, which the outside-click test reads (quite
|
||||
// correctly) as "this click was not inside the panel".
|
||||
type fakeEvent struct{ key string }
|
||||
|
||||
func (fakeEvent) PreventDefault() {}
|
||||
func (fakeEvent) StopPropagation() {}
|
||||
func (fakeEvent) Value() string { return "" }
|
||||
func (fakeEvent) Checked() bool { return false }
|
||||
func (e fakeEvent) Key() string { return e.key }
|
||||
func (fakeEvent) ClientX() int { return 0 }
|
||||
func (fakeEvent) ClientY() int { return 0 }
|
||||
func (fakeEvent) Target() any { return nil }
|
||||
func (fakeEvent) SetData(_, _ string) {}
|
||||
func (fakeEvent) GetData(string) string { return "" }
|
||||
|
||||
var _ vdom.Event = fakeEvent{}
|
||||
|
||||
func dropdownOptions() []FormSelectOption {
|
||||
return []FormSelectOption{
|
||||
{Value: "eng", Label: "Engineering"},
|
||||
{Value: "res", Label: "Research"},
|
||||
{Value: "net", Label: "Networking"},
|
||||
}
|
||||
}
|
||||
|
||||
// ---- closing ----
|
||||
|
||||
// The previous port took Open/OnToggle from the caller and had NO outside-click and
|
||||
// NO Escape: once open, a dropdown stayed open until you clicked its trigger again.
|
||||
// Both now come from Floating.
|
||||
|
||||
func TestMultiSelectClosesOnOutsideClick(t *testing.T) {
|
||||
m := NewMultiSelect(DropdownOptions{})
|
||||
m.Open()
|
||||
if !m.IsOpen() {
|
||||
t.Fatal("did not open")
|
||||
}
|
||||
|
||||
m.f.onOutside(fakeEvent{}) // a mousedown that landed outside the panel
|
||||
if m.IsOpen() {
|
||||
t.Error("a multi-select must close on an outside click")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiSelectClosesOnEscape(t *testing.T) {
|
||||
m := NewMultiSelect(DropdownOptions{})
|
||||
m.Open()
|
||||
|
||||
m.f.onKeydown(fakeEvent{key: vdom.KEY_ESCAPE})
|
||||
if m.IsOpen() {
|
||||
t.Error("a multi-select must close on Escape")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComboboxClosesOnOutsideClickAndEscape(t *testing.T) {
|
||||
c := NewCombobox(DropdownOptions{})
|
||||
|
||||
c.Open()
|
||||
c.f.onOutside(fakeEvent{})
|
||||
if c.IsOpen() {
|
||||
t.Error("a combobox must close on an outside click")
|
||||
}
|
||||
|
||||
c.Open()
|
||||
c.f.onKeydown(fakeEvent{key: vdom.KEY_ESCAPE})
|
||||
if c.IsOpen() {
|
||||
t.Error("a combobox must close on Escape")
|
||||
}
|
||||
}
|
||||
|
||||
// Another key must NOT close it — otherwise typing in the search box would dismiss
|
||||
// the thing you are searching.
|
||||
func TestDropdownIgnoresOtherKeys(t *testing.T) {
|
||||
m := NewMultiSelect(DropdownOptions{})
|
||||
m.Open()
|
||||
m.f.onKeydown(fakeEvent{key: "a"})
|
||||
if !m.IsOpen() {
|
||||
t.Error("an ordinary keypress closed the dropdown")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- where they differ, deliberately ----
|
||||
|
||||
// A combobox picks ONE thing, so choosing is the end of the interaction: it closes.
|
||||
func TestComboboxClosesWhenAnOptionIsChosen(t *testing.T) {
|
||||
c := NewCombobox(DropdownOptions{})
|
||||
got := ""
|
||||
c.Open()
|
||||
|
||||
// Render, then invoke the option's click handler the way the DOM would.
|
||||
node := c.Render(FormComboboxProps{
|
||||
Options: dropdownOptions(),
|
||||
OnChange: func(v string) { got = v },
|
||||
})
|
||||
clickOption(t, node, "Research")
|
||||
|
||||
if got != "res" {
|
||||
t.Errorf("OnChange got %q, want \"res\"", got)
|
||||
}
|
||||
if c.IsOpen() {
|
||||
t.Error("a combobox should close once an option is chosen")
|
||||
}
|
||||
}
|
||||
|
||||
// A multi-select picks SEVERAL, so choosing is NOT the end: closing the panel after
|
||||
// every tick would make it unusable. This is the difference the two have to keep.
|
||||
func TestMultiSelectStaysOpenWhenAnOptionIsChosen(t *testing.T) {
|
||||
m := NewMultiSelect(DropdownOptions{})
|
||||
var got []string
|
||||
m.Open()
|
||||
|
||||
node := m.Render(FormMultiSelectProps{
|
||||
Options: dropdownOptions(),
|
||||
Value: []string{},
|
||||
OnChange: func(v []string) { got = v },
|
||||
})
|
||||
clickOption(t, node, "Research")
|
||||
|
||||
if len(got) != 1 || got[0] != "res" {
|
||||
t.Errorf("OnChange got %v, want [res]", got)
|
||||
}
|
||||
if !m.IsOpen() {
|
||||
t.Error("a multi-select must STAY OPEN when an option is ticked — you are mid-selection")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- the search box actually filters ----
|
||||
//
|
||||
// It used to be decorative: it rendered, and filtered nothing.
|
||||
|
||||
func TestDropdownSearchFilters(t *testing.T) {
|
||||
m := NewMultiSelect(DropdownOptions{})
|
||||
m.Open()
|
||||
|
||||
all := m.filter(dropdownOptions())
|
||||
if len(all) != 3 {
|
||||
t.Fatalf("an empty query filtered to %d options, want all 3", len(all))
|
||||
}
|
||||
|
||||
m.search.Set("res")
|
||||
got := m.filter(dropdownOptions())
|
||||
if len(got) != 1 || got[0].Value != "res" {
|
||||
t.Errorf("query \"res\" matched %v, want just Research", got)
|
||||
}
|
||||
|
||||
// Case-insensitive, on the label the user can actually see.
|
||||
m.search.Set("NETWORK")
|
||||
if got := m.filter(dropdownOptions()); len(got) != 1 || got[0].Value != "net" {
|
||||
t.Errorf("query \"NETWORK\" matched %v, want Networking", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Closing abandons the query — reopening to a stale filter, showing one of three
|
||||
// options for no visible reason, is worse than retyping.
|
||||
func TestClosingClearsTheSearch(t *testing.T) {
|
||||
m := NewMultiSelect(DropdownOptions{})
|
||||
m.Open()
|
||||
m.search.Set("res")
|
||||
|
||||
m.Close()
|
||||
if m.search.Get() != "" {
|
||||
t.Errorf("search survived the close: %q", m.search.Get())
|
||||
}
|
||||
}
|
||||
|
||||
// These open INSIDE other floatings (a filter popover, the calculated-column form).
|
||||
// Without Standalone the single-open manager reads the dropdown as a rival panel and
|
||||
// closes the very popover it lives in.
|
||||
func TestDropdownsAreStandalone(t *testing.T) {
|
||||
m := NewMultiSelect(DropdownOptions{})
|
||||
if !m.f.opts.Standalone {
|
||||
t.Error("a dropdown must be Standalone, or opening it closes its own parent popover")
|
||||
}
|
||||
}
|
||||
|
||||
// clickOption finds an option button by its label and invokes its click handler.
|
||||
func clickOption(t *testing.T, n *vdom.VNode, label string) {
|
||||
t.Helper()
|
||||
if !findAndClick(n, label) {
|
||||
t.Fatalf("no clickable option labelled %q in the rendered dropdown", label)
|
||||
}
|
||||
}
|
||||
|
||||
func findAndClick(n *vdom.VNode, label string) bool {
|
||||
if n == nil {
|
||||
return false
|
||||
}
|
||||
if n.Tag == "button" && nodeText(n) == label {
|
||||
if h := n.Events[vdom.EVENT_CLICK]; h != nil {
|
||||
h(fakeEvent{})
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, c := range n.Children {
|
||||
if findAndClick(c, label) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func nodeText(n *vdom.VNode) string {
|
||||
if n.Tag == "" {
|
||||
return n.Text
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, c := range n.Children {
|
||||
b.WriteString(nodeText(c))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
Reference in New Issue
Block a user