mirror of
https://github.com/boolean-maybe/tiki
synced 2026-04-21 21:47:36 +00:00
1056 lines
27 KiB
Go
1056 lines
27 KiB
Go
package controller
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/boolean-maybe/tiki/config"
|
|
"github.com/boolean-maybe/tiki/model"
|
|
"github.com/boolean-maybe/tiki/store"
|
|
"github.com/boolean-maybe/tiki/task"
|
|
"github.com/boolean-maybe/tiki/workflow"
|
|
)
|
|
|
|
func init() {
|
|
// set up the default status registry for tests.
|
|
config.ResetStatusRegistry([]workflow.StatusDef{
|
|
{Key: "backlog", Label: "Backlog", Emoji: "📥", Default: true},
|
|
{Key: "ready", Label: "Ready", Emoji: "📋", Active: true},
|
|
{Key: "in_progress", Label: "In Progress", Emoji: "⚙️", Active: true},
|
|
{Key: "review", Label: "Review", Emoji: "👀", Active: true},
|
|
{Key: "done", Label: "Done", Emoji: "✅", Done: true},
|
|
})
|
|
}
|
|
|
|
// Test Draft Task Lifecycle
|
|
|
|
func TestTaskController_SetDraft(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
draft := newTestTask()
|
|
tc.SetDraft(draft)
|
|
|
|
if tc.GetDraftTask() != draft {
|
|
t.Error("SetDraft did not set the draft task")
|
|
}
|
|
|
|
if tc.GetCurrentTaskID() != draft.ID {
|
|
t.Errorf("SetDraft did not set currentTaskID, got %q, want %q", tc.GetCurrentTaskID(), draft.ID)
|
|
}
|
|
}
|
|
|
|
func TestTaskController_ClearDraft(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
tc.SetDraft(newTestTask())
|
|
tc.ClearDraft()
|
|
|
|
if tc.GetDraftTask() != nil {
|
|
t.Error("ClearDraft did not clear the draft task")
|
|
}
|
|
}
|
|
|
|
func TestTaskController_StartEditSession(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
// Create a task in the store
|
|
original := newTestTask()
|
|
original.LoadedMtime = time.Now()
|
|
_ = taskStore.CreateTask(original)
|
|
|
|
// Start edit session
|
|
editingTask := tc.StartEditSession(original.ID)
|
|
|
|
if editingTask == nil {
|
|
t.Fatal("StartEditSession returned nil")
|
|
}
|
|
|
|
if editingTask.ID != original.ID {
|
|
t.Errorf("StartEditSession returned wrong task, got ID %q, want %q", editingTask.ID, original.ID)
|
|
}
|
|
|
|
if tc.GetEditingTask() == nil {
|
|
t.Error("StartEditSession did not set editingTask")
|
|
}
|
|
|
|
if tc.GetCurrentTaskID() != original.ID {
|
|
t.Errorf("StartEditSession did not set currentTaskID, got %q, want %q", tc.GetCurrentTaskID(), original.ID)
|
|
}
|
|
}
|
|
|
|
func TestTaskController_StartEditSession_NonExistent(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
editingTask := tc.StartEditSession("NONEXISTENT")
|
|
|
|
if editingTask != nil {
|
|
t.Error("StartEditSession should return nil for non-existent task")
|
|
}
|
|
}
|
|
|
|
func TestTaskController_CancelEditSession(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
// Start an edit session
|
|
original := newTestTask()
|
|
_ = taskStore.CreateTask(original)
|
|
tc.StartEditSession(original.ID)
|
|
|
|
// Cancel it
|
|
tc.CancelEditSession()
|
|
|
|
if tc.GetEditingTask() != nil {
|
|
t.Error("CancelEditSession did not clear editingTask")
|
|
}
|
|
|
|
if tc.GetCurrentTaskID() != "" {
|
|
t.Errorf("CancelEditSession did not clear currentTaskID, got %q", tc.GetCurrentTaskID())
|
|
}
|
|
}
|
|
|
|
// Test Field Update Methods
|
|
|
|
func TestTaskController_SaveStatus(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupTask func(*TaskController, store.Store)
|
|
statusDisplay string
|
|
wantStatus task.Status
|
|
wantSuccess bool
|
|
}{
|
|
{
|
|
name: "valid status on draft task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
statusDisplay: task.StatusDisplay(task.StatusReady),
|
|
wantStatus: task.StatusReady,
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "valid status on editing task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
t := newTestTask()
|
|
_ = s.CreateTask(t)
|
|
tc.StartEditSession(t.ID)
|
|
},
|
|
statusDisplay: task.StatusDisplay(task.StatusInProgress),
|
|
wantStatus: task.StatusInProgress,
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "draft takes priority over editing",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
t := newTestTask()
|
|
_ = s.CreateTask(t)
|
|
tc.StartEditSession(t.ID)
|
|
tc.SetDraft(newTestTaskWithID())
|
|
},
|
|
statusDisplay: task.StatusDisplay(task.StatusDone),
|
|
wantStatus: task.StatusDone,
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "invalid status normalizes to default",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
statusDisplay: "InvalidStatus",
|
|
wantStatus: task.StatusBacklog, // NormalizeStatus defaults to backlog
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "no active task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
// Don't set up any task
|
|
},
|
|
statusDisplay: task.StatusDisplay(task.StatusReady),
|
|
wantSuccess: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
tt.setupTask(tc, taskStore)
|
|
|
|
got := tc.SaveStatus(tt.statusDisplay)
|
|
|
|
if got != tt.wantSuccess {
|
|
t.Errorf("SaveStatus() = %v, want %v", got, tt.wantSuccess)
|
|
}
|
|
|
|
if tt.wantSuccess {
|
|
var actualStatus task.Status
|
|
if tc.draftTask != nil {
|
|
actualStatus = tc.draftTask.Status
|
|
} else if tc.editingTask != nil {
|
|
actualStatus = tc.editingTask.Status
|
|
}
|
|
if actualStatus != tt.wantStatus {
|
|
t.Errorf("task.Status = %v, want %v", actualStatus, tt.wantStatus)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTaskController_SaveType(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupTask func(*TaskController, store.Store)
|
|
typeDisplay string
|
|
wantType task.Type
|
|
wantSuccess bool
|
|
}{
|
|
{
|
|
name: "valid type on draft task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
typeDisplay: task.TypeDisplay(task.TypeBug),
|
|
wantType: task.TypeBug,
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "valid type on editing task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
t := newTestTask()
|
|
_ = s.CreateTask(t)
|
|
tc.StartEditSession(t.ID)
|
|
},
|
|
typeDisplay: task.TypeDisplay(task.TypeSpike),
|
|
wantType: task.TypeSpike,
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "invalid type is rejected",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
typeDisplay: "InvalidType",
|
|
wantType: task.TypeStory, // task type unchanged from setup
|
|
wantSuccess: false,
|
|
},
|
|
{
|
|
name: "no active task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
// Don't set up any task
|
|
},
|
|
typeDisplay: task.TypeDisplay(task.TypeStory),
|
|
wantSuccess: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
tt.setupTask(tc, taskStore)
|
|
|
|
got := tc.SaveType(tt.typeDisplay)
|
|
|
|
if got != tt.wantSuccess {
|
|
t.Errorf("SaveType() = %v, want %v", got, tt.wantSuccess)
|
|
}
|
|
|
|
if tt.wantSuccess {
|
|
var actualType task.Type
|
|
if tc.draftTask != nil {
|
|
actualType = tc.draftTask.Type
|
|
} else if tc.editingTask != nil {
|
|
actualType = tc.editingTask.Type
|
|
}
|
|
if actualType != tt.wantType {
|
|
t.Errorf("task.Type = %v, want %v", actualType, tt.wantType)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTaskController_SavePriority(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupTask func(*TaskController, store.Store)
|
|
priority int
|
|
wantPriority int
|
|
wantSuccess bool
|
|
}{
|
|
{
|
|
name: "valid priority on draft task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
priority: 1,
|
|
wantPriority: 1,
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "valid priority on editing task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
t := newTestTask()
|
|
_ = s.CreateTask(t)
|
|
tc.StartEditSession(t.ID)
|
|
},
|
|
priority: 5,
|
|
wantPriority: 5,
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "invalid priority - negative",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
priority: -1,
|
|
wantSuccess: false,
|
|
},
|
|
{
|
|
name: "invalid priority - too high",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
priority: 10,
|
|
wantSuccess: false,
|
|
},
|
|
{
|
|
name: "no active task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
// Don't set up any task
|
|
},
|
|
priority: 3,
|
|
wantSuccess: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
tt.setupTask(tc, taskStore)
|
|
|
|
got := tc.SavePriority(tt.priority)
|
|
|
|
if got != tt.wantSuccess {
|
|
t.Errorf("SavePriority() = %v, want %v", got, tt.wantSuccess)
|
|
}
|
|
|
|
if tt.wantSuccess {
|
|
var actualPriority int
|
|
if tc.draftTask != nil {
|
|
actualPriority = tc.draftTask.Priority
|
|
} else if tc.editingTask != nil {
|
|
actualPriority = tc.editingTask.Priority
|
|
}
|
|
if actualPriority != tt.wantPriority {
|
|
t.Errorf("task.Priority = %v, want %v", actualPriority, tt.wantPriority)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTaskController_SaveAssignee(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupTask func(*TaskController, store.Store)
|
|
assignee string
|
|
wantAssignee string
|
|
wantSuccess bool
|
|
}{
|
|
{
|
|
name: "valid assignee on draft task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
assignee: "john.doe",
|
|
wantAssignee: "john.doe",
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "unassigned becomes empty string",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
assignee: "Unassigned",
|
|
wantAssignee: "",
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "valid assignee on editing task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
t := newTestTask()
|
|
_ = s.CreateTask(t)
|
|
tc.StartEditSession(t.ID)
|
|
},
|
|
assignee: "jane.smith",
|
|
wantAssignee: "jane.smith",
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "no active task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
// Don't set up any task
|
|
},
|
|
assignee: "john.doe",
|
|
wantSuccess: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
tt.setupTask(tc, taskStore)
|
|
|
|
got := tc.SaveAssignee(tt.assignee)
|
|
|
|
if got != tt.wantSuccess {
|
|
t.Errorf("SaveAssignee() = %v, want %v", got, tt.wantSuccess)
|
|
}
|
|
|
|
if tt.wantSuccess {
|
|
var actualAssignee string
|
|
if tc.draftTask != nil {
|
|
actualAssignee = tc.draftTask.Assignee
|
|
} else if tc.editingTask != nil {
|
|
actualAssignee = tc.editingTask.Assignee
|
|
}
|
|
if actualAssignee != tt.wantAssignee {
|
|
t.Errorf("task.Assignee = %q, want %q", actualAssignee, tt.wantAssignee)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTaskController_SavePoints(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupTask func(*TaskController, store.Store)
|
|
points int
|
|
wantPoints int
|
|
wantSuccess bool
|
|
}{
|
|
{
|
|
name: "valid points on draft task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
points: 8,
|
|
wantPoints: 8,
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "valid points on editing task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
t := newTestTask()
|
|
_ = s.CreateTask(t)
|
|
tc.StartEditSession(t.ID)
|
|
},
|
|
points: 3,
|
|
wantPoints: 3,
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "invalid points - negative",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
points: -1,
|
|
wantSuccess: false,
|
|
},
|
|
{
|
|
name: "no active task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
// Don't set up any task
|
|
},
|
|
points: 5,
|
|
wantSuccess: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
tt.setupTask(tc, taskStore)
|
|
|
|
got := tc.SavePoints(tt.points)
|
|
|
|
if got != tt.wantSuccess {
|
|
t.Errorf("SavePoints() = %v, want %v", got, tt.wantSuccess)
|
|
}
|
|
|
|
if tt.wantSuccess {
|
|
var actualPoints int
|
|
if tc.draftTask != nil {
|
|
actualPoints = tc.draftTask.Points
|
|
} else if tc.editingTask != nil {
|
|
actualPoints = tc.editingTask.Points
|
|
}
|
|
if actualPoints != tt.wantPoints {
|
|
t.Errorf("task.Points = %v, want %v", actualPoints, tt.wantPoints)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTaskController_SaveTitle(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupTask func(*TaskController, store.Store)
|
|
title string
|
|
wantTitle string
|
|
wantSuccess bool
|
|
}{
|
|
{
|
|
name: "valid title on draft task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
title: "New Title",
|
|
wantTitle: "New Title",
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "valid title on editing task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
t := newTestTask()
|
|
_ = s.CreateTask(t)
|
|
tc.StartEditSession(t.ID)
|
|
},
|
|
title: "Updated Title",
|
|
wantTitle: "Updated Title",
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "draft takes priority over editing",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
t := newTestTask()
|
|
_ = s.CreateTask(t)
|
|
tc.StartEditSession(t.ID)
|
|
tc.SetDraft(newTestTaskWithID())
|
|
},
|
|
title: "Draft Title",
|
|
wantTitle: "Draft Title",
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "no active task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
// Don't set up any task
|
|
},
|
|
title: "Title",
|
|
wantSuccess: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
tt.setupTask(tc, taskStore)
|
|
|
|
got := tc.SaveTitle(tt.title)
|
|
|
|
if got != tt.wantSuccess {
|
|
t.Errorf("SaveTitle() = %v, want %v", got, tt.wantSuccess)
|
|
}
|
|
|
|
if tt.wantSuccess {
|
|
var actualTitle string
|
|
if tc.draftTask != nil {
|
|
actualTitle = tc.draftTask.Title
|
|
} else if tc.editingTask != nil {
|
|
actualTitle = tc.editingTask.Title
|
|
}
|
|
if actualTitle != tt.wantTitle {
|
|
t.Errorf("task.Title = %q, want %q", actualTitle, tt.wantTitle)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTaskController_SaveDescription(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupTask func(*TaskController, store.Store)
|
|
description string
|
|
wantDescription string
|
|
wantSuccess bool
|
|
}{
|
|
{
|
|
name: "valid description on draft task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
description: "New description",
|
|
wantDescription: "New description",
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "valid description on editing task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
t := newTestTask()
|
|
_ = s.CreateTask(t)
|
|
tc.StartEditSession(t.ID)
|
|
},
|
|
description: "Updated description",
|
|
wantDescription: "Updated description",
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "no active task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
// Don't set up any task
|
|
},
|
|
description: "Description",
|
|
wantSuccess: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
tt.setupTask(tc, taskStore)
|
|
|
|
got := tc.SaveDescription(tt.description)
|
|
|
|
if got != tt.wantSuccess {
|
|
t.Errorf("SaveDescription() = %v, want %v", got, tt.wantSuccess)
|
|
}
|
|
|
|
if tt.wantSuccess {
|
|
var actualDescription string
|
|
if tc.draftTask != nil {
|
|
actualDescription = tc.draftTask.Description
|
|
} else if tc.editingTask != nil {
|
|
actualDescription = tc.editingTask.Description
|
|
}
|
|
if actualDescription != tt.wantDescription {
|
|
t.Errorf("task.Description = %q, want %q", actualDescription, tt.wantDescription)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test Edit Session Management
|
|
|
|
func TestTaskController_CommitEditSession_Draft(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
draft := newTestTaskWithID()
|
|
draft.Title = "Draft Title"
|
|
tc.SetDraft(draft)
|
|
|
|
err := tc.CommitEditSession()
|
|
if err != nil {
|
|
t.Fatalf("CommitEditSession failed: %v", err)
|
|
}
|
|
|
|
// Verify draft was cleared
|
|
if tc.GetDraftTask() != nil {
|
|
t.Error("CommitEditSession did not clear draft")
|
|
}
|
|
|
|
// Verify task was created in store
|
|
created := taskStore.GetTask("DRAFT-1")
|
|
if created == nil {
|
|
t.Fatal("Task was not created in store")
|
|
}
|
|
|
|
if created.Title != "Draft Title" {
|
|
t.Errorf("Created task has wrong title, got %q, want %q", created.Title, "Draft Title")
|
|
}
|
|
}
|
|
|
|
func TestTaskController_CommitEditSession_DraftValidationFailure(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
draft := newTestTaskWithID()
|
|
draft.Title = "" // Invalid - empty title
|
|
tc.SetDraft(draft)
|
|
|
|
err := tc.CommitEditSession()
|
|
if err != nil {
|
|
// Note: Current implementation returns nil on validation failure for drafts
|
|
// and just logs a warning. This test documents that behavior.
|
|
t.Logf("CommitEditSession returned error as expected: %v", err)
|
|
}
|
|
|
|
// Draft should still exist since validation failed
|
|
if tc.GetDraftTask() == nil {
|
|
t.Error("Draft was cleared despite validation failure")
|
|
}
|
|
}
|
|
|
|
func TestTaskController_CommitEditSession_Existing(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
// Create original task
|
|
original := newTestTask()
|
|
_ = taskStore.CreateTask(original)
|
|
|
|
// Start edit session and modify
|
|
tc.StartEditSession(original.ID)
|
|
tc.editingTask.Title = "Modified Title"
|
|
|
|
err := tc.CommitEditSession()
|
|
if err != nil {
|
|
t.Fatalf("CommitEditSession failed: %v", err)
|
|
}
|
|
|
|
// Verify editing task was cleared
|
|
if tc.GetEditingTask() != nil {
|
|
t.Error("CommitEditSession did not clear editingTask")
|
|
}
|
|
|
|
// Verify task was updated in store
|
|
updated := taskStore.GetTask(original.ID)
|
|
if updated == nil {
|
|
t.Fatal("Task not found in store")
|
|
}
|
|
|
|
if updated.Title != "Modified Title" {
|
|
t.Errorf("Task was not updated, got title %q, want %q", updated.Title, "Modified Title")
|
|
}
|
|
}
|
|
|
|
func TestTaskController_CommitEditSession_NoActiveSession(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
err := tc.CommitEditSession()
|
|
if err != nil {
|
|
t.Errorf("CommitEditSession with no active session should return nil, got error: %v", err)
|
|
}
|
|
}
|
|
|
|
// Test Helper Methods
|
|
|
|
func TestTaskController_GetCurrentTask(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
// Create task
|
|
original := newTestTask()
|
|
_ = taskStore.CreateTask(original)
|
|
|
|
// Set as current
|
|
tc.SetCurrentTask(original.ID)
|
|
|
|
current := tc.GetCurrentTask()
|
|
if current == nil {
|
|
t.Fatal("GetCurrentTask returned nil")
|
|
}
|
|
|
|
if current.ID != original.ID {
|
|
t.Errorf("GetCurrentTask returned wrong task, got ID %q, want %q", current.ID, original.ID)
|
|
}
|
|
}
|
|
|
|
func TestTaskController_GetCurrentTask_Empty(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
current := tc.GetCurrentTask()
|
|
if current != nil {
|
|
t.Error("GetCurrentTask should return nil when currentTaskID is empty")
|
|
}
|
|
}
|
|
|
|
func TestTaskController_GetCurrentTask_NonExistent(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
tc.SetCurrentTask("NONEXISTENT")
|
|
|
|
current := tc.GetCurrentTask()
|
|
if current != nil {
|
|
t.Error("GetCurrentTask should return nil for non-existent task")
|
|
}
|
|
}
|
|
|
|
// Test Action Registry
|
|
|
|
func TestTaskController_GetActionRegistry(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
registry := tc.GetActionRegistry()
|
|
if registry == nil {
|
|
t.Error("GetActionRegistry returned nil")
|
|
}
|
|
|
|
// Verify it's the task detail registry (should have some actions)
|
|
actions := registry.GetActions()
|
|
if len(actions) == 0 {
|
|
t.Error("Task detail action registry has no actions")
|
|
}
|
|
}
|
|
|
|
func TestTaskController_GetEditActionRegistry(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
registry := tc.GetEditActionRegistry()
|
|
if registry == nil {
|
|
t.Error("GetEditActionRegistry returned nil")
|
|
}
|
|
|
|
// Verify it's the edit registry (should have some actions)
|
|
actions := registry.GetActions()
|
|
if len(actions) == 0 {
|
|
t.Error("Task edit action registry has no actions")
|
|
}
|
|
}
|
|
|
|
// Test Focused Field
|
|
|
|
func TestTaskController_FocusedField(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
// Initially should be empty
|
|
if tc.GetFocusedField() != "" {
|
|
t.Errorf("Initial focused field should be empty, got %v", tc.GetFocusedField())
|
|
}
|
|
|
|
// Set focused field
|
|
tc.SetFocusedField(model.EditFieldTitle)
|
|
if tc.GetFocusedField() != model.EditFieldTitle {
|
|
t.Errorf("SetFocusedField did not set field, got %v, want %v", tc.GetFocusedField(), model.EditFieldTitle)
|
|
}
|
|
}
|
|
|
|
func TestTaskController_SaveDue(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupTask func(*TaskController, store.Store)
|
|
dateStr string
|
|
wantDue string // expected Format(DateFormat) or "" for zero
|
|
wantSuccess bool
|
|
}{
|
|
{
|
|
name: "valid date on draft task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
dateStr: "2025-06-15",
|
|
wantDue: "2025-06-15",
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "valid date on editing task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
t := newTestTask()
|
|
_ = s.CreateTask(t)
|
|
tc.StartEditSession(t.ID)
|
|
},
|
|
dateStr: "2025-12-31",
|
|
wantDue: "2025-12-31",
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "empty string clears due date",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
draft := newTestTask()
|
|
draft.Due = time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
|
|
tc.SetDraft(draft)
|
|
},
|
|
dateStr: "",
|
|
wantDue: "",
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "invalid date returns false",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
dateStr: "not-a-date",
|
|
wantSuccess: false,
|
|
},
|
|
{
|
|
name: "no active task returns false",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
// no task set up
|
|
},
|
|
dateStr: "2025-06-15",
|
|
wantSuccess: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
tt.setupTask(tc, taskStore)
|
|
|
|
got := tc.SaveDue(tt.dateStr)
|
|
|
|
if got != tt.wantSuccess {
|
|
t.Errorf("SaveDue() = %v, want %v", got, tt.wantSuccess)
|
|
}
|
|
|
|
if tt.wantSuccess {
|
|
var actualDue time.Time
|
|
if tc.draftTask != nil {
|
|
actualDue = tc.draftTask.Due
|
|
} else if tc.editingTask != nil {
|
|
actualDue = tc.editingTask.Due
|
|
}
|
|
|
|
var actualStr string
|
|
if !actualDue.IsZero() {
|
|
actualStr = actualDue.Format(task.DateFormat)
|
|
}
|
|
if actualStr != tt.wantDue {
|
|
t.Errorf("task.Due = %q, want %q", actualStr, tt.wantDue)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTaskController_SaveTags(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupTask func(*TaskController, store.Store)
|
|
tags []string
|
|
wantTags []string
|
|
wantSuccess bool
|
|
}{
|
|
{
|
|
name: "valid tags on draft task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
tags: []string{"api", "backend"},
|
|
wantTags: []string{"api", "backend"},
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "valid tags on editing task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
t := newTestTask()
|
|
_ = s.CreateTask(t)
|
|
tc.StartEditSession(t.ID)
|
|
},
|
|
tags: []string{"frontend", "ui"},
|
|
wantTags: []string{"frontend", "ui"},
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "empty tags slice",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
tags: []string{},
|
|
wantTags: []string{},
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "nil tags",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
tc.SetDraft(newTestTask())
|
|
},
|
|
tags: nil,
|
|
wantTags: nil,
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "draft takes priority over editing",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
t := newTestTask()
|
|
_ = s.CreateTask(t)
|
|
tc.StartEditSession(t.ID)
|
|
tc.SetDraft(newTestTaskWithID())
|
|
},
|
|
tags: []string{"draft-tag"},
|
|
wantTags: []string{"draft-tag"},
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "no active task",
|
|
setupTask: func(tc *TaskController, s store.Store) {
|
|
// no task set up
|
|
},
|
|
tags: []string{"orphan"},
|
|
wantSuccess: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
taskStore := store.NewInMemoryStore()
|
|
navController := newMockNavigationController()
|
|
tc := NewTaskController(taskStore, navController, nil)
|
|
|
|
tt.setupTask(tc, taskStore)
|
|
|
|
got := tc.SaveTags(tt.tags)
|
|
|
|
if got != tt.wantSuccess {
|
|
t.Errorf("SaveTags() = %v, want %v", got, tt.wantSuccess)
|
|
}
|
|
|
|
if tt.wantSuccess {
|
|
var actualTags []string
|
|
if tc.draftTask != nil {
|
|
actualTags = tc.draftTask.Tags
|
|
} else if tc.editingTask != nil {
|
|
actualTags = tc.editingTask.Tags
|
|
}
|
|
if !slices.Equal(actualTags, tt.wantTags) {
|
|
t.Errorf("task.Tags = %v, want %v", actualTags, tt.wantTags)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|