Files
8bitMyGoUI/navigation/navigation_test.go
T

275 lines
7.6 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) {
build := navigation.Tabs
if side {
build = navigation.VerticalTabs
}
names := []string{"Items", "Magic", "Status"}
if build(c, "Menu", names, &tab, navigation.TabsOptions{}, func() {
pixel.Text(c, "panel "+names[tab], pixel.TextOptions{})
}).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")
}
}