Files
MujicaUI/tree_test.go
T

223 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mujica
import (
"fmt"
"testing"
"github.com/egoist/mygo/ui"
)
type treeHarness struct {
kids map[string][]string
status map[string]DataStatus
loads []string
st TreeState[string]
opts TreeOptions[string]
built map[string]bool
changes int
}
func newTreeHarness() *treeHarness {
h := &treeHarness{
kids: map[string][]string{
"Nave": {"Pulpit", "Pews"},
"Crypt": {}, // loaded lazily
"Pulpit": nil,
"Pews": nil,
"Sacristy": nil,
},
status: map[string]DataStatus{"Crypt": DataUnloaded},
}
h.opts = TreeOptions[string]{
Roots: []string{"Nave", "Crypt", "Sacristy"},
Children: func(n string) []string { return h.kids[n] },
Label: func(n string) string { return n },
ItemStatus: func(n string) DataStatus { return h.status[n] },
Load: func(n string) {
h.loads = append(h.loads, n)
h.status[n] = DataLoading
},
}
return h
}
func (h *treeHarness) view(c *ui.Context) {
Use(c, Settings{Mode: Light})
h.built = map[string]bool{}
opts := h.opts
label := opts.Label
opts.Label = func(n string) string { h.built[n] = true; return label(n) }
ui.Column(c).Padding(10).Children(func() {
v := Tree(c, &h.st, opts)
v.Size(320, 360)
if v.Changed() {
h.changes++
}
})
}
func TestTreeExpandAndKeyboard(t *testing.T) {
h := newTreeHarness()
tt := ui.NewTester(h.view, 400, 400)
if tt.HasText("Pulpit") || h.built["Pulpit"] {
t.Fatal("closed subtree built")
}
if err := tt.Click("Nave"); err != nil {
t.Fatal(err)
}
if k, _ := h.st.Selected(); k != "Nave" || h.changes != 1 {
t.Fatalf("selected %q changes %d", k, h.changes)
}
tt.Key(0, ui.KeyRight)
if !h.st.IsOpen("Nave") || !tt.HasText("Pulpit") {
t.Fatalf("Right did not open: %q", tt.Texts())
}
tt.Key(0, ui.KeyRight)
if k, _ := h.st.Selected(); k != "Pulpit" {
t.Fatalf("Right to first child chose %q", k)
}
tt.Key(0, ui.KeyDown)
if k, _ := h.st.Selected(); k != "Pews" {
t.Fatalf("Down chose %q", k)
}
tt.Key(0, ui.KeyLeft)
if k, _ := h.st.Selected(); k != "Nave" {
t.Fatalf("Left to parent chose %q", k)
}
tt.Key(0, ui.KeyLeft)
if h.st.IsOpen("Nave") || tt.HasText("Pews") {
t.Fatal("Left did not close")
}
}
func TestTreeLazyLoad(t *testing.T) {
h := newTreeHarness()
tt := ui.NewTester(h.view, 400, 400)
if len(h.loads) != 0 {
t.Fatalf("loaded before opening: %v", h.loads)
}
h.st.SetOpen("Crypt", true)
tt.Frame()
if fmt.Sprint(h.loads) != "[Crypt]" || !tt.HasText("Loading") {
t.Fatalf("loads %v texts %q", h.loads, tt.Texts())
}
tt.Frame()
if len(h.loads) != 1 {
t.Fatalf("asked twice: %v", h.loads)
}
h.status["Crypt"] = DataFailed
tt.Frame()
if err := tt.Click("Retry"); err != nil {
t.Fatal(err)
}
if len(h.loads) != 2 {
t.Fatalf("retry did not load: %v", h.loads)
}
h.kids["Crypt"] = []string{"Ossuary"}
h.status["Crypt"] = DataReady
tt.Frame()
if !tt.HasText("Ossuary") || tt.HasText("Loading") {
t.Fatalf("children missing: %q", tt.Texts())
}
}
func TestTreeMultipleAndEmpty(t *testing.T) {
h := newTreeHarness()
h.opts.Multiple = true
tt := ui.NewTester(h.view, 400, 400)
_ = tt.Click("Nave")
_ = tt.ClickWith(ui.Cmd, "Sacristy")
if h.st.Selection.Len() != 2 || !h.st.Selection.Has("Sacristy") {
t.Fatalf("chose %d", h.st.Selection.Len())
}
h.opts.Roots = nil
tt.Frame()
if !tt.HasText("No items") {
t.Fatalf("empty: %q", tt.Texts())
}
}
func TestTreeBadOptions(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("no panic without Children")
}
}()
ui.NewTester(func(c *ui.Context) {
Use(c, Settings{})
var s TreeState[int]
Tree(c, &s, TreeOptions[int]{Roots: []int{1}})
}, 100, 100)
}
// bigTree has 100 roots of 999 children each: 100,000 nodes.
func bigTree() (roots []int, kids func(int) []int) {
for r := range 100 {
roots = append(roots, r*1000)
}
cache := map[int][]int{}
return roots, func(n int) []int {
if n%1000 != 0 {
return nil
}
if k, ok := cache[n]; ok {
return k
}
var k []int
for i := 1; i < 1000; i++ {
k = append(k, n+i)
}
cache[n] = k
return k
}
}
func TestTreeLargeIsVirtualized(t *testing.T) {
roots, kids := bigTree()
var st TreeState[int]
built := 0
view := func(c *ui.Context) {
Use(c, Settings{})
built = 0
Tree(c, &st, TreeOptions[int]{Roots: roots, Children: kids, Label: func(n int) string {
built++
return fmt.Sprintf("Node %d", n)
}}).Size(320, 360)
}
tt := ui.NewTester(view, 400, 400)
// Label runs twice per built row (row name and text). A jump builds a
// view on each side of its target: 2×10 rows of 36 DIPs, plus 2×2 overscan and 2 edges.
const bound = 2 * 26
if built == 0 || built > bound {
t.Fatalf("collapsed: label calls %d, bound %d", built, bound)
}
for _, r := range roots {
st.SetOpen(r, true)
}
tt.Frame()
if built > bound || st.tree.outline.Rows() != 100_000 {
t.Fatalf("open: label calls %d rows %d", built, st.tree.outline.Rows())
}
st.tree.outline.List.ScrollTo(64_000, ui.Start)
tt.Frame()
if built > bound || !tt.HasText(fmt.Sprintf("Node %d", st.tree.outline.Item(64_000))) {
t.Fatalf("scrolled: label calls %d texts %q", built, tt.Texts())
}
}
func TestTreeSnapshot(t *testing.T) {
snapshot(t, "tree", 400, 260, func(c *ui.Context) {
h := ui.Local(c.Root(), "h", func() *treeHarness {
h := newTreeHarness()
h.st.SetOpen("Nave", true)
h.st.SetOpen("Crypt", true)
h.status["Crypt"] = DataLoading
h.st.tree.started, h.st.tree.cursor, h.st.tree.asked = true, 1, map[string]bool{}
return h
})
ui.Column(c).Padding(16).Children(func() {
Tree(c, &(*h).st, (*h).opts).Size(360, 228)
})
})
}