Add input components 086-105; park focus off disabled controls

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 20:13:21 -04:00
1 parent 095eab2b4c
commit 03b789f533
34 files changed
+3058 -43

No files matched your search

+290
View File
@@ -0,0 +1,290 @@
package input_test
import (
"slices"
"testing"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
func TestRadioGroupArrowsChooseAfterAClick(t *testing.T) {
season := 0
tt := view(400, 200, nil, func(c *ui.Context) {
input.RadioGroup(c, []string{"Spring", "Summer", "Autumn"}, &season, input.RadioGroupOptions{})
})
click(t, tt, "Autumn")
tt.Key(0, ui.KeyUp)
if season != 1 {
t.Fatalf("Up after a click chose %d, want 1", season)
}
// A press dragged off a radio chooses nothing.
r := find(t, tt, "Spring")
tt.Press(r.X+4, r.Y+r.H/2)
tt.Move(r.X+4, r.Y+200)
tt.Release(r.X+4, r.Y+200)
if season != 1 {
t.Fatalf("a press dragged off chose %d", season)
}
}
func TestSelectPicksByClickAndKeys(t *testing.T) {
class, changes, disabled := "Knight", 0, false
tt := view(400, 300, nil, func(c *ui.Context) {
if input.Select(c, "Class", &class, []string{"Knight", "Mage", "Thief"}, input.SelectOptions{Disabled: disabled}).Changed {
changes++
}
})
click(t, tt, "Class")
mage := find(t, tt, "Mage")
if !has(slots(t, tt.Image(), find(t, tt, "Knight")), pixel.Accent) {
t.Fatal("the chosen option is not marked")
}
tt.Move(mage.X+mage.W/2, mage.Y+mage.H/2)
if !has(slots(t, tt.Image(), find(t, tt, "Mage")), pixel.Lit) {
t.Fatal("the option under the pointer is not lit")
}
click(t, tt, "Mage")
if class != "Mage" || changes != 1 || tt.HasText("Thief") {
t.Fatalf("click chose %q with %d changes; popup open %v", class, changes, tt.HasText("Thief"))
}
if _, ok := tt.Find("Thief"); ok {
t.Fatal("the popup stayed open")
}
tt.Key(0, ui.KeyTab)
tt.Key(ui.Shift, ui.KeyTab)
tt.Key(0, ui.KeyDown)
tt.Key(0, ui.KeyDown)
tt.Key(0, ui.KeyEnter)
if class != "Thief" || changes != 2 {
t.Fatalf("Down, Down, Enter chose %q", class)
}
tt.Key(0, ui.KeyDown)
tt.Key(0, ui.KeyEscape)
if _, ok := tt.Find("Mage"); ok || !tt.Focused("Class") {
t.Fatal("Escape left the popup open or the focus elsewhere")
}
disabled = true
tt.Frame()
click(t, tt, "Class")
if _, ok := tt.Find("Mage"); ok {
t.Fatal("a disabled select opened")
}
}
func TestComboboxReachesEveryOption(t *testing.T) {
v := ""
letters := []string{"A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9"}
tt := view(400, 400, nil, func(c *ui.Context) {
input.Combobox(c, "Pick", &v, letters, input.ComboboxOptions{})
})
click(t, tt, "Pick")
tt.Key(0, ui.KeyDown)
if _, ok := tt.Find("A9"); ok {
t.Fatal("more than eight rows show")
}
// A resting pointer must not steal the light.
a1 := find(t, tt, "A1")
tt.Move(a1.X+a1.W/2, a1.Y+a1.H/2)
for range 8 {
tt.Key(0, ui.KeyDown)
}
_, first := tt.Find("A1")
if _, last := tt.Find("A9"); first || !last {
t.Fatal("the rows did not follow the lit option")
}
tt.Key(0, ui.KeyEnter)
if v != "A9" {
t.Fatalf("eight Downs and Enter took %q", v)
}
tt.Key(0, ui.KeyDown)
// With the pointer on the first row, the wheel moves.
r := find(t, tt, "A1")
tt.Move(r.X+r.W/2, r.Y+r.H/2)
tt.Scroll(r.X+r.W/2, r.Y+r.H/2, 0, 4*2*pixel.LineHeight)
tt.Key(0, ui.KeyEnter)
if v != "A5" {
t.Fatalf("four rows of wheel then Enter took %q", v)
}
}
func TestBadOptionsPanic(t *testing.T) {
for name, build := range map[string]func(c *ui.Context){
"Select Width": func(c *ui.Context) {
s := "A"
input.Select(c, "S", &s, []string{"A"}, input.SelectOptions{Width: -1})
},
"Knob Step": func(c *ui.Context) {
v := 0.5
input.Knob(c, "K", &v, input.KnobOptions{Step: -0.1})
},
} {
func() {
defer func() {
if recover() == nil {
t.Errorf("%s: a negative value did not panic", name)
}
}()
view(200, 100, nil, build)
}()
}
}
func TestMultiSelectChecksAndShowsTags(t *testing.T) {
var gear []string
changes := 0
tt := view(400, 300, nil, func(c *ui.Context) {
if input.MultiSelect(c, "Gear", &gear, []string{"Sword", "Shield", "Potion"}, input.MultiSelectOptions{}).Changed {
changes++
}
})
trigger := find(t, tt, "Gear")
empty := slots(t, tt.Image(), trigger)
click(t, tt, "Gear")
click(t, tt, "Shield")
click(t, tt, "Potion")
click(t, tt, "Shield")
if !slices.Equal(gear, []string{"Potion"}) || changes != 3 {
t.Fatalf("checks left %q after %d changes", gear, changes)
}
tt.Key(0, ui.KeyEscape)
if _, ok := tt.Find("Sword"); ok || !tt.Focused("Gear") {
t.Fatal("Escape left the popup open or the focus in it")
}
if equal(empty, slots(t, tt.Image(), trigger)) {
t.Fatal("the chosen option shows no tag")
}
tt.Key(0, ui.KeyDown)
if !tt.Focused("Sword") {
t.Fatal("opening did not focus the first option")
}
tt.Key(0, ui.KeySpace)
if !slices.Contains(gear, "Sword") {
t.Fatalf("Space left %q", gear)
}
}
func TestComboboxFiltersAndChooses(t *testing.T) {
fruit := ""
chosen := 0
tt := view(400, 300, nil, func(c *ui.Context) {
if input.Combobox(c, "Fruit", &fruit, []string{"Apple", "Apricot", "Banana", "Cherry"}, input.ComboboxOptions{}).Chosen {
chosen++
}
input.Button(c, "after", input.ButtonOptions{})
})
click(t, tt, "Fruit")
tt.Type("ap")
if _, ok := tt.Find("Banana"); ok {
t.Fatal("Banana does not match ap")
}
find(t, tt, "Apricot")
tt.Key(0, ui.KeyDown)
tt.Key(0, ui.KeyEnter)
if fruit != "Apricot" || chosen != 1 {
t.Fatalf("Down, Enter took %q", fruit)
}
tt.Key(ui.Cmd, ui.KeyA)
tt.Type("b")
click(t, tt, "Banana")
if fruit != "Banana" || !tt.Focused("Fruit") {
t.Fatalf("a click took %q; focus kept %v", fruit, tt.Focused("Fruit"))
}
tt.Key(ui.Cmd, ui.KeyA)
tt.Type("c")
find(t, tt, "Cherry")
tt.Key(0, ui.KeyEscape)
if _, ok := tt.Find("Cherry"); ok {
t.Fatal("Escape left the list open")
}
tt.Type("h")
find(t, tt, "Cherry")
tt.Key(0, ui.KeyTab)
if _, ok := tt.Find("Cherry"); ok || !tt.Focused("after") {
t.Fatal("leaving the field left the list open")
}
}
func TestAutocompleteGhostsAndAccepts(t *testing.T) {
monster, accepted := "", 0
tt := view(400, 200, nil, func(c *ui.Context) {
if input.Autocomplete(c, "Monster", &monster, []string{"Dragon", "Drake", "Dungeon"}, input.AutocompleteOptions{}).Accepted {
accepted++
}
input.Button(c, "after", input.ButtonOptions{})
})
click(t, tt, "Monster")
tt.Type("zz")
if has(slots(t, tt.Image(), find(t, tt, "Monster")), pixel.Faint) {
t.Fatal("a ghost shows without a completion")
}
tt.Key(ui.Cmd, ui.KeyA)
tt.Type("dra")
if !has(slots(t, tt.Image(), find(t, tt, "Monster")), pixel.Faint) {
t.Fatal("no ghost completion")
}
tt.Key(0, ui.KeyTab)
if monster != "Dragon" || accepted != 1 || !tt.Focused("Monster") {
t.Fatalf("Tab took %q, focus kept %v", monster, tt.Focused("Monster"))
}
tt.Key(ui.Cmd, ui.KeyA)
tt.Type("dun")
tt.Key(0, ui.KeyEnter)
if monster != "Dungeon" || accepted != 2 {
t.Fatalf("Enter took %q", monster)
}
tt.Key(0, ui.KeyTab)
if !tt.Focused("after") {
t.Fatal("Tab without a completion kept the focus")
}
}
func TestTokenFieldAddsAndRemoves(t *testing.T) {
var tags []string
tt := view(400, 200, nil, func(c *ui.Context) {
input.TokenField(c, "Tags", &tags, input.TokenFieldOptions{})
})
click(t, tt, "Tags")
tt.Type("fire")
tt.Key(0, ui.KeyEnter)
tt.Type("ice,wind,")
tt.Type("fire")
tt.Key(0, ui.KeyEnter)
if !slices.Equal(tags, []string{"fire", "ice", "wind"}) {
t.Fatalf("Enter and commas gave %q", tags)
}
tt.Type("x")
tt.Key(0, ui.KeyBackspace)
tt.Key(0, ui.KeyBackspace)
if !slices.Equal(tags, []string{"fire", "ice"}) {
t.Fatalf("Backspace on empty gave %q", tags)
}
click(t, tt, "REMOVE fire")
if !slices.Equal(tags, []string{"ice"}) {
t.Fatalf("the × gave %q", tags)
}
}
func TestSegmentedClicksAndArrows(t *testing.T) {
mode, changes := 0, 0
tt := view(400, 200, nil, func(c *ui.Context) {
if input.Segmented(c, "Mode", []string{"EASY", "NORMAL", "HARD"}, &mode, input.SegmentedOptions{}).Changed {
changes++
}
})
click(t, tt, "HARD")
tt.Key(0, ui.KeyLeft)
if mode != 1 || changes != 2 {
t.Fatalf("click and Left gave %d after %d changes", mode, changes)
}
// One dot in from each segment's corner is its fill.
fill := func(l string) pixel.Slot {
r := find(t, tt, l)
return slots(t, tt.Image(), ui.Rect{X: r.X + 4, Y: r.Y + 4, W: 2, H: 2})[0]
}
if fill("NORMAL") != pixel.Accent || fill("HARD") == pixel.Accent {
t.Fatal("the chosen segment is not the Accent one")
}
}