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

+180
View File
@@ -0,0 +1,180 @@
package feedback_test
import (
"image"
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/feedback"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
type clock struct{ t time.Time }
func (c *clock) now() time.Time { return c.t }
// view builds on a fake clock; reduce stops motion.
func view(w, h int, clk *clock, reduce bool, build func(c *ui.Context)) *ui.Tester {
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: clk.now})
build(c)
}, w, h)
if reduce {
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
}
return tt
}
func newClock() *clock { return &clock{time.Unix(3_000_000, 0)} }
// wait moves the clock on and builds a frame.
func wait(tt *ui.Tester, clk *clock, d time.Duration) {
clk.t = clk.t.Add(d)
tt.Frame()
}
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
}
func count(s []pixel.Slot, want pixel.Slot) int {
n := 0
for _, v := range s {
if v == want {
n++
}
}
return n
}
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
}
// moves reports whether the labeled box changes as time passes.
func moves(t *testing.T, tt *ui.Tester, clk *clock, label string, d time.Duration) bool {
t.Helper()
before := slots(t, tt.Image(), find(t, tt, label))
wait(tt, clk, d)
return !equal(before, slots(t, tt.Image(), find(t, tt, label)))
}
func TestProgressFillsChunksAndRuns(t *testing.T) {
clk := newClock()
v := 0.5
tt := view(400, 200, clk, true, func(c *ui.Context) {
feedback.Progress(c, "Load", v, feedback.ProgressOptions{Width: 103, Percent: true})
})
// 103 dots hold 20 chunks of 4×4 dots.
if n := count(slots(t, tt.Image(), find(t, tt, "Load")), pixel.Accent); n != 10*4*4 {
t.Fatalf("half a bar lit %d dots, want %d", n, 10*4*4)
}
v = -1
tt.Frame()
if !moves(t, tt, clk, "Load", 200*time.Millisecond) {
t.Fatal("an unknown progress stood still under Reduce Motion")
}
defer func() {
if recover() == nil {
t.Fatal("a value past 1 did not panic")
}
}()
v = 1.5
tt.Frame()
}
func TestLoadersKeepMovingUnderReduceMotion(t *testing.T) {
clk := newClock()
tt := view(400, 300, clk, true, func(c *ui.Context) {
feedback.Spinner(c, "Spin", feedback.SpinnerOptions{Scale: 2})
feedback.DotsLoader(c, "Dots", feedback.DotsLoaderOptions{})
feedback.BarsLoader(c, "Bars", feedback.BarsLoaderOptions{})
feedback.Hourglass(c, "Sand", feedback.HourglassOptions{})
})
for _, l := range []string{"Spin", "Dots", "Bars", "Sand"} {
if !moves(t, tt, clk, l, 300*time.Millisecond) {
t.Errorf("%s stood still", l)
}
}
}
func TestMeterLevels(t *testing.T) {
for v, want := range map[float64]pixel.Slot{0.3: pixel.Good, 0.7: pixel.Warn, 0.95: pixel.Bad} {
tt := view(300, 100, newClock(), false, func(c *ui.Context) {
feedback.Meter(c, "HP", v, feedback.MeterOptions{Warning: 0.6, Critical: 0.9})
})
s := slots(t, tt.Image(), find(t, tt, "HP"))
if count(s, want) == 0 {
t.Errorf("%v shows no %v", v, want)
}
}
}
func TestSkeletonShineStopsUnderReduceMotion(t *testing.T) {
for _, reduce := range []bool{false, true} {
clk := newClock()
tt := view(400, 200, clk, reduce, func(c *ui.Context) {
feedback.Skeleton(c, feedback.SkeletonOptions{Avatar: true})
})
if moved := moves(t, tt, clk, "NOW LOADING", 250*time.Millisecond); moved == reduce {
t.Errorf("reduce %v: the shine moved %v", reduce, moved)
}
}
}
func TestLoadingScreenRotatesTips(t *testing.T) {
clk := newClock()
tt := view(600, 300, clk, false, func(c *ui.Context) {
feedback.LoadingScreen(c, feedback.LoadingScreenOptions{Tips: []string{"Save often.", "Pet the slime."}})
})
r := find(t, tt, "NOW LOADING")
tips := ui.Rect{X: r.X, Y: r.Y + 2*2*pixel.LineHeight + 16, W: r.W, H: 2 * 2 * pixel.LineHeight}
before := slots(t, tt.Image(), tips)
wait(tt, clk, 4*time.Second)
if equal(before, slots(t, tt.Image(), tips)) {
t.Fatal("the tip did not change after four seconds")
}
}