Add input components 066-085; gallery chrome on real components; deploy warms the edge
This commit is contained in:
38 files changed
+2340
-172
No files matched your search
@@ -0,0 +1,493 @@
|
||||
package input_test
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
type clock struct{ t time.Time }
|
||||
|
||||
func (c *clock) now() time.Time { return c.t }
|
||||
|
||||
func view(w, h int, clk *clock, build func(c *ui.Context)) *ui.Tester {
|
||||
if clk == nil {
|
||||
clk = &clock{time.Unix(3_000_000, 0)}
|
||||
}
|
||||
return ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: clk.now})
|
||||
build(c)
|
||||
}, w, h)
|
||||
}
|
||||
|
||||
func find(t *testing.T, tt *ui.Tester, label string) ui.Rect {
|
||||
t.Helper()
|
||||
r, ok := tt.Find(label)
|
||||
if !ok {
|
||||
t.Fatalf("no %q in %q", label, tt.Texts())
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func click(t *testing.T, tt *ui.Tester, label string) {
|
||||
t.Helper()
|
||||
if err := tt.Click(label); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// slots are the palette slots of a box's dots.
|
||||
func slots(t *testing.T, img *image.RGBA, r ui.Rect) []pixel.Slot {
|
||||
t.Helper()
|
||||
var out []pixel.Slot
|
||||
for y := int(r.Y); y < int(r.Y+r.H); y += 2 {
|
||||
for x := int(r.X); x < int(r.X+r.W); x += 2 {
|
||||
c, found := img.RGBAAt(x, y), false
|
||||
for s, pc := range pixel.SpringDay.Colors {
|
||||
if pc.R == c.R && pc.G == c.G && pc.B == c.B {
|
||||
out, found = append(out, pixel.Slot(s)), true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("pixel %d,%d off the palette", x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ink is where a box holds Ink, ignoring its background.
|
||||
func ink(t *testing.T, img *image.RGBA, r ui.Rect) []bool {
|
||||
t.Helper()
|
||||
var out []bool
|
||||
for _, s := range slots(t, img, r) {
|
||||
out = append(out, s == pixel.Ink)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func has(s []pixel.Slot, want pixel.Slot) bool {
|
||||
for _, v := range s {
|
||||
if v == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestButtonClicksKeysLoadingAndSizes(t *testing.T) {
|
||||
clicks, opts := 0, input.ButtonOptions{Variant: input.Primary}
|
||||
tt := view(400, 200, nil, func(c *ui.Context) {
|
||||
if input.Button(c, "GO", opts).Clicked {
|
||||
clicks++
|
||||
}
|
||||
input.Button(c, "S", input.ButtonOptions{Size: input.Small})
|
||||
input.Button(c, "L", input.ButtonOptions{Size: input.Large})
|
||||
})
|
||||
click(t, tt, "GO")
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
if clicks != 2 {
|
||||
t.Fatalf("click and Enter gave %d clicks", clicks)
|
||||
}
|
||||
if !has(slots(t, tt.Image(), find(t, tt, "GO")), pixel.Accent) {
|
||||
t.Fatal("a primary button has no Accent face")
|
||||
}
|
||||
if s, l := find(t, tt, "S"), find(t, tt, "L"); s.H != 2*pixel.LineHeight || l.H != 2*(2*pixel.LineHeight+4) {
|
||||
t.Fatalf("small %v, large %v", s.H, l.H)
|
||||
}
|
||||
r := find(t, tt, "GO")
|
||||
top := func() pixel.Slot { return slots(t, tt.Image(), ui.Rect{X: r.X + r.W/2, Y: r.Y, W: 2, H: 2})[0] }
|
||||
up := top()
|
||||
tt.Press(r.X+r.W/2, r.Y+r.H/2)
|
||||
if top() == up {
|
||||
t.Fatal("pressing did not sink the face")
|
||||
}
|
||||
tt.Release(r.X+r.W/2, r.Y+r.H/2)
|
||||
opts.Loading = true
|
||||
tt.Frame()
|
||||
click(t, tt, "GO")
|
||||
opts.Loading, opts.Disabled = false, true
|
||||
tt.Frame()
|
||||
click(t, tt, "GO")
|
||||
if clicks != 3 {
|
||||
t.Fatalf("loading or disabled clicked: %d clicks", clicks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIconButtonNeedsAName(t *testing.T) {
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("a nameless icon button did not panic")
|
||||
}
|
||||
}()
|
||||
view(100, 100, nil, func(c *ui.Context) { input.IconButton(c, icon.Star, "", input.IconButtonOptions{}) })
|
||||
}
|
||||
|
||||
func TestButtonGroupIsOneTabStop(t *testing.T) {
|
||||
got := -1
|
||||
tt := view(400, 100, nil, func(c *ui.Context) {
|
||||
if r := input.ButtonGroup(c, []string{"A", "B", "C"}, input.ButtonGroupOptions{}); r.Clicked >= 0 {
|
||||
got = r.Clicked
|
||||
}
|
||||
input.Button(c, "after", input.ButtonOptions{})
|
||||
})
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyRight)
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
if got != 1 {
|
||||
t.Fatalf("Tab, Right, Enter clicked %d", got)
|
||||
}
|
||||
tt.Key(0, ui.KeyTab)
|
||||
if !tt.Focused("after") {
|
||||
t.Fatal("Tab did not leave the group")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTogglesChecksAndSwitch(t *testing.T) {
|
||||
var on, checked, lever bool
|
||||
modes := []bool{true, false, false}
|
||||
tt := view(500, 200, nil, func(c *ui.Context) {
|
||||
input.Toggle(c, "MUSIC", &on, input.ToggleOptions{})
|
||||
input.Checkbox(c, "Remember", &checked, input.CheckboxOptions{})
|
||||
input.Switch(c, "Power", &lever, input.SwitchOptions{})
|
||||
input.ToggleGroup(c, []string{"E", "N", "H"}, modes, input.ToggleGroupOptions{Single: true})
|
||||
})
|
||||
click(t, tt, "MUSIC")
|
||||
click(t, tt, "Remember")
|
||||
tt.Key(0, ui.KeySpace)
|
||||
click(t, tt, "Power")
|
||||
click(t, tt, "H")
|
||||
if !on || checked || !lever || modes[0] || !modes[2] {
|
||||
t.Fatalf("toggle %v, checkbox %v after Space, switch %v, modes %v", on, checked, lever, modes)
|
||||
}
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("a short state slice did not panic")
|
||||
}
|
||||
}()
|
||||
view(100, 100, nil, func(c *ui.Context) {
|
||||
input.ToggleGroup(c, []string{"a", "b"}, []bool{true}, input.ToggleGroupOptions{})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckboxGroupParent(t *testing.T) {
|
||||
checked := []bool{true, false}
|
||||
tt := view(300, 200, nil, func(c *ui.Context) {
|
||||
input.CheckboxGroup(c, "Party", []string{"Knight", "Mage"}, checked, input.CheckboxGroupOptions{})
|
||||
})
|
||||
if !has(slots(t, tt.Image(), find(t, tt, "Party")), pixel.Accent) {
|
||||
t.Fatal("a half-checked group shows no mixed mark")
|
||||
}
|
||||
click(t, tt, "Party")
|
||||
if !checked[0] || !checked[1] {
|
||||
t.Fatalf("parent on a mixed group checked %v", checked)
|
||||
}
|
||||
click(t, tt, "Party")
|
||||
if checked[0] || checked[1] {
|
||||
t.Fatalf("parent on a full group left %v", checked)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRadiosChoose(t *testing.T) {
|
||||
class, season := "knight", 0
|
||||
tt := view(400, 200, nil, func(c *ui.Context) {
|
||||
input.Radio(c, "knight", "knight", &class, input.RadioOptions{})
|
||||
input.Radio(c, "mage", "mage", &class, input.RadioOptions{})
|
||||
input.Button(c, "before", input.ButtonOptions{})
|
||||
input.RadioGroup(c, []string{"Spring", "Summer", "Autumn"}, &season, input.RadioGroupOptions{})
|
||||
})
|
||||
click(t, tt, "mage")
|
||||
click(t, tt, "mage")
|
||||
if class != "mage" {
|
||||
t.Fatalf("radio left %q", class)
|
||||
}
|
||||
click(t, tt, "Autumn")
|
||||
if season != 2 {
|
||||
t.Fatalf("click chose %d", season)
|
||||
}
|
||||
click(t, tt, "before")
|
||||
tt.Key(0, ui.KeyTab)
|
||||
if !tt.Focused("Autumn") || season != 2 {
|
||||
t.Fatal("Tab entered elsewhere than the chosen radio, or chose")
|
||||
}
|
||||
tt.Key(0, ui.KeyUp)
|
||||
if season != 1 {
|
||||
t.Fatalf("Up chose %d, want 1", season)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlidersDragKeysAndSteps(t *testing.T) {
|
||||
v, s, low, high, f := 0.0, 0.0, 20.0, 60.0, 0.0
|
||||
disabled := false
|
||||
tt := view(500, 300, nil, func(c *ui.Context) {
|
||||
ui.Column(c).Gap(10).Children(func() {
|
||||
input.Slider(c, "vol", &v, input.SliderOptions{Width: 100, Disabled: disabled})
|
||||
input.StepSlider(c, "speed", &s, input.StepSliderOptions{Min: 0, Max: 100, Step: 20, Width: 100})
|
||||
input.RangeSlider(c, "lv", &low, &high, input.RangeSliderOptions{Min: 0, Max: 100, Width: 100})
|
||||
input.Fader(c, "master", &f, input.FaderOptions{Height: 50})
|
||||
})
|
||||
})
|
||||
r := find(t, tt, "vol")
|
||||
tt.ClickAt(r.X+r.W*0.75, r.Y+r.H/2)
|
||||
if v < 0.7 || v > 0.8 {
|
||||
t.Fatalf("a click three quarters along set %v", v)
|
||||
}
|
||||
tt.Key(0, ui.KeyHome)
|
||||
tt.Key(0, ui.KeyRight)
|
||||
if v != 0.05 {
|
||||
t.Fatalf("Home then Right gave %v, want 0.05", v)
|
||||
}
|
||||
sr := find(t, tt, "speed")
|
||||
tt.ClickAt(sr.X+100*2*0.33, sr.Y+sr.H/2)
|
||||
if s != 40 {
|
||||
t.Fatalf("a third along snapped to %v, want 40", s)
|
||||
}
|
||||
tt.Key(0, ui.KeyRight)
|
||||
if _, shown := tt.Find("60"); s != 60 || !shown {
|
||||
t.Fatalf("Right stepped to %v, shown %v, want 60", s, shown)
|
||||
}
|
||||
lowKnob := find(t, tt, "lv low")
|
||||
tt.Press(lowKnob.X+lowKnob.W/2, lowKnob.Y+lowKnob.H/2)
|
||||
tt.Move(lowKnob.X+190, lowKnob.Y+lowKnob.H/2)
|
||||
tt.Release(lowKnob.X+190, lowKnob.Y+lowKnob.H/2)
|
||||
if low != high {
|
||||
t.Fatalf("dragging low past high left %v and %v", low, high)
|
||||
}
|
||||
fr := find(t, tt, "master")
|
||||
tt.ClickAt(fr.X+fr.W/2, fr.Y+2)
|
||||
if f < 0.9 {
|
||||
t.Fatalf("a click at the fader's top set %v", f)
|
||||
}
|
||||
disabled, v = true, 0.5
|
||||
tt.Frame()
|
||||
tt.ClickAt(r.X+2, r.Y+r.H/2)
|
||||
if v != 0.5 {
|
||||
t.Fatal("a disabled slider moved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepperRepeatsWhileHeld(t *testing.T) {
|
||||
clk := &clock{time.Unix(3_000_000, 0)}
|
||||
n := 5
|
||||
tt := view(300, 100, clk, func(c *ui.Context) {
|
||||
input.Stepper(c, "Lives", &n, input.StepperOptions{Min: 0, Max: 20})
|
||||
})
|
||||
r := find(t, tt, "Lives")
|
||||
right := r.X + r.W + 4 + 12
|
||||
tt.Press(right, r.Y+r.H/2)
|
||||
if n != 6 {
|
||||
t.Fatalf("a press stepped to %d", n)
|
||||
}
|
||||
clk.t = clk.t.Add(300 * time.Millisecond)
|
||||
tt.Frame()
|
||||
if n != 6 {
|
||||
t.Fatalf("repeat began early: %d", n)
|
||||
}
|
||||
clk.t = clk.t.Add(400 * time.Millisecond)
|
||||
tt.Frame()
|
||||
tt.Release(right, r.Y+r.H/2)
|
||||
if n != 9 {
|
||||
t.Fatalf("700 ms held gave %d, want 9", n)
|
||||
}
|
||||
click(t, tt, "Lives")
|
||||
tt.Key(0, ui.KeyDown)
|
||||
if n != 8 {
|
||||
t.Fatalf("Down stepped to %d", n)
|
||||
}
|
||||
n = 30
|
||||
tt.Key(0, ui.KeyUp)
|
||||
if n != 20 {
|
||||
t.Fatalf("Up past the top gave %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumberInputParsesAndSteps(t *testing.T) {
|
||||
v := 5.0
|
||||
tt := view(300, 100, nil, func(c *ui.Context) {
|
||||
input.NumberInput(c, "Price", &v, input.NumberInputOptions{Min: 0, Max: 10})
|
||||
})
|
||||
click(t, tt, "Price")
|
||||
tt.Key(ui.Cmd, ui.KeyA)
|
||||
tt.Type("7.5")
|
||||
if v != 7.5 {
|
||||
t.Fatalf("typing 7.5 left %v", v)
|
||||
}
|
||||
border := func() []pixel.Slot {
|
||||
r := find(t, tt, "Price")
|
||||
return slots(t, tt.Image(), ui.Rect{X: r.X, Y: r.Y - 6, W: r.W, H: 2})
|
||||
}
|
||||
tt.Type("x")
|
||||
if v != 7.5 || !has(border(), pixel.Bad) {
|
||||
t.Fatalf("bad text changed the value to %v, or the edge stayed: %v", v, border()[:3])
|
||||
}
|
||||
tt.Key(ui.Cmd, ui.KeyA)
|
||||
tt.Type("9")
|
||||
tt.Key(0, ui.KeyUp)
|
||||
tt.Key(0, ui.KeyUp)
|
||||
if v != 10 {
|
||||
t.Fatalf("Up twice from 9 gave %v, want the max 10", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldsTypeSubmitClear(t *testing.T) {
|
||||
name, query, secret := "", "slime", "abc"
|
||||
submits := 0
|
||||
tt := view(500, 300, nil, func(c *ui.Context) {
|
||||
if input.TextInput(c, "Name", &name, input.TextInputOptions{MaxLength: 3}).Submitted {
|
||||
submits++
|
||||
}
|
||||
input.SearchField(c, "Find", &query, input.SearchFieldOptions{})
|
||||
input.PasswordInput(c, "Secret", &secret, input.PasswordInputOptions{})
|
||||
})
|
||||
click(t, tt, "Name")
|
||||
tt.Type("Link!")
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
if name != "Lin" || submits != 1 {
|
||||
t.Fatalf("name %q, submits %d", name, submits)
|
||||
}
|
||||
click(t, tt, "Find")
|
||||
tt.Key(0, ui.KeyEscape)
|
||||
if query != "" {
|
||||
t.Fatalf("Escape left %q", query)
|
||||
}
|
||||
secretInk := func() []bool {
|
||||
r := find(t, tt, "Secret")
|
||||
return ink(t, tt.Image(), ui.Rect{X: r.X, Y: r.Y, W: r.W, H: r.H})
|
||||
}
|
||||
masked := secretInk()
|
||||
click(t, tt, "Show")
|
||||
if equal(secretInk(), masked) {
|
||||
t.Fatal("the eye did not reveal the password in the same frame")
|
||||
}
|
||||
click(t, tt, "Find")
|
||||
query = "potion"
|
||||
tt.Frame()
|
||||
tt.Key(0, ui.KeyEscape)
|
||||
r := find(t, tt, "Find")
|
||||
shown := ink(t, tt.Image(), r)
|
||||
tt.Frame()
|
||||
if query != "" || !equal(shown, ink(t, tt.Image(), r)) {
|
||||
t.Fatalf("Escape left %q, or showed the old text a frame", query)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextAreaFollowsTheCaret(t *testing.T) {
|
||||
notes := ""
|
||||
tt := view(400, 300, nil, func(c *ui.Context) {
|
||||
input.TextArea(c, "Notes", ¬es, input.TextAreaOptions{Lines: 2, MaxLength: 50})
|
||||
pixel.Text(c, "F", pixel.TextOptions{})
|
||||
})
|
||||
click(t, tt, "Notes")
|
||||
tt.Type("A\nB\nC\nD\nE\nF")
|
||||
if _, ok := tt.Find("11/50"); !ok {
|
||||
t.Fatalf("no character count in %q", tt.Texts())
|
||||
}
|
||||
area, ref := find(t, tt, "Notes"), find(t, tt, "F")
|
||||
img := tt.Image()
|
||||
glyph := ink(t, img, ui.Rect{X: ref.X, Y: ref.Y, W: ref.W, H: ref.H})
|
||||
last := ink(t, img, ui.Rect{X: area.X, Y: area.Y + area.H - ref.H, W: ref.W, H: ref.H})
|
||||
if !equal(glyph, last) {
|
||||
t.Fatal("the caret's line F is not the bottom line in view")
|
||||
}
|
||||
}
|
||||
|
||||
func equal[T comparable](a, b []T) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestStepSliderStaysOnStepsWithinMax(t *testing.T) {
|
||||
s := 0.0
|
||||
tt := view(300, 100, nil, func(c *ui.Context) {
|
||||
input.StepSlider(c, "odd", &s, input.StepSliderOptions{Min: 0, Max: 10, Step: 6})
|
||||
})
|
||||
click(t, tt, "odd")
|
||||
tt.Key(0, ui.KeyEnd)
|
||||
if s != 6 {
|
||||
t.Fatalf("End went to %v, want the last step 6", s)
|
||||
}
|
||||
f := 0.0
|
||||
tt = view(300, 100, nil, func(c *ui.Context) {
|
||||
input.StepSlider(c, "tenths", &f, input.StepSliderOptions{Min: 0, Max: 0.3, Step: 0.1})
|
||||
})
|
||||
click(t, tt, "tenths")
|
||||
tt.Key(0, ui.KeyEnd)
|
||||
if math.Abs(f-0.3) > 1e-9 {
|
||||
t.Fatalf("End over tenths went to %v, want 0.3", f)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumberInputRejectsNaNAndSyncsAtOnce(t *testing.T) {
|
||||
v, changes := 5.0, 0
|
||||
tt := view(300, 100, nil, func(c *ui.Context) {
|
||||
if input.NumberInput(c, "N", &v, input.NumberInputOptions{Min: 0, Max: 10}).Changed {
|
||||
changes++
|
||||
}
|
||||
})
|
||||
r := find(t, tt, "N")
|
||||
border := func() []pixel.Slot { return slots(t, tt.Image(), ui.Rect{X: r.X, Y: r.Y - 6, W: r.W, H: 2}) }
|
||||
field := func() []bool { return ink(t, tt.Image(), r) }
|
||||
eight := 8.0
|
||||
ref := view(300, 100, nil, func(c *ui.Context) {
|
||||
input.NumberInput(c, "N", &eight, input.NumberInputOptions{Min: 0, Max: 10})
|
||||
})
|
||||
shows8 := func() bool { return equal(field(), ink(t, ref.Image(), r)) }
|
||||
v = 8
|
||||
tt.Frame()
|
||||
if !shows8() {
|
||||
t.Fatal("an outside change did not show at once")
|
||||
}
|
||||
click(t, tt, "N")
|
||||
v = 3
|
||||
tt.Frame()
|
||||
eight = 3
|
||||
ref.Frame()
|
||||
click(t, ref, "N")
|
||||
if !shows8() {
|
||||
t.Fatal("a focused field ignored an outside change")
|
||||
}
|
||||
click(t, tt, "N")
|
||||
tt.Key(ui.Cmd, ui.KeyA)
|
||||
tt.Type("NaN")
|
||||
if v != 3 || !has(border(), pixel.Bad) {
|
||||
t.Fatalf("NaN set %v or left the edge plain", v)
|
||||
}
|
||||
tt.Key(ui.Cmd, ui.KeyA)
|
||||
tt.Type("10")
|
||||
changes = 0
|
||||
tt.Key(0, ui.KeyUp)
|
||||
if v != 10 || changes != 0 {
|
||||
t.Fatalf("Up at the max gave %v with %d changes", v, changes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisabledTextAreaHoldsStill(t *testing.T) {
|
||||
notes, disabled := "A\nB\nC\nD\nE\nF", false
|
||||
tt := view(400, 300, nil, func(c *ui.Context) {
|
||||
input.TextArea(c, "Notes", ¬es, input.TextAreaOptions{Lines: 2, Disabled: disabled})
|
||||
})
|
||||
disabled = true
|
||||
tt.Frame()
|
||||
r := find(t, tt, "Notes")
|
||||
before := ink(t, tt.Image(), r)
|
||||
// The caret's line keeps it at the bottom; wheel up.
|
||||
tt.Scroll(r.X+10, r.Y+10, 0, -64)
|
||||
if !equal(before, ink(t, tt.Image(), find(t, tt, "Notes"))) {
|
||||
t.Fatal("a disabled text area scrolled")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user