Add feedback components 106-130; inert scope, focus return, two-step deploy

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 20:58:30 -04:00
1 parent bce416182e
commit bd3877e1b1
87 files changed
+2492 -147

No files matched your search

+269
View File
@@ -0,0 +1,269 @@
package feedback_test
import (
"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 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 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")
}
}
}