Palette cycling, sky, sun and moon, clouds, starfield, rain, snow, petals, leaves, fireflies, waterfall, ocean, campfire, lava, aurora, lightning, mountains and a season scene; scanlines, vignette, screen shake, flash, dither fades, iris wipes, dissolves, mosaics, glitches, sparkles, explosions and confetti. Home opens on a live scene in the palette's season and a strip of all eight palettes. pixel adds Canvas.Cycled, Sprite.At, SpriteOf, Kit.Toward and Kit.Shot, which spends a cue under Reduce Motion so it never replays; display exports Reduce. Gallery grids and the sidebar are one Tab stop each.
320 lines
9.0 KiB
Go
320 lines
9.0 KiB
Go
package feedback_test
|
||
|
||
import (
|
||
"math"
|
||
"strconv"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/egoist/mygo/ui"
|
||
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/feedback"
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||
)
|
||
|
||
func TestCountUpStepsOrJumps(t *testing.T) {
|
||
for _, reduce := range []bool{false, true} {
|
||
clk := newClock()
|
||
v := 0
|
||
tt := view(400, 100, clk, reduce, func(c *ui.Context) {
|
||
feedback.CountUp(c, v, feedback.CountUpOptions{Slot: pixel.Ink})
|
||
})
|
||
v = 800
|
||
wait(tt, clk, 0)
|
||
r := find(t, tt, "800")
|
||
mid := slots(t, tt.Image(), r)
|
||
wait(tt, clk, time.Second)
|
||
end := slots(t, tt.Image(), r)
|
||
if counted := !equal(mid, end); counted == reduce {
|
||
t.Errorf("reduce %v: counted in steps %v", reduce, counted)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCountUpReachesAHugeValue(t *testing.T) {
|
||
clk := newClock()
|
||
v := 0
|
||
tt := view(600, 100, clk, false, func(c *ui.Context) {
|
||
feedback.CountUp(c, v, feedback.CountUpOptions{Slot: pixel.Ink})
|
||
pixel.Text(c, strconv.Itoa(math.MaxInt64), pixel.TextOptions{})
|
||
})
|
||
v = math.MaxInt64
|
||
wait(tt, clk, 0)
|
||
wait(tt, clk, time.Second)
|
||
// The count ends on the same digits as plain text.
|
||
img := tt.Image()
|
||
if !equal(slots(t, img, ui.Rect{W: 600, H: 32}), slots(t, img, ui.Rect{Y: 32, W: 600, H: 32})) {
|
||
t.Fatal("the count ended on other digits")
|
||
}
|
||
// From -1, halfway is 4611686018427387903, across the sign.
|
||
half := -1
|
||
tt = view(600, 100, clk, false, func(c *ui.Context) {
|
||
feedback.CountUp(c, half, feedback.CountUpOptions{Slot: pixel.Ink})
|
||
pixel.Text(c, "4611686018427387903", pixel.TextOptions{})
|
||
})
|
||
half = math.MaxInt64
|
||
wait(tt, clk, 0)
|
||
wait(tt, clk, 4*time.Second/15+time.Millisecond)
|
||
img = tt.Image()
|
||
if !equal(slots(t, img, ui.Rect{W: 600, H: 32}), slots(t, img, ui.Rect{Y: 32, W: 600, H: 32})) {
|
||
t.Fatal("halfway the count drew other digits")
|
||
}
|
||
}
|
||
|
||
func TestPulseStopsUnderReduceMotion(t *testing.T) {
|
||
for _, reduce := range []bool{false, true} {
|
||
clk := newClock()
|
||
tt := view(300, 200, clk, reduce, func(c *ui.Context) {
|
||
feedback.Pulse(c, feedback.PulseOptions{}, func() {
|
||
ui.Box(c).Size(20, 20).Label("dot")
|
||
}).Label("ring")
|
||
})
|
||
if moved := moves(t, tt, clk, "ring", 200*time.Millisecond); moved == reduce {
|
||
t.Errorf("reduce %v: the ring moved %v", reduce, moved)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestLevelUpShowsOnlyOnARise(t *testing.T) {
|
||
clk := newClock()
|
||
lv := 4
|
||
tt := view(500, 200, clk, false, func(c *ui.Context) {
|
||
feedback.LevelUp(c, lv, feedback.LevelUpOptions{})
|
||
})
|
||
lv = 3
|
||
tt.Frame()
|
||
if _, ok := tt.Find("LEVEL UP! LV 3"); ok {
|
||
t.Fatal("LEVEL UP showed without a rise")
|
||
}
|
||
lv = 5
|
||
tt.Frame()
|
||
r := find(t, tt, "LEVEL UP! LV 5")
|
||
if count(slots(t, tt.Image(), r), pixel.Accent) != 0 {
|
||
t.Fatal("the banner did not start off its box")
|
||
}
|
||
wait(tt, clk, time.Second)
|
||
if count(slots(t, tt.Image(), r), pixel.Accent) == 0 {
|
||
t.Fatal("the banner did not slide in")
|
||
}
|
||
wait(tt, clk, 3*time.Second)
|
||
if _, ok := tt.Find("LEVEL UP! LV 5"); ok {
|
||
t.Fatal("the banner stayed")
|
||
}
|
||
}
|
||
|
||
func TestAchievementComesAndGoes(t *testing.T) {
|
||
clk := newClock()
|
||
cue := 0
|
||
tt := view(500, 200, clk, true, func(c *ui.Context) {
|
||
feedback.Achievement(c, "Slime Tamer", feedback.AchievementOptions{Cue: cue})
|
||
})
|
||
if _, ok := tt.Find("ACHIEVEMENT UNLOCKED Slime Tamer"); ok {
|
||
t.Fatal("the card showed before its cue")
|
||
}
|
||
cue++
|
||
tt.Frame()
|
||
find(t, tt, "ACHIEVEMENT UNLOCKED Slime Tamer")
|
||
wait(tt, clk, 3*time.Second)
|
||
find(t, tt, "ACHIEVEMENT UNLOCKED Slime Tamer")
|
||
wait(tt, clk, 2*time.Second)
|
||
if _, ok := tt.Find("ACHIEVEMENT UNLOCKED Slime Tamer"); ok {
|
||
t.Fatal("the card stayed")
|
||
}
|
||
}
|
||
|
||
func TestComboCounterPopsOnARise(t *testing.T) {
|
||
clk := newClock()
|
||
n := 0
|
||
tt := view(400, 200, clk, false, func(c *ui.Context) {
|
||
feedback.ComboCounter(c, n, feedback.ComboCounterOptions{})
|
||
})
|
||
if _, ok := tt.Find("×0 COMBO!"); ok {
|
||
t.Fatal("a zero combo showed")
|
||
}
|
||
n = 12
|
||
tt.Frame()
|
||
r := find(t, tt, "×12 COMBO!")
|
||
popped := count(slots(t, tt.Image(), r), pixel.Warn)
|
||
wait(tt, clk, 500*time.Millisecond)
|
||
if rest := count(slots(t, tt.Image(), r), pixel.Warn); rest >= popped {
|
||
t.Fatalf("no pop: %d Warn dots, then %d", popped, rest)
|
||
}
|
||
}
|
||
|
||
func TestSaveIndicatorStates(t *testing.T) {
|
||
saving := false
|
||
tt := view(300, 100, newClock(), false, func(c *ui.Context) {
|
||
feedback.SaveIndicator(c, saving, feedback.SaveIndicatorOptions{})
|
||
})
|
||
if _, ok := tt.Find("SAVED"); ok {
|
||
t.Fatal("SAVED before any save")
|
||
}
|
||
saving = true
|
||
tt.Frame()
|
||
find(t, tt, "SAVING…")
|
||
saving = false
|
||
tt.Frame()
|
||
find(t, tt, "SAVED")
|
||
}
|
||
|
||
func TestBusyOverlayBlocksInputAndGivesFocusBack(t *testing.T) {
|
||
busy, clicks, raw := false, 0, true
|
||
tt := view(400, 200, newClock(), false, func(c *ui.Context) {
|
||
input.Button(c, "top", input.ButtonOptions{})
|
||
feedback.BusyOverlay(c, busy, feedback.BusyOverlayOptions{}, func() {
|
||
if input.Button(c, "SAVE", input.ButtonOptions{}).Clicked {
|
||
clicks++
|
||
}
|
||
// Plain boxes, not library controls.
|
||
if ui.Box(c).Size(40, 20).Label("raw").Clicked() {
|
||
clicks++
|
||
}
|
||
if raw && ui.Box(c).Size(40, 20).Label("rawkey").Focusable().Shortcut(0, ui.KeyEnter) {
|
||
clicks++
|
||
}
|
||
})
|
||
ui.Box(c).Size(4, 4).Label("elsewhere")
|
||
input.Button(c, "after", input.ButtonOptions{})
|
||
})
|
||
// Focused counts ancestors; a sibling tells the two apart.
|
||
own := func() bool { return tt.Focused("SAVE") && !tt.Focused("elsewhere") }
|
||
click(t, tt, "rawkey")
|
||
busy = true
|
||
tt.Frame()
|
||
tt.Key(0, ui.KeyEnter)
|
||
if clicks != 0 || tt.Focused("rawkey") && !tt.Focused("elsewhere") {
|
||
t.Fatal("a plain focusable inside kept the focus or its keys")
|
||
}
|
||
busy, raw = false, false
|
||
tt.Frame()
|
||
click(t, tt, "SAVE")
|
||
busy = true
|
||
tt.Frame()
|
||
tt.Key(0, ui.KeyEnter)
|
||
// Focus waits on the veil; Tab goes on from it.
|
||
tt.Key(0, ui.KeyTab)
|
||
if !tt.Focused("after") || tt.Focused("elsewhere") {
|
||
t.Fatal("Tab did not pass over the busy content")
|
||
}
|
||
// Presses land on the veil, which keeps the focus.
|
||
click(t, tt, "SAVE")
|
||
click(t, tt, "raw")
|
||
if clicks != 1 || own() {
|
||
t.Fatalf("busy content took input: %d clicks, focus %v", clicks, own())
|
||
}
|
||
busy = false
|
||
tt.Frame()
|
||
if !own() {
|
||
t.Fatal("the focus did not come back")
|
||
}
|
||
tt.Key(0, ui.KeyEnter)
|
||
if clicks != 2 {
|
||
t.Fatal("the content stayed blocked")
|
||
}
|
||
}
|
||
|
||
func TestBusyContentIsOneTabStop(t *testing.T) {
|
||
tt := view(400, 200, newClock(), false, func(c *ui.Context) {
|
||
input.Button(c, "before", input.ButtonOptions{})
|
||
feedback.BusyOverlay(c, true, feedback.BusyOverlayOptions{}, func() {
|
||
ui.Box(c).Size(40, 20).Label("raw").Focusable()
|
||
input.Button(c, "SAVE", input.ButtonOptions{})
|
||
})
|
||
input.Button(c, "after", input.ButtonOptions{})
|
||
})
|
||
click(t, tt, "before")
|
||
tt.Key(0, ui.KeyTab)
|
||
// FocusMark rings the veil two dots outside it.
|
||
v := find(t, tt, "PLEASE WAIT")
|
||
if count(slots(t, tt.Image(), ui.Rect{X: v.X, Y: v.Y + v.H + 2, W: v.W, H: 2}), pixel.Accent) == 0 {
|
||
t.Fatal("the focused veil showed no focus")
|
||
}
|
||
tt.Key(0, ui.KeyTab)
|
||
if !tt.Focused("after") || tt.Focused("before") {
|
||
t.Fatal("two Tabs did not cross the busy content")
|
||
}
|
||
tt.Key(ui.Shift, ui.KeyTab)
|
||
tt.Key(ui.Shift, ui.KeyTab)
|
||
if !tt.Focused("before") || tt.Focused("after") {
|
||
t.Fatal("two Shift+Tabs did not cross back")
|
||
}
|
||
}
|
||
|
||
func TestReadyGoCountsAndReportsDone(t *testing.T) {
|
||
clk := newClock()
|
||
cue, done := 0, 0
|
||
tt := view(400, 300, clk, true, func(c *ui.Context) {
|
||
if feedback.ReadyGo(c, feedback.ReadyGoOptions{Cue: cue}).Done {
|
||
done++
|
||
}
|
||
})
|
||
cue++
|
||
tt.Frame()
|
||
for _, want := range []string{"3", "2", "1", "GO!"} {
|
||
find(t, tt, want)
|
||
wait(tt, clk, 500*time.Millisecond)
|
||
wait(tt, clk, 500*time.Millisecond)
|
||
}
|
||
if _, ok := tt.Find("GO!"); ok || done != 1 {
|
||
t.Fatalf("GO! stayed, or Done came %d times", done)
|
||
}
|
||
}
|
||
|
||
func TestReadyGoReportsDoneAfterAJump(t *testing.T) {
|
||
clk := newClock()
|
||
cue, done := 0, 0
|
||
tt := view(400, 300, clk, false, func(c *ui.Context) {
|
||
if feedback.ReadyGo(c, feedback.ReadyGoOptions{Cue: cue}).Done {
|
||
done++
|
||
}
|
||
})
|
||
cue++
|
||
tt.Frame()
|
||
wait(tt, clk, 5*time.Second)
|
||
wait(tt, clk, time.Second)
|
||
if done != 1 {
|
||
t.Fatalf("a jump past GO! gave Done %d times", done)
|
||
}
|
||
}
|
||
|
||
func TestShakeSpendsCuesUnderReduceMotion(t *testing.T) {
|
||
clk, cue := newClock(), 0
|
||
tt := view(300, 100, clk, false, func(c *ui.Context) {
|
||
feedback.Shake(c, feedback.ShakeOptions{Cue: cue}, func() {
|
||
ui.Box(c).Size(20, 20).Label("box")
|
||
})
|
||
})
|
||
x := find(t, tt, "box").X
|
||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||
cue++
|
||
tt.Frame()
|
||
tt.SetPreferences(ui.Preferences{ReduceMotion: false, TextScale: 1})
|
||
tt.Frame()
|
||
if find(t, tt, "box").X != x {
|
||
t.Fatal("a cue spent under Reduce Motion shook later")
|
||
}
|
||
}
|
||
|
||
func TestShakeJoltsUnlessReduced(t *testing.T) {
|
||
for _, reduce := range []bool{false, true} {
|
||
clk := newClock()
|
||
cue := 0
|
||
tt := view(300, 100, clk, reduce, func(c *ui.Context) {
|
||
feedback.Shake(c, feedback.ShakeOptions{Cue: cue}, func() {
|
||
ui.Box(c).Size(20, 20).Label("box")
|
||
})
|
||
})
|
||
x := find(t, tt, "box").X
|
||
cue++
|
||
tt.Frame()
|
||
if jolted := find(t, tt, "box").X != x; jolted == reduce {
|
||
t.Errorf("reduce %v: jolted %v", reduce, jolted)
|
||
}
|
||
wait(tt, clk, time.Second)
|
||
if find(t, tt, "box").X != x {
|
||
t.Error("the box did not settle")
|
||
}
|
||
}
|
||
}
|