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.
398 lines
13 KiB
Go
398 lines
13 KiB
Go
package overlay_test
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/egoist/mygo/ui"
|
||
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/display"
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon"
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/overlay"
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||
)
|
||
|
||
func TestDrawerSlidesInInSteps(t *testing.T) {
|
||
clk, open, closed := newClock(), false, 0
|
||
side := display.Right
|
||
tt := view(640, 400, clk, false, func(c *ui.Context) {
|
||
if input.Button(c, "MENU", input.ButtonOptions{}).Clicked {
|
||
open = true
|
||
}
|
||
if overlay.Drawer(c, "Menu", &open, overlay.DrawerOptions{Side: side, Size: 100}, func() {
|
||
pixel.Text(c, "Items", pixel.TextOptions{})
|
||
}).Closed {
|
||
closed++
|
||
}
|
||
})
|
||
click(t, tt, "MENU")
|
||
var xs []float32
|
||
for range 5 {
|
||
xs = append(xs, find(t, tt, "Items").X)
|
||
clk.t = clk.t.Add(70 * time.Millisecond)
|
||
tt.Frame()
|
||
}
|
||
// Four 50-pixel steps, then rest at 640-200+16.
|
||
want := []float32{456 + 200, 456 + 150, 456 + 100, 456 + 50, 456}
|
||
for i := range want {
|
||
if xs[i] != want[i] {
|
||
t.Fatalf("x by step %v, want %v", xs, want)
|
||
}
|
||
}
|
||
keeps(t, tt.Image())
|
||
tt.Key(0, ui.KeyEscape)
|
||
if open || closed != 1 {
|
||
t.Fatal("Escape left it open")
|
||
}
|
||
side, open = display.Left, true
|
||
tt.Frame()
|
||
clk.t = clk.t.Add(time.Second)
|
||
tt.Frame()
|
||
if x := find(t, tt, "Items").X; x != 16 {
|
||
t.Fatalf("a left drawer opened at %v", x)
|
||
}
|
||
}
|
||
|
||
func TestDrawerOpenedUnderReduceMotionNeverSlides(t *testing.T) {
|
||
clk, open := newClock(), false
|
||
tt := view(640, 400, clk, false, func(c *ui.Context) {
|
||
overlay.Drawer(c, "Menu", &open, overlay.DrawerOptions{Side: display.Right, Size: 100}, func() {
|
||
pixel.Text(c, "Items", pixel.TextOptions{})
|
||
})
|
||
})
|
||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||
open = true
|
||
// At rest the panel's text sits at 640-200+16.
|
||
for _, reduce := range []bool{true, false} {
|
||
tt.SetPreferences(ui.Preferences{ReduceMotion: reduce, TextScale: 1})
|
||
tt.Frame()
|
||
if x := find(t, tt, "Items").X; x != 456 {
|
||
t.Fatalf("reduce %v: the drawer's text is at %v", reduce, x)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestSheetClosesOnALongPull(t *testing.T) {
|
||
open, closed := true, 0
|
||
tt := view(640, 400, newClock(), false, func(c *ui.Context) {
|
||
if overlay.Sheet(c, "Options", &open, overlay.SheetOptions{Height: 120}, func() {
|
||
pixel.Text(c, "Sound: ON", pixel.TextOptions{})
|
||
}).Closed {
|
||
closed++
|
||
}
|
||
})
|
||
// The bar tops the sheet, which is 240 pixels.
|
||
top := float32(400 - 240)
|
||
tt.Press(320, top+8)
|
||
tt.Move(320, top+40)
|
||
tt.Release(320, top+40)
|
||
if !open || find(t, tt, "Sound: ON").Y > top+40 {
|
||
t.Fatal("a short pull closed or moved the sheet")
|
||
}
|
||
tt.Press(320, top+8)
|
||
tt.Move(320, top+60)
|
||
tt.Move(320, top+120)
|
||
tt.Release(320, top+120)
|
||
if open || closed != 1 {
|
||
t.Fatalf("a long pull left open %v", open)
|
||
}
|
||
}
|
||
|
||
func TestLightboxTurnsWrapsAndCloses(t *testing.T) {
|
||
open, at, closed := true, 0, 0
|
||
tt := view(640, 400, newClock(), false, func(c *ui.Context) {
|
||
if overlay.Lightbox(c, "Gallery", 3, &at, &open, overlay.LightboxOptions{}, func(i int) {
|
||
pixel.Text(c, []string{"Castle", "Cave", "Coast"}[i], pixel.TextOptions{})
|
||
}).Closed {
|
||
closed++
|
||
}
|
||
})
|
||
tt.Key(0, ui.KeyLeft)
|
||
if at != 2 || !shows(tt, "Coast") || !shows(tt, "3 / 3") {
|
||
t.Fatalf("Left from the first went to %d", at)
|
||
}
|
||
click(t, tt, "NEXT")
|
||
tt.Key(0, ui.KeyRight)
|
||
if at != 1 || !shows(tt, "Cave") {
|
||
t.Fatalf("▶, Right went to %d", at)
|
||
}
|
||
keeps(t, tt.Image())
|
||
click(t, tt, "CLOSE")
|
||
if open || closed != 1 {
|
||
t.Fatal("× did not close it")
|
||
}
|
||
}
|
||
|
||
func TestDialogueBoxTypesThenTurns(t *testing.T) {
|
||
clk, page, done := newClock(), 0, 0
|
||
pages := []string{"It is dangerous to go alone!", "Take this."}
|
||
tt := view(640, 400, clk, false, func(c *ui.Context) {
|
||
if overlay.DialogueBox(c, pages, &page, overlay.DialogueBoxOptions{Name: "OLD MAN", Portrait: icon.User}).Done {
|
||
done++
|
||
}
|
||
})
|
||
r := find(t, tt, "OLD MAN "+pages[0])
|
||
tt.ClickAt(r.X+4, r.Y+4)
|
||
if page != 0 {
|
||
t.Fatal("the first press turned the page before the text was out")
|
||
}
|
||
tt.Key(0, ui.KeyEnter)
|
||
if page != 1 || !shows(tt, "OLD MAN Take this.") {
|
||
t.Fatalf("Enter after the text left page %d", page)
|
||
}
|
||
clk.t = clk.t.Add(2 * time.Second)
|
||
tt.Frame()
|
||
tt.Key(0, ui.KeyEnter)
|
||
if done != 1 || !shows(tt, "OLD MAN") {
|
||
t.Fatalf("Enter on the last page: done %d", done)
|
||
}
|
||
keeps(t, tt.Image())
|
||
}
|
||
|
||
func TestDialogueBoxUnderReduceMotionTurnsAtOnce(t *testing.T) {
|
||
page := 0
|
||
tt := view(640, 400, newClock(), false, func(c *ui.Context) {
|
||
overlay.DialogueBox(c, []string{"One.", "Two.", "Three."}, &page, overlay.DialogueBoxOptions{})
|
||
})
|
||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||
click(t, tt, "One.")
|
||
tt.Key(0, ui.KeyEnter)
|
||
if page != 2 {
|
||
t.Fatalf("two presses under Reduce Motion reached page %d", page)
|
||
}
|
||
}
|
||
|
||
func TestNotificationsExpireAndDismiss(t *testing.T) {
|
||
clk := newClock()
|
||
queue := []overlay.Notification{{Title: "Saved", Text: "Slot 1."}, {Icon: icon.Mail, Title: "Letter", Text: "From mom.", For: 10 * time.Second}}
|
||
var dismissed string
|
||
tt := view(640, 400, clk, false, func(c *ui.Context) {
|
||
if r := overlay.Notifications(c, &queue, overlay.NotificationsOptions{}); r.Dismissed != nil {
|
||
dismissed = r.Dismissed.Title
|
||
}
|
||
})
|
||
a, b := find(t, tt, "Saved"), find(t, tt, "Letter")
|
||
if a.X+a.W != 640-16 || b.Y <= a.Y {
|
||
t.Fatalf("cards at %v and %v", a, b)
|
||
}
|
||
clk.t = clk.t.Add(6 * time.Second)
|
||
tt.Frame()
|
||
if shows(tt, "Saved") || len(queue) != 1 {
|
||
t.Fatalf("the first card outlived five seconds: %d left", len(queue))
|
||
}
|
||
click(t, tt, "CLOSE")
|
||
if dismissed != "Letter" || len(queue) != 0 {
|
||
t.Fatalf("× dismissed %q", dismissed)
|
||
}
|
||
}
|
||
|
||
func TestKeyHintsDrawPadButtonsAndKeycaps(t *testing.T) {
|
||
tt := view(640, 100, newClock(), false, func(c *ui.Context) {
|
||
overlay.KeyHints(c, []overlay.KeyHint{{Key: "A", Action: "CONFIRM"}, {Key: "START", Action: "MENU"}}, overlay.KeyHintsOptions{})
|
||
})
|
||
r := find(t, tt, "A CONFIRM")
|
||
if s := slot(t, tt.Image(), int(r.X)+18, int(r.Y)+r2i(r.H/2)); s != pixel.Ink {
|
||
t.Fatalf("pad button is %v", s)
|
||
}
|
||
if !shows(tt, "START") || !shows(tt, "MENU") {
|
||
t.Fatalf("texts %q", tt.Texts())
|
||
}
|
||
keeps(t, tt.Image())
|
||
}
|
||
|
||
func r2i(f float32) int { return int(f) }
|
||
|
||
func TestTitleCardDithersInHoldsAndGoes(t *testing.T) {
|
||
clk, cue := newClock(), 0
|
||
tt := view(320, 200, clk, false, func(c *ui.Context) {
|
||
ui.Box(c).Size(300, 160).Children(func() {
|
||
overlay.TitleCard(c, "WORLD 1-1", overlay.TitleCardOptions{Cue: cue, Hold: time.Second})
|
||
})
|
||
})
|
||
area := ui.Rect{X: 0, Y: 0, W: 300, H: 160}
|
||
if n := count(t, tt.Image(), area, pixel.Ink); n != 0 || shows(tt, "WORLD 1-1") {
|
||
t.Fatal("the card showed before its cue")
|
||
}
|
||
cue = 1
|
||
tt.Frame()
|
||
var inks []int
|
||
for range 4 {
|
||
inks = append(inks, count(t, tt.Image(), area, pixel.Ink))
|
||
clk.t = clk.t.Add(time.Second / 8)
|
||
tt.Frame()
|
||
}
|
||
if !(inks[0] < inks[1] && inks[1] < inks[2] && inks[2] < inks[3]) || !shows(tt, "WORLD 1-1") {
|
||
t.Fatalf("ink by step %v", inks)
|
||
}
|
||
clk.t = clk.t.Add(2 * time.Second)
|
||
tt.Frame()
|
||
if shows(tt, "WORLD 1-1") || count(t, tt.Image(), area, pixel.Ink) != 0 {
|
||
t.Fatal("the card stayed past its hold")
|
||
}
|
||
}
|
||
|
||
func TestOffDialogueBoxStaysPut(t *testing.T) {
|
||
for _, inert := range []bool{false, true} {
|
||
page := 0
|
||
tt := view(640, 400, newClock(), inert, func(c *ui.Context) {
|
||
overlay.DialogueBox(c, []string{"One.", "Two."}, &page, overlay.DialogueBoxOptions{Disabled: !inert})
|
||
})
|
||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||
click(t, tt, "One.")
|
||
click(t, tt, "One.")
|
||
tt.Key(0, ui.KeyEnter)
|
||
if page != 0 {
|
||
t.Fatalf("inert %v: an off box turned to page %d", inert, page)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestSheetVanishesOnTheReleaseThatClosesIt(t *testing.T) {
|
||
open := true
|
||
tt := view(640, 400, newClock(), false, func(c *ui.Context) {
|
||
overlay.Sheet(c, "Options", &open, overlay.SheetOptions{Height: 120}, func() {
|
||
pixel.Text(c, "Sound: ON", pixel.TextOptions{})
|
||
})
|
||
})
|
||
top := float32(400 - 240)
|
||
tt.Press(320, top+8)
|
||
tt.Move(320, top+120)
|
||
tt.Release(320, top+120)
|
||
if open || shows(tt, "Sound: ON") {
|
||
t.Fatal("the sheet stayed on screen after the pull closed it")
|
||
}
|
||
}
|
||
|
||
func TestPanelsWithoutControlsTakeTheFocus(t *testing.T) {
|
||
for _, kind := range []string{"drawer", "sheet", "dialog", "alert", "confirm", "choice", "pause", "prompt", "progress"} {
|
||
open, opened := false, 0
|
||
tt := view(640, 400, newClock(), false, func(c *ui.Context) {
|
||
if input.Button(c, "OPEN", input.ButtonOptions{}).Clicked {
|
||
open = true
|
||
opened++
|
||
}
|
||
body := func() { pixel.Text(c, "Just words.", pixel.TextOptions{}) }
|
||
name := "Menu"
|
||
switch kind {
|
||
case "drawer":
|
||
overlay.Drawer(c, name, &open, overlay.DrawerOptions{Side: display.Left}, body)
|
||
case "sheet":
|
||
overlay.Sheet(c, name, &open, overlay.SheetOptions{}, body)
|
||
case "dialog":
|
||
overlay.Dialog(c, name, &open, overlay.DialogOptions{}, body)
|
||
case "alert":
|
||
overlay.AlertDialog(c, name, "Hi.", &open, overlay.AlertDialogOptions{Disabled: true})
|
||
case "confirm":
|
||
overlay.ConfirmDialog(c, "Hi.", &open, overlay.ConfirmDialogOptions{Disabled: true})
|
||
case "choice":
|
||
overlay.ChoicePrompt(c, name, []string{"A"}, &open, overlay.ChoicePromptOptions{Disabled: true})
|
||
case "pause":
|
||
overlay.PauseMenu(c, &open, overlay.PauseMenuOptions{Disabled: true})
|
||
case "prompt":
|
||
s := ""
|
||
overlay.PromptDialog(c, name, &s, &open, overlay.PromptDialogOptions{Disabled: true})
|
||
case "progress":
|
||
overlay.ProgressDialog(c, name, 0.5, &open, overlay.ProgressDialogOptions{Disabled: true})
|
||
}
|
||
})
|
||
click(t, tt, "OPEN")
|
||
tt.Key(0, ui.KeyEnter)
|
||
if opened != 1 || tt.Focused("OPEN") {
|
||
t.Fatalf("%s: Enter reached the opener behind it (%d opens)", kind, opened)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestOffSheetTakesNoPull(t *testing.T) {
|
||
for _, inert := range []bool{false, true} {
|
||
open := true
|
||
tt := view(640, 400, newClock(), inert, func(c *ui.Context) {
|
||
overlay.Sheet(c, "Options", &open, overlay.SheetOptions{Height: 120, Disabled: !inert}, func() {})
|
||
})
|
||
top := float32(400 - 240)
|
||
tt.Press(320, top+8)
|
||
tt.Move(320, top+120)
|
||
tt.Release(320, top+120)
|
||
if !open {
|
||
t.Fatalf("inert %v: a pull closed an off sheet", inert)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestDisabledPanelsTakeNoClicks(t *testing.T) {
|
||
open, at, saving, next := true, 0, true, 0
|
||
queue := []overlay.Notification{{Title: "Saved", Text: "Slot 1."}}
|
||
tt := view(640, 400, newClock(), false, func(c *ui.Context) {
|
||
overlay.Lightbox(c, "Gallery", 3, &at, &open, overlay.LightboxOptions{Disabled: true}, func(int) {})
|
||
})
|
||
tt.Key(0, ui.KeyRight)
|
||
click(t, tt, "NEXT")
|
||
click(t, tt, "CLOSE")
|
||
tt = view(640, 400, newClock(), false, func(c *ui.Context) {
|
||
overlay.ProgressDialog(c, "Saving", 0.2, &saving, overlay.ProgressDialogOptions{Disabled: true})
|
||
})
|
||
click(t, tt, "CANCEL")
|
||
tt = view(640, 400, newClock(), false, func(c *ui.Context) {
|
||
ui.Column(c).Padding(40).Children(func() {
|
||
b := input.Button(c, "BAG", input.ButtonOptions{})
|
||
if overlay.Coachmark(c, b.Element, "Bag.", 1, 2, overlay.CoachmarkOptions{Disabled: true}).Next {
|
||
next++
|
||
}
|
||
})
|
||
overlay.Notifications(c, &queue, overlay.NotificationsOptions{Disabled: true})
|
||
})
|
||
click(t, tt, "NEXT ▶")
|
||
click(t, tt, "CLOSE")
|
||
if at != 0 || !open || !saving || next != 0 || len(queue) != 1 {
|
||
t.Fatalf("disabled: at %d open %v saving %v next %d queue %d", at, open, saving, next, len(queue))
|
||
}
|
||
}
|
||
|
||
func TestDialogueBoxKeepsItsSpeedOnS5Steps(t *testing.T) {
|
||
for _, wait := range []time.Duration{900 * time.Millisecond, 1050 * time.Millisecond} {
|
||
clk, page := newClock(), 0
|
||
// Thirty letters at the default thirty a second.
|
||
pages := []string{"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234", "NEXT"}
|
||
tt := view(640, 400, clk, false, func(c *ui.Context) {
|
||
overlay.DialogueBox(c, pages, &page, overlay.DialogueBoxOptions{})
|
||
})
|
||
clk.t = clk.t.Add(wait)
|
||
tt.Frame()
|
||
r := find(t, tt, pages[0])
|
||
tt.ClickAt(r.X+4, r.Y+4)
|
||
// A press on typed-out text turns the page.
|
||
if turned := page == 1; turned != (wait > time.Second) {
|
||
t.Errorf("after %v a press left page %d", wait, page)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestPausedBlinksOnceASecond(t *testing.T) {
|
||
clk, paused := newClock(), true
|
||
tt := view(640, 400, clk, false, func(c *ui.Context) { overlay.PauseMenu(c, &paused, overlay.PauseMenuOptions{}) })
|
||
var ink []int
|
||
for _, at := range []time.Duration{100, 350, 600} {
|
||
clk.t = time.Unix(3_000_000, 0).Add(at * time.Millisecond)
|
||
tt.Frame()
|
||
ink = append(ink, count(t, tt.Image(), find(t, tt, "PAUSED"), pixel.Back))
|
||
}
|
||
if ink[0] != ink[1] || ink[1] == ink[2] {
|
||
t.Fatalf("PAUSED ink at .1, .35 and .6 s: %v", ink)
|
||
}
|
||
}
|
||
|
||
func TestDialogueBoxSpeedHasACeiling(t *testing.T) {
|
||
defer func() {
|
||
if r := fmt.Sprint(recover()); !strings.HasPrefix(r, "overlay: DialogueBox CPS") {
|
||
t.Fatalf("CPS 1001 gave %q", r)
|
||
}
|
||
}()
|
||
page := 0
|
||
view(640, 400, newClock(), false, func(c *ui.Context) {
|
||
overlay.DialogueBox(c, []string{"HI"}, &page, overlay.DialogueBoxOptions{CPS: 1001})
|
||
})
|
||
}
|