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.
381 lines
10 KiB
Go
381 lines
10 KiB
Go
package pixel_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
|
)
|
|
|
|
type editHarness struct {
|
|
t *testing.T
|
|
tt *ui.Tester
|
|
value string
|
|
o pixel.EditorOptions
|
|
last pixel.Edit
|
|
changes, submits int
|
|
at time.Time
|
|
}
|
|
|
|
func newEdit(t *testing.T, v string, width float32, o pixel.EditorOptions) *editHarness {
|
|
t.Helper()
|
|
h := &editHarness{t: t, value: v, o: o}
|
|
if h.o.Label == "" {
|
|
h.o.Label = "field"
|
|
}
|
|
h.tt = ui.NewTester(func(c *ui.Context) {
|
|
pixel.Use(c, pixel.Settings{Palette: pixel.SummerDay, Dot: 2, Now: func() time.Time {
|
|
if h.at.IsZero() {
|
|
return time.Now()
|
|
}
|
|
return h.at
|
|
}})
|
|
ui.Column(c).Width(width).Padding(4).Children(func() {
|
|
h.last = pixel.Editor(c, &h.value, h.o)
|
|
if h.last.Changed {
|
|
h.changes++
|
|
}
|
|
if h.last.Submitted {
|
|
h.submits++
|
|
}
|
|
})
|
|
}, 300, 200)
|
|
return h
|
|
}
|
|
|
|
func (h *editHarness) focus() {
|
|
h.t.Helper()
|
|
if err := h.tt.Click(h.o.Label); err != nil {
|
|
h.t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func (h *editHarness) want(v string) {
|
|
h.t.Helper()
|
|
if h.value != v {
|
|
h.t.Fatalf("value %q, want %q", h.value, v)
|
|
}
|
|
}
|
|
|
|
func TestEditorTypingAndKeys(t *testing.T) {
|
|
h := newEdit(t, "", 200, pixel.EditorOptions{})
|
|
h.focus()
|
|
h.tt.Type("hello")
|
|
h.want("hello")
|
|
if h.changes == 0 {
|
|
t.Fatal("typing reported no change")
|
|
}
|
|
h.tt.Key(0, ui.KeyLeft)
|
|
h.tt.Key(0, ui.KeyLeft)
|
|
h.tt.Type("X")
|
|
h.want("helXlo")
|
|
h.tt.Key(0, ui.KeyBackspace)
|
|
h.tt.Key(0, ui.KeyDelete)
|
|
h.want("helo")
|
|
h.tt.Key(0, ui.KeyHome)
|
|
h.tt.Type("<")
|
|
h.tt.Key(0, ui.KeyEnd)
|
|
h.tt.Type(">")
|
|
h.want("<helo>")
|
|
h.tt.Key(ui.Shift, ui.KeyLeft)
|
|
h.tt.Key(ui.Shift, ui.KeyLeft)
|
|
h.tt.Type("!")
|
|
h.want("<hel!")
|
|
h.tt.Key(0, ui.KeyEnter)
|
|
if h.submits != 1 {
|
|
t.Fatalf("Enter submitted %d times", h.submits)
|
|
}
|
|
h.want("<hel!")
|
|
}
|
|
|
|
func TestEditorWordsClipboardUndo(t *testing.T) {
|
|
h := newEdit(t, "foo bar baz", 200, pixel.EditorOptions{})
|
|
h.focus()
|
|
h.tt.Key(0, ui.KeyEnd)
|
|
h.tt.Key(ui.Alt, ui.KeyLeft)
|
|
h.tt.Type("X")
|
|
h.want("foo bar Xbaz")
|
|
h.tt.Key(ui.Cmd, ui.KeyZ)
|
|
h.want("foo bar baz")
|
|
h.tt.Key(ui.Cmd|ui.Shift, ui.KeyZ)
|
|
h.want("foo bar Xbaz")
|
|
h.tt.Key(ui.Alt, ui.KeyBackspace)
|
|
h.want("foo bar baz")
|
|
h.tt.Key(ui.Cmd, ui.KeyA)
|
|
h.tt.Key(ui.Cmd, ui.KeyC)
|
|
if h.tt.Clipboard() != "foo bar baz" {
|
|
t.Fatalf("clipboard %q", h.tt.Clipboard())
|
|
}
|
|
h.tt.Key(0, ui.KeyRight)
|
|
h.tt.Key(ui.Cmd, ui.KeyV)
|
|
h.want("foo bar bazfoo bar baz")
|
|
h.tt.Command("selectAll")
|
|
h.tt.Command("cut")
|
|
h.want("")
|
|
if h.tt.Clipboard() != "foo bar bazfoo bar baz" {
|
|
t.Fatalf("cut put %q on the clipboard", h.tt.Clipboard())
|
|
}
|
|
h.tt.SetClipboard("a\tb\nc")
|
|
h.tt.Command("paste")
|
|
h.want("a b c")
|
|
}
|
|
|
|
func TestEditorMultiline(t *testing.T) {
|
|
h := newEdit(t, "ab\ncd", 200, pixel.EditorOptions{Multiline: true})
|
|
h.focus()
|
|
h.tt.Key(0, ui.KeyUp)
|
|
h.tt.Type("X")
|
|
h.want("abX\ncd")
|
|
if h.last.Line != 0 {
|
|
t.Fatalf("caret line %d on the first line", h.last.Line)
|
|
}
|
|
h.tt.Key(0, ui.KeyEnter)
|
|
h.want("abX\n\ncd")
|
|
if h.last.Line != 1 {
|
|
t.Fatalf("caret line %d after a newline", h.last.Line)
|
|
}
|
|
h.tt.Key(ui.Cmd, ui.KeyEnter)
|
|
if h.submits != 1 {
|
|
t.Fatalf("Cmd+Enter submitted %d times", h.submits)
|
|
}
|
|
if r, _ := h.tt.Find("field"); r.H != 3*16*2 {
|
|
t.Fatalf("three lines are %v high, want 96", r.H)
|
|
}
|
|
}
|
|
|
|
func TestEditorEnterSends(t *testing.T) {
|
|
h := newEdit(t, "hi", 200, pixel.EditorOptions{Multiline: true, EnterSends: true})
|
|
h.focus()
|
|
h.tt.Key(ui.Shift, ui.KeyEnter)
|
|
h.tt.Type("yo")
|
|
h.want("hi\nyo")
|
|
h.tt.Key(0, ui.KeyEnter)
|
|
if h.submits != 1 {
|
|
t.Fatalf("Enter submitted %d times", h.submits)
|
|
}
|
|
h.want("hi\nyo")
|
|
mustPanic(t, "one line", func() { newEdit(t, "", 200, pixel.EditorOptions{EnterSends: true}).tt.Frame() })
|
|
}
|
|
|
|
func TestEditorLimitsAndMask(t *testing.T) {
|
|
h := newEdit(t, "", 200, pixel.EditorOptions{MaxLength: 5})
|
|
h.focus()
|
|
h.tt.Type("abcdefg")
|
|
h.want("abcde")
|
|
m := newEdit(t, "secret", 200, pixel.EditorOptions{Mask: '★'})
|
|
m.focus()
|
|
m.tt.Key(ui.Cmd, ui.KeyA)
|
|
m.tt.Key(ui.Cmd, ui.KeyC)
|
|
m.tt.Key(ui.Cmd, ui.KeyX)
|
|
if m.tt.Clipboard() != "" {
|
|
t.Fatalf("masked text reached the clipboard: %q", m.tt.Clipboard())
|
|
}
|
|
m.want("secret")
|
|
}
|
|
|
|
func TestEditorComposition(t *testing.T) {
|
|
h := newEdit(t, "ab", 200, pixel.EditorOptions{})
|
|
h.focus()
|
|
h.tt.Compose("ni", 2)
|
|
h.want("ab")
|
|
if r, ok := h.tt.TextCaret(); !ok || r.H != 32 {
|
|
t.Fatalf("caret %v active %v", r, ok)
|
|
}
|
|
h.tt.Compose("", 0)
|
|
h.tt.Type("你")
|
|
h.want("ab你")
|
|
h.tt.Key(ui.Cmd, ui.KeyA)
|
|
h.tt.Compose("x", 1)
|
|
h.want("")
|
|
}
|
|
|
|
func TestEditorPointer(t *testing.T) {
|
|
h := newEdit(t, "hello world", 300, pixel.EditorOptions{})
|
|
r, _ := h.tt.Find("field")
|
|
x := r.X + float32(pixel.Measure("hello ")+4)*2
|
|
for range 2 {
|
|
h.tt.Press(x, r.Y+8)
|
|
h.tt.Release(x, r.Y+8)
|
|
}
|
|
h.tt.Type("there")
|
|
h.want("hello there")
|
|
h.tt.Press(r.X+1, r.Y+8)
|
|
h.tt.Move(r.X+r.W-1, r.Y+8)
|
|
h.tt.Release(r.X+r.W-1, r.Y+8)
|
|
h.tt.Type("Z")
|
|
h.want("Z")
|
|
}
|
|
|
|
func TestEditorExternalChangeAndDisabled(t *testing.T) {
|
|
h := newEdit(t, "start", 200, pixel.EditorOptions{})
|
|
h.focus()
|
|
h.value = "reset"
|
|
h.tt.Frame()
|
|
h.tt.Type("!")
|
|
h.want("reset!")
|
|
d := newEdit(t, "frozen", 200, pixel.EditorOptions{Disabled: true})
|
|
d.focus()
|
|
d.tt.Type("x")
|
|
d.tt.Key(0, ui.KeyBackspace)
|
|
d.want("frozen")
|
|
if d.tt.Focused("field") {
|
|
t.Fatal("disabled editor took the focus")
|
|
}
|
|
}
|
|
|
|
func TestEditorKeepsCaretInView(t *testing.T) {
|
|
h := newEdit(t, "", 48, pixel.EditorOptions{})
|
|
h.focus()
|
|
h.tt.Type("a long line that cannot fit in a narrow field")
|
|
field, _ := h.tt.Find("field")
|
|
caret, _ := h.tt.TextCaret()
|
|
if caret.X < field.X || caret.X+caret.W > field.X+field.W {
|
|
t.Fatalf("caret %v outside field %v", caret, field)
|
|
}
|
|
}
|
|
|
|
func TestEditorCaretBlinks(t *testing.T) {
|
|
h := newEdit(t, "pixel", 200, pixel.EditorOptions{})
|
|
h.at = time.Unix(1000, 0)
|
|
h.focus()
|
|
h.tt.Key(0, ui.KeyEnd)
|
|
r, _ := h.tt.Find("field")
|
|
x, y := int(r.X/2)+pixel.Measure("pixel"), int(r.Y/2)
|
|
caret := func() int { return dots{t: t, img: h.tt.Image(), pal: pixel.SummerDay}.count(x, y+1, 1, 14, pixel.Ink) }
|
|
if caret() != 14 {
|
|
t.Fatal("caret not drawn after the text")
|
|
}
|
|
h.at = time.Unix(1000, 500_000_000)
|
|
h.tt.Frame()
|
|
if caret() != 0 {
|
|
t.Fatal("caret did not blink off")
|
|
}
|
|
h.tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
|
if caret() != 14 {
|
|
t.Fatal("caret hidden under Reduce Motion")
|
|
}
|
|
}
|
|
|
|
func TestMultilineCaretInsideField(t *testing.T) {
|
|
h := newEdit(t, "ab", float32(pixel.Measure("ab")*2+8), pixel.EditorOptions{Multiline: true})
|
|
h.focus()
|
|
h.tt.Key(ui.Ctrl, ui.KeyEnd)
|
|
field, _ := h.tt.Find("field")
|
|
caret, _ := h.tt.TextCaret()
|
|
if caret.X+caret.W > field.X+field.W || caret.Y+caret.H > field.Y+field.H {
|
|
t.Fatalf("caret %v outside field %v", caret, field)
|
|
}
|
|
}
|
|
|
|
func TestEditorDrawsCaretAndSelection(t *testing.T) {
|
|
h := newEdit(t, "pixel", 200, pixel.EditorOptions{})
|
|
h.focus()
|
|
d := dots{t: t, img: h.tt.Image(), pal: pixel.SummerDay}
|
|
r, _ := h.tt.Find("field")
|
|
x0, y0, w := int(r.X/2), int(r.Y/2), int(r.W/2)
|
|
if d.count(x0, y0, w, 16, pixel.Ink) == 0 {
|
|
t.Fatal("no text drawn")
|
|
}
|
|
if d.count(x0, y0, w, 16, pixel.Accent) != 0 {
|
|
t.Fatal("selection drawn without a selection")
|
|
}
|
|
h.tt.Key(ui.Cmd, ui.KeyA)
|
|
d = dots{t: t, img: h.tt.Image(), pal: pixel.SummerDay}
|
|
if d.count(x0, y0, pixel.Measure("pixel"), 16, pixel.Accent) == 0 || d.count(x0, y0, w, 16, pixel.AccentInk) == 0 {
|
|
t.Fatal("selection not drawn in Accent with AccentInk text")
|
|
}
|
|
}
|
|
|
|
func TestEditorOwnsTypingKeys(t *testing.T) {
|
|
value, shortcuts := "", 0
|
|
tt := ui.NewTester(func(c *ui.Context) {
|
|
pixel.Use(c, pixel.Settings{Palette: pixel.SummerDay, Dot: 2})
|
|
pixel.Editor(c, &value, pixel.EditorOptions{Label: "field"})
|
|
if pixel.Typing(c) {
|
|
return
|
|
}
|
|
for _, key := range []ui.Key{ui.KeyP, ui.KeySlash, ui.KeyBracketRight, ui.KeySpace} {
|
|
if c.Shortcut(0, key) {
|
|
shortcuts++
|
|
}
|
|
}
|
|
}, 300, 100)
|
|
if err := tt.Click("field"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tt.TypeKey(0, ui.KeyP, "p")
|
|
tt.TypeKey(0, ui.KeySlash, "/")
|
|
tt.TypeKey(0, ui.KeyBracketRight, "]")
|
|
tt.TypeKey(0, ui.KeySpace, " ")
|
|
if value != "p/] " || shortcuts != 0 {
|
|
t.Fatalf("value %q, shortcuts fired %d times", value, shortcuts)
|
|
}
|
|
tt.ClickAt(150, 90)
|
|
tt.Key(0, ui.KeyP)
|
|
if shortcuts != 1 {
|
|
t.Fatalf("after leaving the field, P fired %d shortcuts", shortcuts)
|
|
}
|
|
}
|
|
|
|
func TestEditorSpaceDoesNotScroll(t *testing.T) {
|
|
value, scroll := "", ui.ScrollState{}
|
|
tt := ui.NewTester(func(c *ui.Context) {
|
|
pixel.Use(c, pixel.Settings{Palette: pixel.SummerDay, Dot: 2})
|
|
ui.Scroll(c).Height(100).TrackScroll(&scroll).Children(func() {
|
|
pixel.Editor(c, &value, pixel.EditorOptions{Label: "field"})
|
|
ui.Box(c).Height(1000)
|
|
})
|
|
}, 300, 100)
|
|
if err := tt.Click("field"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tt.TypeKey(0, ui.KeySpace, " ")
|
|
if value != " " || scroll.Y != 0 {
|
|
t.Fatalf("value %q, scrolled to %v", value, scroll.Y)
|
|
}
|
|
}
|
|
|
|
func TestEditorTakesAValueChangedBetweenFrames(t *testing.T) {
|
|
h := newEdit(t, "help", 200, pixel.EditorOptions{})
|
|
h.focus()
|
|
// The caller clears it; a key lands before a frame.
|
|
h.value = ""
|
|
h.tt.Type("x")
|
|
h.want("x")
|
|
}
|
|
|
|
func TestEditorVeilKeepsCallersAndClipboard(t *testing.T) {
|
|
// The caller writes during the build the veil falls in.
|
|
v, inert, update := "old", false, false
|
|
tt := ui.NewTester(func(c *ui.Context) {
|
|
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Inert: inert})
|
|
if update {
|
|
v, update = "server", false
|
|
}
|
|
pixel.Editor(c, &v, pixel.EditorOptions{Label: "Edit"}).Element.Width(200)
|
|
}, 400, 100)
|
|
tt.Key(0, ui.KeyTab)
|
|
inert, update = true, true
|
|
tt.Type("x")
|
|
if v != "server" {
|
|
t.Fatalf("the caller's write became %q", v)
|
|
}
|
|
for _, command := range []string{"copy", "cut"} {
|
|
v, inert := "SecretValue", false
|
|
tt := ui.NewTester(func(c *ui.Context) {
|
|
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Inert: inert})
|
|
pixel.Editor(c, &v, pixel.EditorOptions{Label: "Edit"}).Element.Width(200)
|
|
}, 400, 100)
|
|
tt.Key(0, ui.KeyTab)
|
|
tt.Key(ui.Cmd, ui.KeyA)
|
|
tt.SetClipboard("original")
|
|
inert = true
|
|
tt.Command(command)
|
|
if got := tt.Clipboard(); got != "original" || v != "SecretValue" {
|
|
t.Errorf("%s as the veil fell: clipboard %q, value %q", command, got, v)
|
|
}
|
|
}
|
|
}
|