mirror of
https://github.com/boolean-maybe/tiki
synced 2026-04-21 13:37:20 +00:00
287 lines
7.1 KiB
Go
287 lines
7.1 KiB
Go
package component
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/boolean-maybe/tiki/config"
|
|
"github.com/boolean-maybe/tiki/task"
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/rivo/tview"
|
|
)
|
|
|
|
func makeTasks(ids ...string) []*task.Task {
|
|
tasks := make([]*task.Task, len(ids))
|
|
for i, id := range ids {
|
|
tasks[i] = &task.Task{
|
|
ID: id,
|
|
Title: "Task " + id,
|
|
Status: task.StatusBacklog,
|
|
}
|
|
}
|
|
return tasks
|
|
}
|
|
|
|
func TestNewTaskList(t *testing.T) {
|
|
tl := NewTaskList(5)
|
|
|
|
if tl == nil {
|
|
t.Fatal("NewTaskList returned nil")
|
|
return
|
|
}
|
|
if tl.maxVisibleRows != 5 {
|
|
t.Errorf("Expected maxVisibleRows=5, got %d", tl.maxVisibleRows)
|
|
}
|
|
if tl.selectionIndex != 0 {
|
|
t.Errorf("Expected initial selectionIndex=0, got %d", tl.selectionIndex)
|
|
}
|
|
|
|
colors := config.GetColors()
|
|
if tl.idGradient != colors.TaskBoxIDColor {
|
|
t.Error("Expected ID gradient from config")
|
|
}
|
|
if tl.idFallback != config.GetColors().FallbackTaskIDColor {
|
|
t.Error("Expected ID fallback from config")
|
|
}
|
|
if tl.titleColor != colors.TaskBoxTitleColor {
|
|
t.Error("Expected title color from config")
|
|
}
|
|
}
|
|
|
|
func TestSetTasks_RecomputesIDColumnWidth(t *testing.T) {
|
|
tl := NewTaskList(10)
|
|
|
|
tl.SetTasks(makeTasks("AB", "ABCDE", "XY"))
|
|
if tl.idColumnWidth != 5 {
|
|
t.Errorf("Expected idColumnWidth=5, got %d", tl.idColumnWidth)
|
|
}
|
|
|
|
tl.SetTasks(makeTasks("A"))
|
|
if tl.idColumnWidth != 1 {
|
|
t.Errorf("Expected idColumnWidth=1, got %d", tl.idColumnWidth)
|
|
}
|
|
}
|
|
|
|
func TestSetTasks_EmptyList(t *testing.T) {
|
|
tl := NewTaskList(10)
|
|
tl.SetTasks(nil)
|
|
|
|
if tl.idColumnWidth != 0 {
|
|
t.Errorf("Expected idColumnWidth=0, got %d", tl.idColumnWidth)
|
|
}
|
|
if tl.selectionIndex != 0 {
|
|
t.Errorf("Expected selectionIndex=0, got %d", tl.selectionIndex)
|
|
}
|
|
}
|
|
|
|
func TestSelection_ClampsBounds(t *testing.T) {
|
|
tl := NewTaskList(10)
|
|
tl.SetTasks(makeTasks("A", "B", "C"))
|
|
|
|
tl.SetSelection(-5)
|
|
if tl.selectionIndex != 0 {
|
|
t.Errorf("Expected clamped to 0, got %d", tl.selectionIndex)
|
|
}
|
|
|
|
tl.SetSelection(100)
|
|
if tl.selectionIndex != 2 {
|
|
t.Errorf("Expected clamped to 2, got %d", tl.selectionIndex)
|
|
}
|
|
|
|
tl.SetSelection(1)
|
|
if tl.selectionIndex != 1 {
|
|
t.Errorf("Expected 1, got %d", tl.selectionIndex)
|
|
}
|
|
}
|
|
|
|
func TestGetSelectedTask(t *testing.T) {
|
|
tl := NewTaskList(10)
|
|
tasks := makeTasks("A", "B", "C")
|
|
tl.SetTasks(tasks)
|
|
|
|
tl.SetSelection(1)
|
|
selected := tl.GetSelectedTask()
|
|
if selected == nil || selected.ID != "B" {
|
|
t.Errorf("Expected task B, got %v", selected)
|
|
}
|
|
}
|
|
|
|
func TestGetSelectedTask_EmptyList(t *testing.T) {
|
|
tl := NewTaskList(10)
|
|
if tl.GetSelectedTask() != nil {
|
|
t.Error("Expected nil for empty task list")
|
|
}
|
|
}
|
|
|
|
func TestScrollDown(t *testing.T) {
|
|
tl := NewTaskList(10)
|
|
tl.SetTasks(makeTasks("A", "B", "C"))
|
|
|
|
tl.ScrollDown()
|
|
if tl.selectionIndex != 1 {
|
|
t.Errorf("Expected 1 after ScrollDown, got %d", tl.selectionIndex)
|
|
}
|
|
|
|
tl.ScrollDown()
|
|
if tl.selectionIndex != 2 {
|
|
t.Errorf("Expected 2 after second ScrollDown, got %d", tl.selectionIndex)
|
|
}
|
|
|
|
// Should not go past last item
|
|
tl.ScrollDown()
|
|
if tl.selectionIndex != 2 {
|
|
t.Errorf("Expected 2 (clamped), got %d", tl.selectionIndex)
|
|
}
|
|
}
|
|
|
|
func TestScrollUp(t *testing.T) {
|
|
tl := NewTaskList(10)
|
|
tl.SetTasks(makeTasks("A", "B", "C"))
|
|
tl.SetSelection(2)
|
|
|
|
tl.ScrollUp()
|
|
if tl.selectionIndex != 1 {
|
|
t.Errorf("Expected 1 after ScrollUp, got %d", tl.selectionIndex)
|
|
}
|
|
|
|
tl.ScrollUp()
|
|
if tl.selectionIndex != 0 {
|
|
t.Errorf("Expected 0 after second ScrollUp, got %d", tl.selectionIndex)
|
|
}
|
|
|
|
// Should not go below 0
|
|
tl.ScrollUp()
|
|
if tl.selectionIndex != 0 {
|
|
t.Errorf("Expected 0 (clamped), got %d", tl.selectionIndex)
|
|
}
|
|
}
|
|
|
|
func TestScrollDown_EmptyList(t *testing.T) {
|
|
tl := NewTaskList(10)
|
|
// Should not panic
|
|
tl.ScrollDown()
|
|
tl.ScrollUp()
|
|
}
|
|
|
|
func TestFewerItemsThanViewport(t *testing.T) {
|
|
tl := NewTaskList(10)
|
|
tl.SetTasks(makeTasks("A", "B"))
|
|
|
|
// scrollOffset should stay at 0 since all items fit
|
|
if tl.scrollOffset != 0 {
|
|
t.Errorf("Expected scrollOffset=0, got %d", tl.scrollOffset)
|
|
}
|
|
|
|
tl.ScrollDown()
|
|
if tl.scrollOffset != 0 {
|
|
t.Errorf("Expected scrollOffset=0 with fewer items, got %d", tl.scrollOffset)
|
|
}
|
|
}
|
|
|
|
func TestSetIDColors(t *testing.T) {
|
|
tl := NewTaskList(10)
|
|
g := config.Gradient{Start: [3]int{255, 0, 0}, End: [3]int{0, 255, 0}}
|
|
fb := config.NewColor(tcell.ColorRed)
|
|
|
|
result := tl.SetIDColors(g, fb)
|
|
if result != tl {
|
|
t.Error("SetIDColors should return self for chaining")
|
|
}
|
|
if tl.idGradient != g {
|
|
t.Error("Expected custom gradient")
|
|
}
|
|
if tl.idFallback != fb {
|
|
t.Error("Expected custom fallback")
|
|
}
|
|
}
|
|
|
|
func TestSetTitleColor(t *testing.T) {
|
|
tl := NewTaskList(10)
|
|
c := config.NewColor(tcell.ColorRed)
|
|
result := tl.SetTitleColor(c)
|
|
if result != tl {
|
|
t.Error("SetTitleColor should return self for chaining")
|
|
}
|
|
if tl.titleColor != c {
|
|
t.Errorf("Expected color red, got %v", tl.titleColor)
|
|
}
|
|
}
|
|
|
|
func TestSetTasks_ClampsSelectionOnShrink(t *testing.T) {
|
|
tl := NewTaskList(10)
|
|
tl.SetTasks(makeTasks("A", "B", "C", "D", "E"))
|
|
tl.SetSelection(4)
|
|
|
|
// Shrink list — selection should clamp
|
|
tl.SetTasks(makeTasks("A", "B"))
|
|
if tl.selectionIndex != 1 {
|
|
t.Errorf("Expected selectionIndex clamped to 1, got %d", tl.selectionIndex)
|
|
}
|
|
}
|
|
|
|
func TestGetSelectedIndex(t *testing.T) {
|
|
tl := NewTaskList(10)
|
|
tl.SetTasks(makeTasks("A", "B", "C"))
|
|
tl.SetSelection(2)
|
|
|
|
if tl.GetSelectedIndex() != 2 {
|
|
t.Errorf("Expected 2, got %d", tl.GetSelectedIndex())
|
|
}
|
|
}
|
|
|
|
func TestBuildRow(t *testing.T) {
|
|
tl := NewTaskList(10)
|
|
|
|
pendingTask := &task.Task{ID: "TIKI-ABC001", Title: "My pending task", Status: task.StatusBacklog}
|
|
doneTask := &task.Task{ID: "TIKI-ABC002", Title: "My done task", Status: task.StatusDone}
|
|
|
|
// set tasks so idColumnWidth is computed
|
|
tl.SetTasks([]*task.Task{pendingTask, doneTask})
|
|
|
|
width := 80
|
|
|
|
t.Run("pending task shows circle", func(t *testing.T) {
|
|
row := tl.buildRow(pendingTask, false, width)
|
|
if !strings.Contains(row, "\u25CB") {
|
|
t.Error("pending task row should contain circle (○)")
|
|
}
|
|
})
|
|
|
|
t.Run("done task shows checkmark", func(t *testing.T) {
|
|
row := tl.buildRow(doneTask, false, width)
|
|
if !strings.Contains(row, "\u2713") {
|
|
t.Error("done task row should contain checkmark (✓)")
|
|
}
|
|
})
|
|
|
|
t.Run("contains task ID", func(t *testing.T) {
|
|
row := tl.buildRow(pendingTask, false, width)
|
|
if !strings.Contains(row, pendingTask.ID) {
|
|
t.Errorf("row should contain task ID %q", pendingTask.ID)
|
|
}
|
|
})
|
|
|
|
t.Run("contains title", func(t *testing.T) {
|
|
row := tl.buildRow(pendingTask, false, width)
|
|
escaped := tview.Escape(pendingTask.Title)
|
|
if !strings.Contains(row, escaped) {
|
|
t.Errorf("row should contain escaped title %q", escaped)
|
|
}
|
|
})
|
|
|
|
t.Run("selected row has selection color prefix", func(t *testing.T) {
|
|
row := tl.buildRow(pendingTask, true, width)
|
|
selTag := tl.selectionColor.Tag().WithBg(tl.selectionBgColor).String()
|
|
if !strings.HasPrefix(row, selTag) {
|
|
t.Errorf("selected row should start with selection color %q", selTag)
|
|
}
|
|
})
|
|
|
|
t.Run("unselected row has no selection prefix", func(t *testing.T) {
|
|
row := tl.buildRow(pendingTask, false, width)
|
|
selTag := tl.selectionColor.Tag().WithBg(tl.selectionBgColor).String()
|
|
if strings.HasPrefix(row, selTag) {
|
|
t.Error("unselected row should not start with selection color")
|
|
}
|
|
})
|
|
}
|