A cross-cutting pass, each fix with a test that failed first. Veils: MyGo still hands a key to a base that turned off this frame, so every element key now goes through pixel.Shortcut, which an archtest enforces. Input handlers record and the build applies them only while enabled; the editor turns away a veil-frame letter or copy; value bases get a copy and keep only enabled turns. Popups give a held focus back to their opener when the veil lifts. Motion: S5 is enforced at the clock. Every Tick, Since, Step, Toward and Shot runs at 4-15 fps; blinks keep 1 Hz on 4 fps steps, fast animations step at 15, and drawn time is quantized to frames. Clocks stepping back: Since holds at zero without overflow, and k.Elapsed moves a stored start to now, so nothing goes negative, runs backward or indexes out of range. Also: logs count typed text instead of holding it, gallery names use glyphs the font has, removable rows are keyed, components take their own Options (archtest), and sweeps over all 508 demos check veiled keys, clicks, wheels, the clock stepping back and names.
276 lines
7.7 KiB
Go
276 lines
7.7 KiB
Go
package navigation_test
|
|
|
|
import (
|
|
"image"
|
|
"slices"
|
|
"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/navigation"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
|
)
|
|
|
|
type clock struct{ t time.Time }
|
|
|
|
func (c *clock) now() time.Time { return c.t }
|
|
|
|
func newClock() *clock { return &clock{time.Unix(3_000_000, 0)} }
|
|
|
|
// 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 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)
|
|
}
|
|
}
|
|
|
|
func shows(tt *ui.Tester, label string) bool {
|
|
_, ok := tt.Find(label)
|
|
return ok
|
|
}
|
|
|
|
// 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 TestTabsChooseAndShowTheirPanel(t *testing.T) {
|
|
for _, side := range []bool{false, true} {
|
|
tab, changes := 0, 0
|
|
tt := view(500, 300, newClock(), false, func(c *ui.Context) {
|
|
names := []string{"Items", "Magic", "Status"}
|
|
panel := func() { pixel.Text(c, "panel "+names[tab], pixel.TextOptions{}) }
|
|
changed := false
|
|
if side {
|
|
changed = navigation.VerticalTabs(c, "Menu", names, &tab, navigation.VerticalTabsOptions{}, panel).Changed
|
|
} else {
|
|
changed = navigation.Tabs(c, "Menu", names, &tab, navigation.TabsOptions{}, panel).Changed
|
|
}
|
|
if changed {
|
|
changes++
|
|
}
|
|
})
|
|
click(t, tt, "Magic")
|
|
find(t, tt, "panel Magic")
|
|
// The panel's edge breaks open under the chosen tab only.
|
|
edge := func(l string) pixel.Slot {
|
|
r := find(t, tt, l)
|
|
if side {
|
|
return slots(t, tt.Image(), ui.Rect{X: r.X + r.W, Y: r.Y + r.H/2, W: 2, H: 2})[0]
|
|
}
|
|
return slots(t, tt.Image(), ui.Rect{X: r.X + r.W/2, Y: r.Y + r.H, W: 2, H: 2})[0]
|
|
}
|
|
if edge("Magic") == pixel.Edge || edge("Items") != pixel.Edge {
|
|
t.Fatalf("side %v: the chosen tab does not join the panel", side)
|
|
}
|
|
tt.Key(0, ui.KeyRight)
|
|
if tab != 2 || changes != 2 || !shows(tt, "panel Status") {
|
|
t.Fatalf("side %v: click and Right gave tab %d after %d changes", side, tab, changes)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSidebarChoosesAcrossSections(t *testing.T) {
|
|
sel := 0
|
|
tt := view(400, 300, newClock(), false, func(c *ui.Context) {
|
|
navigation.Sidebar(c, "Places", []navigation.SidebarSection{
|
|
{Title: "WORLD", Items: []navigation.SidebarItem{{Icon: icon.Home, Label: "Town"}, {Icon: icon.Map, Label: "Field"}}},
|
|
{Title: "MAIL", Items: []navigation.SidebarItem{{Icon: icon.Mail, Label: "Inbox", Badge: "3"}}},
|
|
}, &sel, navigation.SidebarOptions{})
|
|
})
|
|
click(t, tt, "Field")
|
|
tt.Key(0, ui.KeyDown)
|
|
if sel != 2 {
|
|
t.Fatalf("click and Down chose %d, want the next section's item", sel)
|
|
}
|
|
if count(slots(t, tt.Image(), find(t, tt, "Inbox")), pixel.Bad) == 0 {
|
|
t.Fatal("the badge does not show")
|
|
}
|
|
}
|
|
|
|
func TestBreadcrumbsLinkAllButTheLast(t *testing.T) {
|
|
clicked := -2
|
|
tt := view(400, 100, newClock(), false, func(c *ui.Context) {
|
|
if r := navigation.Breadcrumbs(c, "Path", []string{"World", "Cave", "B2"}, navigation.BreadcrumbsOptions{}); r.Clicked >= 0 {
|
|
clicked = r.Clicked
|
|
}
|
|
})
|
|
click(t, tt, "B2")
|
|
n := 0
|
|
for _, s := range tt.Texts() {
|
|
if s == "B2" {
|
|
n++
|
|
}
|
|
}
|
|
if clicked != -2 || n != 1 {
|
|
t.Fatalf("the last part linked, or shows %d times", n)
|
|
}
|
|
click(t, tt, "Cave")
|
|
if clicked != 1 {
|
|
t.Fatalf("Cave reported %d", clicked)
|
|
}
|
|
}
|
|
|
|
func TestPaginationAndDots(t *testing.T) {
|
|
page, dot := 5, 1
|
|
tt := view(600, 200, newClock(), false, func(c *ui.Context) {
|
|
navigation.Pagination(c, "Pages", &page, 9, navigation.PaginationOptions{})
|
|
navigation.PageDots(c, "Slides", &dot, 4, navigation.PageDotsOptions{})
|
|
})
|
|
for _, l := range []string{"1", "4", "5", "6", "9"} {
|
|
find(t, tt, l)
|
|
}
|
|
if shows(tt, "3") || shows(tt, "7") {
|
|
t.Fatal("pages far from the current one show")
|
|
}
|
|
click(t, tt, "6")
|
|
tt.Key(0, ui.KeySpace)
|
|
if page != 6 || !tt.Focused("6") {
|
|
t.Fatalf("the focus left the clicked page: page %d", page)
|
|
}
|
|
click(t, tt, "NEXT")
|
|
if page != 7 {
|
|
t.Fatalf("6 then NEXT gave %d", page)
|
|
}
|
|
page = 1
|
|
tt.Frame()
|
|
click(t, tt, "PREVIOUS")
|
|
if page != 1 {
|
|
t.Fatal("PREVIOUS moved past page 1")
|
|
}
|
|
// The click lands on the third of four dots.
|
|
click(t, tt, "Slides")
|
|
tt.Key(0, ui.KeyRight)
|
|
tt.Key(0, ui.KeyEnter)
|
|
if dot != 4 {
|
|
t.Fatalf("a click and Right gave dot %d", dot)
|
|
}
|
|
r := find(t, tt, "Slides")
|
|
tt.ClickAt(r.X+4, r.Y+r.H/2)
|
|
if dot != 1 {
|
|
t.Fatalf("a click on the first dot gave %d", dot)
|
|
}
|
|
}
|
|
|
|
func TestStepsWaveUnlessReduced(t *testing.T) {
|
|
for _, reduce := range []bool{false, true} {
|
|
clk := newClock()
|
|
tt := view(400, 100, clk, reduce, func(c *ui.Context) {
|
|
navigation.Steps(c, []string{"Gate", "Cave", "Peak"}, 1, navigation.StepsOptions{})
|
|
})
|
|
r := find(t, tt, "Gate, Cave, Peak")
|
|
before := slots(t, tt.Image(), r)
|
|
clk.t = clk.t.Add(300 * time.Millisecond)
|
|
tt.Frame()
|
|
if waved := !slices.Equal(before, slots(t, tt.Image(), r)); waved == reduce {
|
|
t.Errorf("reduce %v: the flag waved %v", reduce, waved)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestToolbarIsOneTabStop(t *testing.T) {
|
|
tt := view(400, 100, newClock(), false, func(c *ui.Context) {
|
|
input.Button(c, "before", input.ButtonOptions{})
|
|
navigation.Toolbar(c, "Tools", navigation.ToolbarOptions{}, func() {
|
|
input.IconButton(c, icon.Edit, "Pen", input.IconButtonOptions{})
|
|
input.IconButton(c, icon.Trash, "Erase", input.IconButtonOptions{})
|
|
})
|
|
input.Button(c, "after", input.ButtonOptions{})
|
|
})
|
|
click(t, tt, "before")
|
|
tt.Key(0, ui.KeyTab)
|
|
tt.Key(0, ui.KeyRight)
|
|
if !tt.Focused("Erase") {
|
|
t.Fatal("Right did not move along the toolbar")
|
|
}
|
|
tt.Key(0, ui.KeyTab)
|
|
if !tt.Focused("after") {
|
|
t.Fatal("Tab did not leave the toolbar")
|
|
}
|
|
}
|
|
|
|
func TestBackForwardWalksHistory(t *testing.T) {
|
|
r := ui.NewRouter("/a")
|
|
r.Push("/b")
|
|
tt := view(300, 100, newClock(), false, func(c *ui.Context) {
|
|
navigation.BackForward(c, r, navigation.BackForwardOptions{})
|
|
})
|
|
click(t, tt, "FORWARD")
|
|
click(t, tt, "BACK")
|
|
if r.Path() != "/a" {
|
|
t.Fatalf("BACK went to %q", r.Path())
|
|
}
|
|
click(t, tt, "BACK")
|
|
click(t, tt, "FORWARD")
|
|
if r.Path() != "/b" {
|
|
t.Fatalf("FORWARD went to %q", r.Path())
|
|
}
|
|
}
|
|
|
|
func TestTableOfContentsMarksTheCurrent(t *testing.T) {
|
|
cur := 0
|
|
tt := view(400, 200, newClock(), false, func(c *ui.Context) {
|
|
navigation.TableOfContents(c, "Manual", []navigation.TocItem{{Title: "Controls"}, {Title: "Jumping", Level: 1}, {Title: "Bosses"}}, &cur, navigation.TableOfContentsOptions{})
|
|
})
|
|
click(t, tt, "Jumping")
|
|
tt.Key(0, ui.KeyDown)
|
|
if cur != 2 {
|
|
t.Fatalf("click and Down gave %d", cur)
|
|
}
|
|
// The marker sits left of the title's indent.
|
|
b := find(t, tt, "Bosses")
|
|
if count(slots(t, tt.Image(), ui.Rect{X: b.X, Y: b.Y, W: 12, H: b.H}), pixel.Accent) == 0 {
|
|
t.Fatal("no ▶ at the current section")
|
|
}
|
|
}
|