A terminal with history, a Go-colored code block, a log viewer that follows, a hex viewer, a memory map, a JSON tree, a line diff, a GAME OVER crash screen, an FPS meter, profiler bars, breakpoints, variables, command history, a shortcut list, a debug grid, a pixel editor that exports string art, a palette editor, a sprite sheet, a glyph map and a 6502 register view. layout.ScrollArea takes Follow, and ChatLog now uses it. data.TreeNode takes a Value drawn in its Slot. pixel gains Has, and Sprite.Art writes MustSprite art. pixel.Editor takes the caller's value at each input event, so a value changed between frames is never typed onto.
207 lines
7.6 KiB
Go
207 lines
7.6 KiB
Go
package code_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/code"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
|
)
|
|
|
|
func TestLogViewerFiltersByLevel(t *testing.T) {
|
|
wd := newWorld()
|
|
at := wd.clk.t
|
|
lines := []code.LogLine{{At: at, Level: code.Debug, Text: "d"}, {At: at, Level: code.Info, Text: "i"}, {At: at, Level: code.Error, Text: "e"}}
|
|
least, changed := code.Info, false
|
|
tt := view(600, 300, wd, func(c *ui.Context) {
|
|
changed = code.LogViewer(c, "Log", lines, &least, code.LogViewerOptions{}).Changed || changed
|
|
})
|
|
if tt.HasText("10:30:00 DEBUG d") || !tt.HasText("10:30:00 INFO i") {
|
|
t.Fatalf("INFO and up shows %q", tt.Texts())
|
|
}
|
|
x, y, w, h := dots(find(t, tt, "10:30:00 ERROR e"))
|
|
if count(tt.Image(), x, y, w, h, pixel.Bad) == 0 {
|
|
t.Fatal("an ERROR line is not Bad")
|
|
}
|
|
click(t, tt, "ERROR")
|
|
if least != code.Error || !changed || tt.HasText("10:30:00 INFO i") {
|
|
t.Fatalf("ERROR filter: least %v, changed %v, %q", least, changed, tt.Texts())
|
|
}
|
|
mustPanic(t, "a level past ERROR", func() {
|
|
view(600, 300, newWorld(), func(c *ui.Context) {
|
|
l := code.Level(4)
|
|
code.LogViewer(c, "Log", nil, &l, code.LogViewerOptions{})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestHexViewerWalksBytes(t *testing.T) {
|
|
wd := newWorld()
|
|
data := []byte("HELLO\x00WORLD\x00\x01\x02\x03\x04\xff\xfe\x00\x10")
|
|
cur, changed := 0, false
|
|
tt := view(600, 300, wd, func(c *ui.Context) {
|
|
changed = code.HexViewer(c, "Bytes", data, &cur, code.HexViewerOptions{}).Changed || changed
|
|
})
|
|
r := find(t, tt, "Bytes")
|
|
x, y, _, _ := dots(r)
|
|
img := tt.Image()
|
|
// The cursor's cell is Accent; a zero byte is Faint.
|
|
if at(img, x+1, y+2) != pixel.Accent || count(img, x+5*12, y, 11, 12, pixel.Faint) == 0 || count(img, x+12, y, 11, 12, pixel.Faint) != 0 {
|
|
t.Fatal("cursor or zero bytes are not marked")
|
|
}
|
|
click(t, tt, "Bytes")
|
|
tt.Key(0, ui.KeyHome)
|
|
tt.Key(0, ui.KeyRight)
|
|
tt.Key(0, ui.KeyDown)
|
|
if cur != 9 || !changed {
|
|
t.Fatalf("Right, Down: cursor %d, changed %v", cur, changed)
|
|
}
|
|
tt.Key(0, ui.KeyEnd)
|
|
if cur != len(data)-1 {
|
|
t.Fatalf("End: cursor %d", cur)
|
|
}
|
|
for name, cols := range map[string]int{"3 a row": 3, "17 a row": 17} {
|
|
mustPanic(t, name, func() {
|
|
view(600, 300, newWorld(), func(c *ui.Context) {
|
|
n := 0
|
|
code.HexViewer(c, "B", data, &n, code.HexViewerOptions{Columns: cols})
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMemoryMapFillsByUse(t *testing.T) {
|
|
wd := newWorld()
|
|
tt := view(600, 300, wd, func(c *ui.Context) {
|
|
code.MemoryMap(c, "Memory", []code.Bank{{Name: "ROM0", Used: 15, Size: 16}, {Name: "ROM1", Used: 8, Size: 16}, {Name: "WRAM", Used: 3, Size: 16}, {Name: "OAM", Size: 16}}, code.MemoryMapOptions{})
|
|
})
|
|
for i, tc := range []struct {
|
|
label string
|
|
floor pixel.Slot
|
|
}{{"ROM0, 0 of 0 KB, 93%", pixel.Bad}, {"ROM1, 0 of 0 KB, 50%", pixel.Warn}, {"WRAM, 0 of 0 KB, 18%", pixel.Good}, {"OAM, 0 of 0 KB, 0%", pixel.Well}} {
|
|
x, y, _, _ := dots(find(t, tt, tc.label))
|
|
if got := at(tt.Image(), x+20, y+26); got != tc.floor {
|
|
t.Errorf("bank %d floor is %v, want %v", i, got, tc.floor)
|
|
}
|
|
}
|
|
for name, b := range map[string]code.Bank{"overfull": {Name: "A", Used: 2, Size: 1}, "empty": {Name: "A"}, "hanzi": {Name: "内存", Size: 1}} {
|
|
mustPanic(t, name, func() {
|
|
view(600, 300, newWorld(), func(c *ui.Context) { code.MemoryMap(c, "M", []code.Bank{b}, code.MemoryMapOptions{}) })
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJSONTreeOpensAndRejectsBadJSON(t *testing.T) {
|
|
wd := newWorld()
|
|
sel, toggled := "", false
|
|
tt := view(600, 300, wd, func(c *ui.Context) {
|
|
toggled = code.JSONTree(c, "Doc", `{"a": {"b": 1}, "c": "x"}`, &sel, code.JSONTreeOptions{}).Toggled || toggled
|
|
})
|
|
r := find(t, tt, "Doc")
|
|
if r.H != 2*2*pixel.LineHeight {
|
|
t.Fatalf("closed, the tree is %v tall", r.H)
|
|
}
|
|
click(t, tt, "Doc")
|
|
tt.Key(0, ui.KeyHome)
|
|
tt.Key(0, ui.KeyUp)
|
|
tt.Key(0, ui.KeyRight)
|
|
if r := find(t, tt, "Doc"); !toggled || r.H != 3*2*pixel.LineHeight {
|
|
t.Fatalf("Right did not open a: toggled %v, %v tall", toggled, r.H)
|
|
}
|
|
for name, doc := range map[string]string{"broken": `{"a": }`, "two values": `{} {}`, "a stray brace": `{}}`} {
|
|
mustPanic(t, name, func() {
|
|
view(600, 300, newWorld(), func(c *ui.Context) {
|
|
s := ""
|
|
code.JSONTree(c, "J", doc, &s, code.JSONTreeOptions{})
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDiffViewSignsAndColors(t *testing.T) {
|
|
wd := newWorld()
|
|
tt := view(600, 300, wd, func(c *ui.Context) {
|
|
code.DiffView(c, "Diff", "a = 1\nb = 2", "a = 1\nb = 3\nc = 4", code.DiffViewOptions{})
|
|
})
|
|
find(t, tt, " a = 1")
|
|
for label, s := range map[string]pixel.Slot{"- b = 2": pixel.Bad, "+ b = 3": pixel.Good, "+ c = 4": pixel.Good} {
|
|
x, y, w, h := dots(find(t, tt, label))
|
|
if at(tt.Image(), x, y+2) != s || count(tt.Image(), x+32, y, w-32, h, s) == 0 {
|
|
t.Errorf("%q has no %v bar and text", label, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrashScreenBlinksAndContinues(t *testing.T) {
|
|
for _, reduce := range []bool{false, true} {
|
|
wd := newWorld()
|
|
wd.reduce = reduce
|
|
cont := false
|
|
tt := view(600, 300, wd, func(c *ui.Context) {
|
|
cont = code.CrashScreen(c, "panic: oops", []string{"main.main()"}, code.CrashScreenOptions{}).Continue || cont
|
|
})
|
|
find(t, tt, "panic: oops")
|
|
find(t, tt, " main.main()")
|
|
// The group is named by its title and found first.
|
|
x, y, _, _ := dots(find(t, tt, "GAME OVER"))
|
|
corner := func() pixel.Slot { return at(tt.Image(), x, y) }
|
|
if corner() != pixel.Bad {
|
|
t.Fatal("the frame is not lit at first")
|
|
}
|
|
later(tt, wd, time.Second/2+time.Millisecond)
|
|
if lit := corner() == pixel.Bad; lit != reduce {
|
|
t.Fatalf("reduce %v: lit %v half a second on", reduce, lit)
|
|
}
|
|
click(t, tt, "CONTINUE?")
|
|
if !cont {
|
|
t.Fatal("CONTINUE did not continue")
|
|
}
|
|
}
|
|
mustPanic(t, "no error", func() {
|
|
view(600, 300, newWorld(), func(c *ui.Context) { code.CrashScreen(c, "", nil, code.CrashScreenOptions{}) })
|
|
})
|
|
}
|
|
|
|
func TestFPSMeterKeepsTheNewestAndColorsByTime(t *testing.T) {
|
|
wd := newWorld()
|
|
ms := time.Millisecond
|
|
// Twenty dots hold eight bars: the first two scroll off.
|
|
frames := []time.Duration{20 * ms, 20 * ms, 40 * ms, 10 * ms, 10 * ms, 10 * ms, 10 * ms, 10 * ms, 10 * ms, 20 * ms}
|
|
tt := view(600, 300, wd, func(c *ui.Context) {
|
|
code.FPSMeter(c, "Frames", frames, code.FPSMeterOptions{Width: 20, Height: 30})
|
|
})
|
|
x, y, _, _ := dots(find(t, tt, "Frames"))
|
|
bar := func(i int) pixel.Slot { return at(tt.Image(), x+2+2*i, y+8+30-3) }
|
|
if bar(0) != pixel.Bad || bar(1) != pixel.Good || bar(7) != pixel.Warn {
|
|
t.Fatalf("bars %v %v %v", bar(0), bar(1), bar(7))
|
|
}
|
|
mustPanic(t, "a negative frame", func() {
|
|
view(600, 300, newWorld(), func(c *ui.Context) {
|
|
code.FPSMeter(c, "F", []time.Duration{-1}, code.FPSMeterOptions{})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestProfilerBarsPlaceSpansByTime(t *testing.T) {
|
|
wd := newWorld()
|
|
ms := time.Millisecond
|
|
root := code.Span{Name: "frame", Length: 10 * ms, Children: []code.Span{{Name: "a", Length: 5 * ms}, {Name: "b", Start: 5 * ms, Length: 5 * ms, Children: []code.Span{{Name: "c", Start: 5 * ms, Length: ms}}}}}
|
|
tt := view(600, 300, wd, func(c *ui.Context) {
|
|
code.ProfilerBars(c, "Frame", root, code.ProfilerBarsOptions{Width: 100})
|
|
})
|
|
x, y, _, h := dots(find(t, tt, "Frame"))
|
|
img := tt.Image()
|
|
row := func(r int) int { return y + r*pixel.LineHeight + 6 }
|
|
if h != 3*pixel.LineHeight || at(img, x+99, row(0)) != pixel.Accent || at(img, x+30, row(1)) != pixel.Info ||
|
|
at(img, x+52, row(2)) != pixel.Good || at(img, x+80, row(2)) == pixel.Good {
|
|
t.Fatal("bars are not where their spans are")
|
|
}
|
|
mustPanic(t, "a child past its parent", func() {
|
|
view(600, 300, newWorld(), func(c *ui.Context) {
|
|
code.ProfilerBars(c, "F", code.Span{Length: ms, Children: []code.Span{{Start: ms / 2, Length: ms}}}, code.ProfilerBarsOptions{})
|
|
})
|
|
})
|
|
}
|