447 lines
12 KiB
Go
447 lines
12 KiB
Go
package text_test
|
|
|
|
import (
|
|
"image"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/text"
|
|
)
|
|
|
|
type clock struct{ t time.Time }
|
|
|
|
func (c *clock) now() time.Time { return c.t }
|
|
|
|
func (c *clock) add(d time.Duration) { c.t = c.t.Add(d) }
|
|
|
|
func view(w, h int, clk *clock, build func(c *ui.Context)) *ui.Tester {
|
|
return ui.NewTester(func(c *ui.Context) {
|
|
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: clk.now})
|
|
build(c)
|
|
}, w, h)
|
|
}
|
|
|
|
func newClock() *clock { return &clock{time.Unix(1_000_000, 0)} }
|
|
|
|
// slot is the palette slot of dot x, y.
|
|
func slot(t *testing.T, img *image.RGBA, x, y int) pixel.Slot {
|
|
t.Helper()
|
|
c := img.RGBAAt(2*x, 2*y)
|
|
for s, pc := range pixel.SpringDay.Colors {
|
|
if pc.R == c.R && pc.G == c.G && pc.B == c.B {
|
|
return pixel.Slot(s)
|
|
}
|
|
}
|
|
t.Fatalf("dot %d,%d off the palette: %v", x, y, c)
|
|
return 0
|
|
}
|
|
|
|
// count is how many dots in a box hold s.
|
|
func count(t *testing.T, img *image.RGBA, x, y, w, h int, s pixel.Slot) int {
|
|
n := 0
|
|
for j := range h {
|
|
for i := range w {
|
|
if slot(t, img, x+i, y+j) == s {
|
|
n++
|
|
}
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
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 TestHeadingLevels(t *testing.T) {
|
|
tt := view(600, 300, newClock(), func(c *ui.Context) {
|
|
ui.Column(c).Children(func() {
|
|
text.Heading(c, "BIG", text.HeadingOptions{})
|
|
text.Heading(c, "small", text.HeadingOptions{Level: 3})
|
|
})
|
|
})
|
|
if r := find(t, tt, "BIG"); r.W != float32((pixel.Measure("BIG")*3+1)*2) || r.H != float32((pixel.LineHeight*3+1)*2) {
|
|
t.Fatalf("level 1 box %v", r)
|
|
}
|
|
if r := find(t, tt, "small"); r.H != float32((pixel.LineHeight+1)*2) {
|
|
t.Fatalf("level 3 box %v", r)
|
|
}
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatal("level 4 did not panic")
|
|
}
|
|
}()
|
|
view(100, 100, newClock(), func(c *ui.Context) { text.Heading(c, "x", text.HeadingOptions{Level: 4}) })
|
|
}
|
|
|
|
func TestParagraphAlignAndLimit(t *testing.T) {
|
|
words := "one two three four five six seven eight nine ten"
|
|
tt := view(400, 300, newClock(), func(c *ui.Context) {
|
|
ui.Column(c).Width(200).Children(func() {
|
|
text.Paragraph(c, words, text.ParagraphOptions{MaxLines: 1})
|
|
text.Paragraph(c, "hi", text.ParagraphOptions{Align: text.Center})
|
|
text.Paragraph(c, "hi", text.ParagraphOptions{Align: text.Right})
|
|
})
|
|
})
|
|
img := tt.Image()
|
|
if r := find(t, tt, words); r.H != pixel.LineHeight*2 {
|
|
t.Fatalf("MaxLines 1 paragraph is %v DIPs tall", r.H)
|
|
}
|
|
left := func(row int) int {
|
|
for x := range 100 {
|
|
for y := row; y < row+pixel.LineHeight; y++ {
|
|
if slot(t, img, x, y) == pixel.Ink {
|
|
return x
|
|
}
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
w := pixel.Measure("hi")
|
|
if c, r := left(16), left(32); c < (100-w)/2 || c > (100-w)/2+2 || r < 100-w || r > 100-w+2 {
|
|
t.Fatalf("centered ink at %d, right ink at %d, for width %d", c, r, w)
|
|
}
|
|
}
|
|
|
|
func TestTypewriterGlyphs(t *testing.T) {
|
|
clk := newClock()
|
|
s, done := "AAAA\nBBBB", 0
|
|
tt := view(200, 100, clk, func(c *ui.Context) {
|
|
ui.Box(c).Width(float32(2 * (4*pixel.Measure("A") + 8))).Children(func() {
|
|
if text.Typewriter(c, s, text.TypewriterOptions{CPS: 10}).Done {
|
|
done++
|
|
}
|
|
})
|
|
})
|
|
w := 4 * pixel.Measure("A")
|
|
ink := func(y int) int { return count(t, tt.Image(), 0, y, w, pixel.LineHeight, pixel.Ink) }
|
|
clk.add(400 * time.Millisecond)
|
|
tt.Frame()
|
|
if ink(0) == 0 || ink(pixel.LineHeight) != 0 || done != 0 {
|
|
t.Fatalf("at 4 of 8 glyphs: line inks %d, %d, done %d", ink(0), ink(pixel.LineHeight), done)
|
|
}
|
|
clk.add(300 * time.Millisecond)
|
|
tt.Frame()
|
|
if ink(pixel.LineHeight) == 0 || done != 0 {
|
|
t.Fatalf("at 7 of 8 glyphs: second line ink %d, done %d", ink(pixel.LineHeight), done)
|
|
}
|
|
clk.add(100 * time.Millisecond)
|
|
tt.Frame()
|
|
if done != 1 {
|
|
t.Fatal("the eighth glyph did not finish the text")
|
|
}
|
|
// A box-wide line would push the mark out.
|
|
s = "AAAAA"
|
|
tt.Frame()
|
|
if err := tt.Click(s); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
img := tt.Image()
|
|
if count(t, img, 0, 0, w+8, 3*pixel.LineHeight, pixel.Accent) == 0 {
|
|
clk.add(500 * time.Millisecond)
|
|
tt.Frame()
|
|
img = tt.Image()
|
|
}
|
|
if count(t, img, 0, 0, w+8, 3*pixel.LineHeight, pixel.Accent) == 0 {
|
|
t.Fatal("the done mark never showed")
|
|
}
|
|
if n := count(t, img, w+8, 0, 20, 3*pixel.LineHeight, pixel.Accent); n != 0 {
|
|
t.Fatalf("the done mark left the box: %d dots outside", n)
|
|
}
|
|
}
|
|
|
|
func TestTypewriter(t *testing.T) {
|
|
clk := newClock()
|
|
s, done := "ABCDEFGHIJ", 0
|
|
opts := text.TypewriterOptions{CPS: 10}
|
|
tt := view(400, 200, clk, func(c *ui.Context) {
|
|
if text.Typewriter(c, s, opts).Done {
|
|
done++
|
|
}
|
|
})
|
|
clk.add(500 * time.Millisecond)
|
|
tt.Frame()
|
|
if done != 0 {
|
|
t.Fatal("half the time showed the whole text")
|
|
}
|
|
clk.add(600 * time.Millisecond)
|
|
tt.Frame()
|
|
if done != 1 {
|
|
t.Fatalf("after the whole time done fired %d times", done)
|
|
}
|
|
s = "NEW LINE"
|
|
tt.Frame()
|
|
if err := tt.Click(s); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if done != 2 {
|
|
t.Fatal("a click did not complete a new line")
|
|
}
|
|
s = "THIRD"
|
|
tt.Frame()
|
|
tt.Key(0, ui.KeyEnter)
|
|
if done != 3 {
|
|
t.Fatal("Enter did not complete the line")
|
|
}
|
|
s, opts.Disabled = "FOURTH", true
|
|
tt.Frame()
|
|
_ = tt.Click(s)
|
|
if done != 3 {
|
|
t.Fatal("a disabled typewriter completed on a click")
|
|
}
|
|
s, opts.Disabled = "FIFTH", false
|
|
tt.SetPreferences(ui.Preferences{ReduceMotion: true})
|
|
if done != 4 {
|
|
t.Fatal("Reduce Motion did not show the whole line at once")
|
|
}
|
|
}
|
|
|
|
func TestMarqueeScrollsAndPauses(t *testing.T) {
|
|
clk := newClock()
|
|
tt := view(400, 100, clk, func(c *ui.Context) {
|
|
text.Marquee(c, "HELLO", text.MarqueeOptions{Width: 60})
|
|
})
|
|
// last is the rightmost ink column inside the window.
|
|
last := func() int {
|
|
img := tt.Image()
|
|
if count(t, img, 60, 0, 140, pixel.LineHeight, pixel.Ink) != 0 {
|
|
t.Fatal("the marquee drew outside its window")
|
|
}
|
|
for x := 59; x >= 0; x-- {
|
|
if count(t, img, x, 0, 1, pixel.LineHeight, pixel.Ink) > 0 {
|
|
return x
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
r0 := last()
|
|
clk.add(500 * time.Millisecond)
|
|
tt.Frame()
|
|
r1 := last()
|
|
tt.Move(10, 10)
|
|
clk.add(500 * time.Millisecond)
|
|
tt.Frame()
|
|
r2 := last()
|
|
tt.Move(300, 90)
|
|
tt.Frame()
|
|
clk.add(250 * time.Millisecond)
|
|
tt.Frame()
|
|
if r3 := last(); r0 < 0 || r0-r1 != 10 || r2 != r1 || r1-r3 != 5 {
|
|
t.Fatalf("ends at %d, %d (10 steps), %d (hovered), %d (5 more)", r0, r1, r2, r3)
|
|
}
|
|
tt.SetPreferences(ui.Preferences{ReduceMotion: true})
|
|
clk.add(time.Second)
|
|
tt.Frame()
|
|
if last() != r0 {
|
|
t.Fatal("under Reduce Motion the text is not shown at rest")
|
|
}
|
|
}
|
|
|
|
func TestBlinkUnderReduceMotion(t *testing.T) {
|
|
clk := newClock()
|
|
tt := view(200, 100, clk, func(c *ui.Context) { text.BlinkText(c, "ON", text.BlinkTextOptions{}) })
|
|
ink := func() int { return count(t, tt.Image(), 0, 0, 30, pixel.LineHeight, pixel.Ink) }
|
|
if ink() == 0 {
|
|
t.Fatal("step 0 hid the text")
|
|
}
|
|
clk.add(500 * time.Millisecond)
|
|
tt.Frame()
|
|
if ink() != 0 {
|
|
t.Fatal("step 1 showed the text")
|
|
}
|
|
tt.SetPreferences(ui.Preferences{ReduceMotion: true})
|
|
if ink() == 0 {
|
|
t.Fatal("Reduce Motion hid the text")
|
|
}
|
|
}
|
|
|
|
func TestLinkOpens(t *testing.T) {
|
|
opts := text.LinkOptions{}
|
|
clicks := 0
|
|
tt := view(300, 100, newClock(), func(c *ui.Context) {
|
|
if text.Link(c, "Docs", "https://example.com/docs", opts).Clicked {
|
|
clicks++
|
|
}
|
|
})
|
|
if err := tt.Click("Docs"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tt.Key(0, ui.KeyEnter)
|
|
if clicks != 2 || len(tt.OpenedURLs()) != 2 || tt.OpenedURLs()[0] != "https://example.com/docs" {
|
|
t.Fatalf("clicks %d, opened %q", clicks, tt.OpenedURLs())
|
|
}
|
|
opts.Disabled = true
|
|
tt.Frame()
|
|
_ = tt.Click("Docs")
|
|
if len(tt.OpenedURLs()) != 2 {
|
|
t.Fatal("a disabled link opened")
|
|
}
|
|
}
|
|
|
|
func TestSevenSegmentLightsSegments(t *testing.T) {
|
|
tt := view(200, 100, newClock(), func(c *ui.Context) {
|
|
text.SevenSegment(c, "1", text.SevenSegmentOptions{})
|
|
})
|
|
img := tt.Image()
|
|
if count(t, img, 6, 1, 1, 4, pixel.Bad) != 4 || count(t, img, 6, 6, 1, 4, pixel.Bad) != 4 {
|
|
t.Fatal("1 did not light b and c")
|
|
}
|
|
if count(t, img, 1, 0, 5, 1, pixel.Bad) != 0 || count(t, img, 1, 0, 5, 1, pixel.Faint) == 0 {
|
|
t.Fatal("1 lit a, or left no ghost")
|
|
}
|
|
}
|
|
|
|
func TestOdometerAndMicroPanics(t *testing.T) {
|
|
for name, build := range map[string]func(c *ui.Context){
|
|
"negative": func(c *ui.Context) { text.Odometer(c, -1, text.OdometerOptions{}) },
|
|
"overflow": func(c *ui.Context) { text.Odometer(c, 1000, text.OdometerOptions{Digits: 3}) },
|
|
"micro rune": func(c *ui.Context) { text.MicroText(c, "é", text.MicroTextOptions{}) },
|
|
"big scale": func(c *ui.Context) { text.ShadowText(c, "x", text.ShadowTextOptions{Scale: 5}) },
|
|
"seven rune": func(c *ui.Context) { text.SevenSegment(c, "x", text.SevenSegmentOptions{}) },
|
|
"paragraph 9": func(c *ui.Context) { text.Paragraph(c, "x", text.ParagraphOptions{Align: 9}) },
|
|
} {
|
|
func() {
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Errorf("%s did not panic", name)
|
|
}
|
|
}()
|
|
view(200, 100, newClock(), build)
|
|
}()
|
|
}
|
|
}
|
|
|
|
func TestOdometerRollsToValue(t *testing.T) {
|
|
clk := newClock()
|
|
value := 5
|
|
tt := view(300, 100, clk, func(c *ui.Context) { text.Odometer(c, value, text.OdometerOptions{Digits: 2}) })
|
|
digits := func() []pixel.Slot {
|
|
img := tt.Image()
|
|
var out []pixel.Slot
|
|
for y := range pixel.LineHeight + 2 {
|
|
for x := range 20 {
|
|
out = append(out, slot(t, img, x, y))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
five := digits()
|
|
value = 6
|
|
tt.Frame()
|
|
clk.add(34 * time.Millisecond)
|
|
tt.Frame()
|
|
mid := digits()
|
|
clk.add(time.Second)
|
|
tt.Frame()
|
|
six := digits()
|
|
if slices.Equal(six, five) || slices.Equal(mid, five) || slices.Equal(mid, six) {
|
|
t.Fatal("the odometer did not roll from 5 through a step to 6")
|
|
}
|
|
}
|
|
|
|
func TestScrambleSettles(t *testing.T) {
|
|
clk := newClock()
|
|
tt := view(300, 100, clk, func(c *ui.Context) {
|
|
ui.Column(c).Children(func() {
|
|
text.ScrambleText(c, "AB", text.ScrambleTextOptions{})
|
|
pixel.Text(c, "AB", pixel.TextOptions{})
|
|
})
|
|
})
|
|
same := func() bool {
|
|
img := tt.Image()
|
|
for y := range pixel.LineHeight {
|
|
for x := range pixel.Measure("AB") {
|
|
if slot(t, img, x, y) != slot(t, img, x, y+pixel.LineHeight) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
if same() {
|
|
t.Fatal("scramble started settled")
|
|
}
|
|
clk.add(200 * time.Millisecond)
|
|
tt.Frame()
|
|
if !same() {
|
|
t.Fatal("after 6 steps the text had not settled")
|
|
}
|
|
}
|
|
|
|
func TestKbdOutlineRainbow(t *testing.T) {
|
|
tt := view(400, 200, newClock(), func(c *ui.Context) {
|
|
ui.Column(c).AlignItems(ui.Start).Children(func() {
|
|
ui.Row(c).Children(func() {
|
|
text.Kbd(c, "A", text.KbdOptions{})
|
|
text.Kbd(c, "B", text.KbdOptions{Pressed: true})
|
|
})
|
|
text.OutlineText(c, "I", text.OutlineTextOptions{})
|
|
text.RainbowText(c, "AB", text.RainbowTextOptions{})
|
|
})
|
|
})
|
|
img := tt.Image()
|
|
a, b := find(t, tt, "A"), find(t, tt, "B")
|
|
if slot(t, img, int(a.X/2)+4, 0) != pixel.Edge || slot(t, img, int(b.X/2)+4, 0) == pixel.Edge {
|
|
t.Fatal("the pressed key did not sink")
|
|
}
|
|
o := find(t, tt, "I")
|
|
ox, oy := int(o.X/2), int(o.Y/2)
|
|
for y := range int(o.H / 2) {
|
|
for x := range int(o.W / 2) {
|
|
if slot(t, img, ox+x, oy+y) != pixel.Lit {
|
|
continue
|
|
}
|
|
for _, d := range [][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} {
|
|
if n := slot(t, img, ox+x+d[0], oy+y+d[1]); n != pixel.Lit && n != pixel.Edge {
|
|
t.Fatalf("outline gap beside %d,%d", x, y)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
r := find(t, tt, "AB")
|
|
wa := pixel.Measure("A")
|
|
if count(t, img, int(r.X/2), int(r.Y/2), wa, pixel.LineHeight, pixel.Bad) == 0 || count(t, img, int(r.X/2)+wa, int(r.Y/2), pixel.Measure("B"), pixel.LineHeight, pixel.Warn) == 0 {
|
|
t.Fatal("rainbow did not give A Bad and B Warn")
|
|
}
|
|
}
|
|
|
|
func TestTypewriterStaysDoneAcrossWraps(t *testing.T) {
|
|
clk := newClock()
|
|
width, done := float32(80), 0
|
|
tt := view(400, 200, clk, func(c *ui.Context) {
|
|
ui.Box(c).Width(width).Children(func() {
|
|
if text.Typewriter(c, "AAAA BBBB", text.TypewriterOptions{}).Done {
|
|
done++
|
|
}
|
|
})
|
|
pixel.Text(c, "AAAA BBBB", pixel.TextOptions{})
|
|
})
|
|
_ = tt.Click("AAAA BBBB")
|
|
if r, _ := tt.Find("AAAA BBBB"); done != 1 || r.H != 2*pixel.LineHeight*2 {
|
|
t.Fatalf("narrow: done %d, box %v", done, r)
|
|
}
|
|
width = 200
|
|
tt.Frame()
|
|
img := tt.Image()
|
|
w := pixel.Measure("AAAA BBBB")
|
|
for y := range pixel.LineHeight {
|
|
for x := range w {
|
|
if slot(t, img, x, y) != slot(t, img, x, pixel.LineHeight+y) {
|
|
t.Fatalf("after widening, dot %d,%d differs from the full text", x, y)
|
|
}
|
|
}
|
|
}
|
|
if done != 1 {
|
|
t.Fatalf("done fired %d times", done)
|
|
}
|
|
}
|