Files
MujicaUI/data/tree_table_test.go
T

266 lines
7.8 KiB
Go

package data
import (
"cmp"
"fmt"
"strconv"
"testing"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
)
type ttHarness struct {
kids map[string][]string
size map[string]int
status map[string]DataStatus
loads int
st TreeTableState[string]
opts TreeTableOptions[string]
compares int
sorts int
c *ui.Context
}
func newTTHarness() *ttHarness {
h := &ttHarness{
kids: map[string][]string{"Vestry": {"Stole", "Alb", "Cope"}, "Belfry": {}},
size: map[string]int{"Vestry": 9, "Belfry": 4, "Stole": 2, "Alb": 5, "Cope": 1, "Organ": 7},
status: map[string]DataStatus{"Belfry": DataUnloaded},
}
text := func(n string) { ui.Text(h.c, n).SingleLine() }
h.opts = TreeTableOptions[string]{
Roots: []string{"Vestry", "Belfry", "Organ"},
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++; h.status[n] = DataLoading },
Columns: []TreeColumn[string]{
{ID: "name", Title: "Name", Cell: text,
Compare: func(a, b string) int { h.compares++; return cmp.Compare(a, b) }},
{ID: "size", Title: "Size", Width: 80, Align: ui.End,
Cell: func(n string) { ui.Text(h.c, strconv.Itoa(h.size[n])) },
Compare: func(a, b string) int { h.compares++; return cmp.Compare(h.size[a], h.size[b]) }},
},
}
return h
}
func (h *ttHarness) view(c *ui.Context) {
core.Use(c, core.Settings{Mode: core.Light})
h.c = c
ui.Column(c).Padding(10).Children(func() {
v := TreeTable(c, &h.st, h.opts)
v.Element.Size(360, 320)
if v.SortChanged() {
h.sorts++
}
})
}
func (h *ttHarness) order(tt *ui.Tester) string {
var out []string
for _, s := range tt.Texts() {
if _, ok := h.size[s]; ok && (len(out) == 0 || out[len(out)-1] != s) {
out = append(out, s)
}
}
return fmt.Sprint(out)
}
func TestTreeTableSortsSiblings(t *testing.T) {
h := newTTHarness()
tt := ui.NewTester(h.view, 400, 360)
h.st.SetOpen("Vestry", true)
tt.Frame()
if got := h.order(tt); got != "[Vestry Stole Alb Cope Belfry Organ]" {
t.Fatalf("unsorted %s", got)
}
if err := tt.Click("Alb"); err != nil {
t.Fatal(err)
}
if err := tt.Click("Size"); err != nil {
t.Fatal(err)
}
if got := h.order(tt); got != "[Belfry Organ Vestry Cope Stole Alb]" || h.sorts != 1 {
t.Fatalf("by size %s sorts %d", got, h.sorts)
}
if k, _ := h.st.Selected(); k != "Alb" {
t.Fatalf("selection moved to %q", k)
}
n := h.compares
tt.Frame()
tt.Frame()
if h.compares != n {
t.Fatalf("re-sorted without a change: %d", h.compares-n)
}
_ = tt.Click("Size")
if got := h.order(tt); got != "[Vestry Alb Stole Cope Organ Belfry]" {
t.Fatalf("descending %s", got)
}
}
func TestTreeTableLazyAndKeys(t *testing.T) {
h := newTTHarness()
tt := ui.NewTester(h.view, 400, 360)
_ = tt.Click("Belfry")
tt.Key(0, ui.KeyRight)
if !h.st.IsOpen("Belfry") || h.loads != 1 || !tt.HasText("Loading") {
t.Fatalf("open %v loads %d texts %q", h.st.IsOpen("Belfry"), h.loads, tt.Texts())
}
h.kids["Belfry"] = []string{"Rope"}
h.size["Rope"] = 3
h.status["Belfry"] = DataReady
tt.Key(0, ui.KeyRight)
if k, _ := h.st.Selected(); k != "Rope" {
t.Fatalf("Right chose %q", k)
}
tt.Key(0, ui.KeyLeft)
tt.Key(0, ui.KeyLeft)
if h.st.IsOpen("Belfry") || tt.HasText("Rope") {
t.Fatal("Left did not close")
}
}
func TestTreeTableLayout(t *testing.T) {
h := newTTHarness()
h.st.Layout = ui.TableLayout{Order: []string{"size", "name"}, Widths: map[string]float32{"size": 120}}
tt := ui.NewTester(h.view, 400, 360)
size, _ := tt.Find("Size")
name, _ := tt.Find("Name")
if size.X >= name.X || h.st.Layout.Widths["size"] != 120 {
t.Fatalf("layout lost: size %v name %v %v", size.X, name.X, h.st.Layout)
}
}
func TestTreeTableBadOptions(t *testing.T) {
h := newTTHarness()
h.opts.Columns[1].Cell = nil
defer func() {
if recover() == nil {
t.Fatal("no panic without Cell")
}
}()
ui.NewTester(h.view, 300, 300)
}
func TestTreeTableLargeIsVirtualized(t *testing.T) {
roots, kids := bigTree()
var st TreeTableState[int]
rows := map[int]bool{}
cell := func(c *ui.Context) func(int) {
return func(n int) { rows[n] = true; ui.Text(c, strconv.Itoa(n)) }
}
view := func(c *ui.Context) {
core.Use(c, core.Settings{})
clear(rows)
TreeTable(c, &st, TreeTableOptions[int]{Roots: roots, Children: kids, Label: strconv.Itoa,
Columns: []TreeColumn[int]{{ID: "n", Title: "N", Cell: cell(c), Compare: cmp.Compare[int]}, {ID: "m", Title: "M", Cell: cell(c)}}}).Element.Size(320, 360)
}
tt := ui.NewTester(view, 400, 400)
const bound = 26 // see TestTreeLargeIsVirtualized
if len(rows) == 0 || len(rows) > bound || rows[1] {
t.Fatalf("collapsed: %d rows", len(rows))
}
for _, r := range roots {
st.SetOpen(r, true)
}
tt.Frame()
if len(rows) > bound || st.Tree.Outline.Rows() != 100_000 {
t.Fatalf("open: %d rows of %d", len(rows), st.Tree.Outline.Rows())
}
st.Sort = ui.SortOrder{Column: "n", Descending: true}
tt.Frame()
if len(rows) > bound || st.Tree.Outline.Item(0) != 99_000 || st.Tree.Outline.Item(1) != 99_999 {
t.Fatalf("sorted: %d rows, first %d %d", len(rows), st.Tree.Outline.Item(0), st.Tree.Outline.Item(1))
}
}
func TestTreeTableSnapshot(t *testing.T) {
snapshot(t, "tree_table", 420, 240, func(c *ui.Context) {
h := ui.Local(c.Root(), "h", func() *ttHarness {
h := newTTHarness()
h.st.SetOpen("Vestry", true)
h.st.Sort = ui.SortOrder{Column: "name"}
h.st.Tree.started, h.st.Tree.cursor, h.st.Tree.asked = true, 2, map[string]bool{}
h.st.Tree.Sel, h.st.Tree.Has = "Vestry", true
return h
})
(*h).c = c
ui.Column(c).Padding(16).Children(func() {
TreeTable(c, &(*h).st, (*h).opts).Element.Size(388, 208)
})
})
}
func TestTreeTableSelectionFollowsLargeSort(t *testing.T) {
kids := make([]int, 3000)
for i := range kids {
kids[i] = i + 1
}
var st TreeTableState[int]
view := func(c *ui.Context) {
core.Use(c, core.Settings{})
TreeTable(c, &st, TreeTableOptions[int]{Roots: []int{0}, Label: strconv.Itoa,
Children: func(n int) []int {
if n == 0 {
return kids
}
return nil
},
Columns: []TreeColumn[int]{{ID: "n", Title: "N", Cell: func(n int) { ui.Text(c, "#"+strconv.Itoa(n)) }, Compare: cmp.Compare[int]}},
}).Element.Size(320, 360)
}
st.SetOpen(0, true)
tt := ui.NewTester(view, 400, 400)
if err := tt.Click("#3"); err != nil {
t.Fatal(err)
}
st.Sort = ui.SortOrder{Column: "n", Descending: true}
tt.Frame()
if k, ok := st.Selected(); !ok || k != 3 || st.Tree.Outline.Item(st.Tree.cursor) != 3 {
t.Fatalf("after sort: selected %d %v at row %d", k, ok, st.Tree.cursor)
}
}
func TestTreeTableColumnMenuKeyboard(t *testing.T) {
h := newTTHarness()
tt := ui.NewTester(h.view, 420, 480)
tabTo(t, tt, "Columns")
tt.Key(0, ui.KeyEnter)
tabTo(t, tt, "Sort Size ascending")
tt.Key(0, ui.KeyEnter)
if h.st.Sort != (ui.SortOrder{Column: "size"}) || h.sorts != 1 {
t.Fatalf("sort %+v sorts %d", h.st.Sort, h.sorts)
}
tabTo(t, tt, "Move Name right")
tt.Key(0, ui.KeyEnter)
tabTo(t, tt, "Make Name narrower")
tt.Key(0, ui.KeyEnter)
if fmt.Sprint(h.st.Layout.Order) != "[size name]" || h.st.Layout.Widths["name"] != 104 {
t.Fatalf("layout %+v", h.st.Layout)
}
tt.Key(0, ui.KeyEscape)
if got := h.order(tt); got != "[Belfry Organ Vestry]" {
t.Fatalf("rows not sorted by size: %s", got)
}
}
func TestTreeTableEmptyClearsSelection(t *testing.T) {
h := newTTHarness()
h.opts.Multiple = true
tt := ui.NewTester(h.view, 400, 360)
if err := tt.Click("Organ"); err != nil {
t.Fatal(err)
}
if k, ok := h.st.Selected(); !ok || k != "Organ" || h.st.Selection.Len() != 1 {
t.Fatalf("selected %q %v", k, ok)
}
h.opts.Roots = nil
tt.Frame()
if k, ok := h.st.Selected(); ok || h.st.Selection.Len() != 0 {
t.Fatalf("empty table still selects %q (%d chosen)", k, h.st.Selection.Len())
}
}