Files
MujicaUI/project/kanban_board_test.go
ZacharyZhang-NYandClaude Opus 5.5 bcce34cda1 Upgrade MyGo to v0.5.5
MyGo v0.5.5 returns ui.Element as a checked value handle instead of a
pointer. Migrated with `mygo migrate-ui`, then fixed what the codemod
leaves to review:

- Element nil checks become .Valid(); absent elements are ui.Element{}.
- Input handlers and paint callbacks run outside the build pass, where
  elements expire and Context methods are inert: read IsDisabled into a
  local during the build, keep a ui.Handle for Focus, use c.Services()
  for clipboard access, create ui.Local state during the build.
- Add core.AutoFocus: v0.5.5 records an overlay's opener after its
  content builds, so same-frame autofocus lost focus restore on close.
  It focuses one frame later; cascader rows defer their focus likewise.
- core.OverlayLayer identifies build passes by the root element, not
  c.Now(), which repeats across passes and under the Tester clock.
- Context menus survive the opening press counted as PressedOutside;
  split button tooltip no longer reorders its open menu; disabled
  menubar rows pass presses to the panel; Form defers blur-started
  async checks so the growing message no longer moves Submit away
  from the release; Combobox drops its disabled copy now that MyGo
  blocks typing under a disabled wrapper (canary flipped to a guard).
- The Tester clock is virtual: tests use Advance/Now instead of
  time.Sleep/time.Now and read element state inside the view. The web
  mirror advances the clock to wall time before each frame and input.
- AGENTS.md and docs describe the value Element API and the new pin.

Snapshot goldens are darwin-only and were not re-verified here.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 01:37:54 -04:00

180 lines
5.9 KiB
Go

package project
import (
"fmt"
"strings"
"testing"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
)
var kanbanColumns3 = []KanbanColumn{{Status: "todo"}, {Status: "doing", WIP: 3}, {Status: "review"}, {Status: "done"}}
type kanbanProbe struct {
tasks []Task
cols []KanbanColumn
disabled bool
moves []KanbanMove
opened []string
}
func (p *kanbanProbe) tester() *ui.Tester {
if p.cols == nil {
p.cols = kanbanColumns3
}
return ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
ui.Column(c).Height(560).Padding(8).Children(func() {
r := KanbanBoard(c, &p.tasks, KanbanBoardOptions{Columns: p.cols, Statuses: projStatuses, Members: projMembers,
Labels: projLabels, Disabled: p.disabled})
if m, ok := r.Moved(); ok {
p.moves = append(p.moves, m)
}
if id, ok := r.Opened(); ok {
p.opened = append(p.opened, id)
}
})
}, 1240, 600)
}
// kanbanOrder lists IDs per status in slice order.
func kanbanOrder(tasks []Task, status string) string {
var out []string
for _, t := range tasks {
if t.Status == status {
out = append(out, t.ID)
}
}
return strings.Join(out, ",")
}
func TestKanbanBoardDragAcrossAndWithin(t *testing.T) {
p := &kanbanProbe{tasks: projTasks()}
tt := p.tester()
src, _ := tt.Find("AVE-1 Write the overture")
dst, _ := tt.Find("AVE-4 Rehearse act two")
calDrag(tt, src.X+20, src.Y+20, dst.X+20, dst.Y+10)
if kanbanOrder(p.tasks, "review") != "AVE-1,AVE-4" || kanbanOrder(p.tasks, "todo") != "" || len(p.moves) != 1 ||
p.moves[0] != (KanbanMove{ID: "AVE-1", From: "todo", To: "review", Index: 0}) {
t.Fatalf("drag onto a card: %v %+v", kanbanOrder(p.tasks, "review"), p.moves)
}
src, _ = tt.Find("AVE-2 Tune the organ")
col, _ := tt.Find("In Progress")
calDrag(tt, src.X+20, src.Y+20, col.X+20, col.Y+400)
if kanbanOrder(p.tasks, "doing") != "AVE-3,AVE-2" {
t.Fatalf("drag to the column end: %v", kanbanOrder(p.tasks, "doing"))
}
}
func TestKanbanBoardKeyboardMoves(t *testing.T) {
p := &kanbanProbe{tasks: projTasks()}
tt := p.tester()
_ = tt.Click("AVE-2 Tune the organ")
tt.Key(ui.Alt, ui.KeyDown)
if kanbanOrder(p.tasks, "doing") != "AVE-3,AVE-2" || !tt.Focused("AVE-2 Tune the organ") {
t.Fatalf("Alt+Down %v", kanbanOrder(p.tasks, "doing"))
}
tt.Key(ui.Alt, ui.KeyRight)
if kanbanOrder(p.tasks, "review") != "AVE-4,AVE-2" || !tt.Focused("AVE-2 Tune the organ") {
t.Fatalf("Alt+Right %v", kanbanOrder(p.tasks, "review"))
}
tt.Key(ui.Alt, ui.KeyUp)
if kanbanOrder(p.tasks, "review") != "AVE-2,AVE-4" {
t.Fatalf("Alt+Up %v", kanbanOrder(p.tasks, "review"))
}
tt.Key(0, ui.KeyEnter)
if len(p.opened) != 2 || p.opened[1] != "AVE-2" {
t.Fatalf("opened %v", p.opened)
}
}
func TestKanbanBoardWIPLimit(t *testing.T) {
p := &kanbanProbe{tasks: projTasks(), cols: []KanbanColumn{{Status: "todo"}, {Status: "doing", WIP: 2}, {Status: "review"}, {Status: "done"}}}
tt := p.tester()
if !tt.HasText("2 of 2 cards") {
t.Fatalf("WIP count %v", tt.Texts())
}
_ = tt.Click("AVE-1 Write the overture")
tt.Key(ui.Alt, ui.KeyRight)
if kanbanOrder(p.tasks, "todo") != "AVE-1" || !tt.HasText("In Progress is at its limit of 2 cards.") {
t.Fatalf("WIP not enforced: %v", tt.Texts())
}
p.cols[1].WIP = 1
tt.Frame()
if !tt.HasText("1 over the limit") {
t.Fatal("over-limit warning missing")
}
}
func TestKanbanBoardDisabledAndInvalid(t *testing.T) {
p := &kanbanProbe{tasks: projTasks(), disabled: true}
tt := p.tester()
src, _ := tt.Find("AVE-1 Write the overture")
dst, _ := tt.Find("AVE-4 Rehearse act two")
calDrag(tt, src.X+20, src.Y+20, dst.X+20, dst.Y+10)
_ = tt.Click("AVE-1 Write the overture")
tt.Key(ui.Alt, ui.KeyRight)
if len(p.moves) != 0 || len(p.opened) != 0 || kanbanOrder(p.tasks, "todo") != "AVE-1" {
t.Fatal("disabled board changed")
}
defer func() {
if recover() == nil {
t.Fatal("expected panic for a repeated column")
}
}()
q := &kanbanProbe{tasks: projTasks(), cols: []KanbanColumn{{Status: "todo"}, {Status: "todo"}}}
q.tester()
}
func TestKanbanBoardLargeData(t *testing.T) {
p := &kanbanProbe{}
for i := range 100000 {
p.tasks = append(p.tasks, Task{ID: fmt.Sprintf("BIG-%d", i+1), Title: "Card", Status: kanbanColumns3[i%4].Status})
}
p.cols = []KanbanColumn{{Status: "todo"}, {Status: "doing"}, {Status: "review"}, {Status: "done"}}
tt := p.tester()
if n := len(tt.Texts()); n > 600 {
t.Fatalf("built %d labels", n)
}
}
func TestKanbanBoardSnapshot(t *testing.T) {
tasks := projTasks()
snapshot(t, "kanban_board", 1240, 460, func(c *ui.Context) {
ui.Column(c).Height(460).Padding(16).Children(func() {
KanbanBoard(c, &tasks, KanbanBoardOptions{Columns: kanbanColumns3, Statuses: projStatuses, Members: projMembers, Labels: projLabels})
})
})
}
func TestKanbanBoardColumnChanges(t *testing.T) {
p := &kanbanProbe{tasks: projTasks(), cols: []KanbanColumn{{Status: "todo"}, {Status: "doing"}}}
tt := p.tester()
p.cols = []KanbanColumn{{Status: "done"}, {Status: "review"}, {Status: "doing"}}
tt.Frame()
if !tt.HasText("AVE-5 Book the hall") || !tt.HasText("AVE-4 Rehearse act two") {
t.Fatal("columns retain old indexes")
}
}
func TestKanbanBoardForeignDrop(t *testing.T) {
left := []Task{{ID: "A-1", Title: "First", Status: "todo"}}
right := []Task{{ID: "B-1", Title: "Second", Status: "todo"}}
opts := KanbanBoardOptions{Columns: []KanbanColumn{{Status: "todo"}}, Statuses: projStatuses}
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
ui.Row(c).Height(400).Children(func() {
ui.Box(c.Key("left")).Width(300).Height(400).Children(func() { KanbanBoard(c, &left, opts) })
ui.Box(c.Key("right")).Width(300).Height(400).Children(func() { KanbanBoard(c, &right, opts) })
})
}, 640, 420)
src, _ := tt.Find("A-1 First")
dst, _ := tt.Find("B-1 Second")
calDrag(tt, src.X+20, src.Y+20, dst.X+20, dst.Y+10)
if len(left) != 1 || left[0].ID != "A-1" || len(right) != 1 || right[0].ID != "B-1" {
t.Fatal("foreign drop changed another board")
}
}