fix multiselect combobox
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -183,6 +184,174 @@ func TestDropdownsAreStandalone(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- the panel rows ----
|
||||
|
||||
// A multi-select row shows a CHECKBOX. Its job is to show a state you toggle, not an
|
||||
// item you pick, and the tick is the only thing that says which is which.
|
||||
//
|
||||
// The unified dropdown dropped it: both kinds of row rendered as a bare label, so a
|
||||
// multi-select gave no indication of what was already selected.
|
||||
func TestMultiSelectOptionsHaveCheckboxes(t *testing.T) {
|
||||
m := NewMultiSelect(DropdownOptions{})
|
||||
m.Open()
|
||||
|
||||
node := m.Render(FormMultiSelectProps{
|
||||
Options: dropdownOptions(),
|
||||
Value: []string{"res"}, // Research is selected; the other two are not
|
||||
})
|
||||
|
||||
for _, want := range []struct {
|
||||
label string
|
||||
checked bool
|
||||
}{
|
||||
{"Engineering", false},
|
||||
{"Research", true},
|
||||
{"Networking", false},
|
||||
} {
|
||||
box := findCheckboxIn(optionRow(t, node, want.label))
|
||||
if box == nil {
|
||||
t.Errorf("option %q has no checkbox", want.label)
|
||||
continue
|
||||
}
|
||||
if got := box.Props["checked"]; got != strconv.FormatBool(want.checked) {
|
||||
t.Errorf("option %q: checked=%q, want %v", want.label, got, want.checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ...and a combobox row does NOT. It picks one thing; a checkbox would promise a
|
||||
// multiple selection it will not honour.
|
||||
func TestComboboxOptionsHaveNoCheckboxes(t *testing.T) {
|
||||
c := NewCombobox(DropdownOptions{})
|
||||
c.Open()
|
||||
|
||||
node := c.Render(FormComboboxProps{Options: dropdownOptions(), Value: "res"})
|
||||
if findCheckboxIn(optionRow(t, node, "Research")) != nil {
|
||||
t.Error("a single-select option must not render a checkbox")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- the collapsed trigger ----
|
||||
|
||||
// Past maxTagsBeforeCollapse the field stops showing pills and says how many are
|
||||
// selected. Without this the trigger either grows without bound or silently clips the
|
||||
// selection, and the user cannot tell how much they picked.
|
||||
func TestMultiSelectCollapsesToACount(t *testing.T) {
|
||||
m := NewMultiSelect(DropdownOptions{})
|
||||
html := vdom.RenderHTML(m.Render(FormMultiSelectProps{
|
||||
Options: manyOptions(6),
|
||||
Value: []string{"o1", "o2", "o3", "o4"}, // 4 > the default max of 3
|
||||
MaxTagsBeforeCollapse: 3,
|
||||
}))
|
||||
|
||||
if !strings.Contains(html, "4 items selected") {
|
||||
t.Errorf("collapsed trigger does not say how many are selected:\n%s", html)
|
||||
}
|
||||
// The count is the SERVER's choice too — this needs no measurement, so SSR must not
|
||||
// ship a row of clipped pills that the client then has to swap out.
|
||||
if !strings.Contains(html, `class="flex flex-nowrap items-center gap-1 min-w-0 overflow-hidden hidden"`) {
|
||||
t.Errorf("the pills were not hidden server-side:\n%s", html)
|
||||
}
|
||||
}
|
||||
|
||||
// One is one ITEM, not one items.
|
||||
func TestCollapsedCountIsPluralised(t *testing.T) {
|
||||
if got := itemsSelected(1); got != "1 item selected" {
|
||||
t.Errorf("itemsSelected(1) = %q", got)
|
||||
}
|
||||
if got := itemsSelected(4); got != "4 items selected" {
|
||||
t.Errorf("itemsSelected(4) = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Under the limit, the pills show and the summary is the hidden one. Both are always in
|
||||
// the DOM: which is visible is settled by a measurement after render (collapseFace),
|
||||
// and that measurement needs something to measure.
|
||||
func TestMultiSelectShowsPillsWhenTheyFit(t *testing.T) {
|
||||
m := NewMultiSelect(DropdownOptions{})
|
||||
html := vdom.RenderHTML(m.Render(FormMultiSelectProps{
|
||||
Options: dropdownOptions(),
|
||||
Value: []string{"eng", "res"},
|
||||
MaxTagsBeforeCollapse: 3,
|
||||
}))
|
||||
|
||||
if !strings.Contains(html, "Engineering") || !strings.Contains(html, "Research") {
|
||||
t.Errorf("the selected pills are missing:\n%s", html)
|
||||
}
|
||||
if !strings.Contains(html, `class="truncate hidden"`) {
|
||||
t.Errorf("the count summary should be the hidden face here:\n%s", html)
|
||||
}
|
||||
// It must be RENDERED though, or there is nothing for collapseFace to reveal.
|
||||
if !strings.Contains(html, "2 items selected") {
|
||||
t.Errorf("the count summary was not rendered at all:\n%s", html)
|
||||
}
|
||||
}
|
||||
|
||||
// The pills must lay out on ONE line and clip. With flex-wrap they wrap instead, so
|
||||
// scrollWidth never exceeds clientWidth, and the overflow collapse can never fire.
|
||||
func TestPillsAreNowrapSoOverflowIsMeasurable(t *testing.T) {
|
||||
m := NewMultiSelect(DropdownOptions{})
|
||||
html := vdom.RenderHTML(m.Render(FormMultiSelectProps{
|
||||
Options: dropdownOptions(),
|
||||
Value: []string{"eng"},
|
||||
}))
|
||||
if strings.Contains(html, "flex-wrap") {
|
||||
t.Error("the pill row wraps; overflow would never be detected")
|
||||
}
|
||||
if !strings.Contains(html, "flex-nowrap") || !strings.Contains(html, "overflow-hidden") {
|
||||
t.Errorf("the pill row must be flex-nowrap + overflow-hidden:\n%s", html)
|
||||
}
|
||||
}
|
||||
|
||||
func manyOptions(n int) []FormSelectOption {
|
||||
out := make([]FormSelectOption, n)
|
||||
for i := range out {
|
||||
id := strconv.Itoa(i + 1)
|
||||
out[i] = FormSelectOption{Value: "o" + id, Label: "Option " + id}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// optionRow returns the panel row whose label is label.
|
||||
func optionRow(t *testing.T, n *vdom.VNode, label string) *vdom.VNode {
|
||||
t.Helper()
|
||||
if row := findRow(n, label); row != nil {
|
||||
return row
|
||||
}
|
||||
t.Fatalf("no option row labelled %q", label)
|
||||
return nil
|
||||
}
|
||||
|
||||
func findRow(n *vdom.VNode, label string) *vdom.VNode {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
if n.Tag == "button" && n.Attrs["role"] == "option" && nodeText(n) == label {
|
||||
return n
|
||||
}
|
||||
for _, c := range n.Children {
|
||||
if row := findRow(c, label); row != nil {
|
||||
return row
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findCheckboxIn(n *vdom.VNode) *vdom.VNode {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
if n.Tag == "input" && n.Attrs["type"] == "checkbox" {
|
||||
return n
|
||||
}
|
||||
for _, c := range n.Children {
|
||||
if box := findCheckboxIn(c); box != nil {
|
||||
return box
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
Reference in New Issue
Block a user