274 lines
7.0 KiB
Go
274 lines
7.0 KiB
Go
package data
|
||
|
||
import (
|
||
"fmt"
|
||
"image/color"
|
||
"slices"
|
||
"testing"
|
||
|
||
"github.com/egoist/mygo/ui"
|
||
|
||
"github.com/ZacharyZhang-NY/MujicaUI/core"
|
||
"github.com/ZacharyZhang-NY/MujicaUI/theme"
|
||
)
|
||
|
||
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) {
|
||
core.Use(c, core.Settings{Mode: core.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.Element.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) {
|
||
core.Use(c, core.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) {
|
||
core.Use(c, core.Settings{})
|
||
built = 0
|
||
Tree(c, &st, TreeOptions[int]{Roots: roots, Children: kids, Label: func(n int) string {
|
||
built++
|
||
return fmt.Sprintf("Node %d", n)
|
||
}}).Element.Size(320, 360)
|
||
}
|
||
tt := ui.NewTester(view, 400, 400)
|
||
// Two Label calls per row; a jump builds 2×(10 rows + 2 overscan) + 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{}
|
||
h.st.Tree.Sel, h.st.Tree.Has = "Pulpit", true
|
||
return h
|
||
})
|
||
ui.Column(c).Padding(16).Children(func() {
|
||
Tree(c, &(*h).st, (*h).opts).Element.Size(360, 228)
|
||
})
|
||
})
|
||
}
|
||
|
||
func TestTreeSelectionFollowsLargeReorder(t *testing.T) {
|
||
roots := make([]int, 3000)
|
||
for i := range roots {
|
||
roots[i] = i
|
||
}
|
||
var st TreeState[int]
|
||
view := func(c *ui.Context) {
|
||
core.Use(c, core.Settings{Mode: core.Light})
|
||
Tree(c, &st, TreeOptions[int]{Roots: roots, Children: func(int) []int { return nil },
|
||
Label: func(n int) string { return fmt.Sprintf("Node %d", n) }}).Element.Size(320, 360)
|
||
}
|
||
tt := ui.NewTester(view, 400, 400)
|
||
if err := tt.Click("Node 2"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
r, _ := tt.Find("Node 2")
|
||
got := color.RGBAModel.Convert(tt.Image().At(int(r.X+r.W-6), int(r.Y+r.H/2))).(color.RGBA)
|
||
if want := toRGBA(theme.Light().Selection); got != want {
|
||
t.Fatalf("selected row background %v, want Selection %v", got, want)
|
||
}
|
||
roots = slices.Clone(roots)
|
||
slices.Reverse(roots)
|
||
tt.Frame()
|
||
if k, ok := st.Selected(); !ok || k != 2 || st.Tree.cursor != 2997 {
|
||
t.Fatalf("after reversing: selected %d %v at row %d", k, ok, st.Tree.cursor)
|
||
}
|
||
}
|
||
|
||
func TestTreeEmptyClearsSelection(t *testing.T) {
|
||
h := newTreeHarness()
|
||
tt := ui.NewTester(h.view, 400, 400)
|
||
if err := tt.Click("Sacristy"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
h.opts.Roots = nil
|
||
tt.Frame()
|
||
if k, ok := h.st.Selected(); ok {
|
||
t.Fatalf("empty tree still selects %q", k)
|
||
}
|
||
h.opts.Roots = []string{"Nave", "Crypt", "Sacristy"}
|
||
tt.Frame()
|
||
if k, ok := h.st.Selected(); ok {
|
||
t.Fatalf("selection came back as %q", k)
|
||
}
|
||
}
|