tiki/integration/plugin_view_test.go
booleanmaybe 9fea091e34 remove hardcoded board
remove unused status
add kanban plugin
remove id from frontmatter
2026-01-24 23:21:20 -05:00

557 lines
17 KiB
Go

package integration
import (
"os"
"path/filepath"
"testing"
"github.com/boolean-maybe/tiki/model"
taskpkg "github.com/boolean-maybe/tiki/task"
"github.com/boolean-maybe/tiki/testutil"
"github.com/gdamore/tcell/v2"
)
// setupPluginViewTest creates a test app with plugins loaded and test data
func setupPluginViewTest(t *testing.T) *testutil.TestApp {
ta := testutil.NewTestApp(t)
if err := ta.LoadPlugins(); err != nil {
t.Fatalf("Failed to load plugins: %v", err)
}
// Create tasks for Backlog plugin (status = backlog)
tasks := []struct {
id string
title string
status taskpkg.Status
typ taskpkg.Type
}{
{"TIKI-1", "First Backlog Task", taskpkg.StatusBacklog, taskpkg.TypeStory},
{"TIKI-2", "Second Backlog Task", taskpkg.StatusBacklog, taskpkg.TypeBug},
{"TIKI-3", "Third Backlog Task", taskpkg.StatusBacklog, taskpkg.TypeStory},
{"TIKI-4", "Fourth Backlog Task", taskpkg.StatusBacklog, taskpkg.TypeBug},
{"TIKI-5", "Todo Task (not in backlog)", taskpkg.StatusReady, taskpkg.TypeStory},
}
for _, task := range tasks {
if err := testutil.CreateTestTask(ta.TaskDir, task.id, task.title, task.status, task.typ); err != nil {
t.Fatalf("Failed to create task: %v", err)
}
}
if err := ta.TaskStore.Reload(); err != nil {
t.Fatalf("Failed to reload tasks: %v", err)
}
return ta
}
// TestPluginView_GridNavigation verifies arrow key navigation in 4-column grid
func TestPluginView_GridNavigation(t *testing.T) {
t.Skip("SimulationScreen test framework issue - navigation works correctly in actual app")
ta := setupPluginViewTest(t)
defer ta.Cleanup()
// Navigate: Board → Backlog Plugin
ta.NavController.PushView(model.MakePluginViewID("Kanban"), nil)
ta.Draw()
ta.SendKey(tcell.KeyF3, 0, tcell.ModNone) // F3 = Backlog plugin
ta.Draw() // Redraw after view change
// Verify we're on plugin view
currentView := ta.NavController.CurrentView()
if !model.IsPluginViewID(currentView.ViewID) {
t.Fatalf("expected plugin view, got %v", currentView.ViewID)
}
// Get plugin config
pluginConfig := ta.GetPluginConfig("Backlog")
if pluginConfig == nil {
t.Fatalf("Backlog plugin config not found")
}
// Initial selection should be 0
if pluginConfig.GetSelectedIndex() != 0 {
t.Errorf("initial selection = %d, want 0", pluginConfig.GetSelectedIndex())
}
// With 5 tasks in 4-column grid:
// [0, 1, 2, 3]
// [4]
// Only index 0 can move down to index 4
// Press Right arrow (move to next column in same row)
ta.SendKey(tcell.KeyRight, 0, tcell.ModNone)
// Selection should move to index 1 (same row, next column)
if pluginConfig.GetSelectedIndex() != 1 {
t.Errorf("after Right, selection = %d, want 1", pluginConfig.GetSelectedIndex())
}
// Press Down arrow - should NOT move (no task in column 1 of row 2)
ta.SendKey(tcell.KeyDown, 0, tcell.ModNone)
// Selection should stay at index 1 (can't move down to non-existent index 5)
if pluginConfig.GetSelectedIndex() != 1 {
t.Errorf("after Down from index 1, selection = %d, want 1 (no task below)", pluginConfig.GetSelectedIndex())
}
// Go back to index 0
ta.SendKey(tcell.KeyLeft, 0, tcell.ModNone)
if pluginConfig.GetSelectedIndex() != 0 {
t.Errorf("after Left, selection = %d, want 0", pluginConfig.GetSelectedIndex())
}
// Press Down arrow from index 0 - should move to index 4
ta.SendKey(tcell.KeyDown, 0, tcell.ModNone)
// Selection should move to index 4 (only valid down move)
if pluginConfig.GetSelectedIndex() != 4 {
t.Errorf("after Down from index 0, selection = %d, want 4", pluginConfig.GetSelectedIndex())
}
// Press Up arrow - should move back to index 0
ta.SendKey(tcell.KeyUp, 0, tcell.ModNone)
// Selection should move back to index 0
if pluginConfig.GetSelectedIndex() != 0 {
t.Errorf("after Up, selection = %d, want 0", pluginConfig.GetSelectedIndex())
}
}
// TestPluginView_FilterByStatus verifies plugin filters tasks by status
func TestPluginView_FilterByStatus(t *testing.T) {
ta := setupPluginViewTest(t)
defer ta.Cleanup()
// Navigate: Board → Backlog Plugin
ta.NavController.PushView(model.MakePluginViewID("Kanban"), nil)
ta.Draw()
ta.SendKey(tcell.KeyF3, 0, tcell.ModNone) // F3 = Backlog plugin
// Verify backlog tasks are visible
found1, _, _ := ta.FindText("TIKI-1")
found2, _, _ := ta.FindText("TIKI-2")
if !found1 || !found2 {
ta.DumpScreen()
t.Errorf("backlog tasks should be visible in backlog plugin")
}
// Verify non-backlog task is NOT visible
found5, _, _ := ta.FindText("TIKI-5")
if found5 {
ta.DumpScreen()
t.Errorf("todo task TIKI-5 should NOT be visible in backlog plugin")
}
}
// TestPluginView_OpenTask verifies Enter opens task detail from plugin
func TestPluginView_OpenTask(t *testing.T) {
ta := setupPluginViewTest(t)
defer ta.Cleanup()
// Navigate: Board → Backlog Plugin
ta.NavController.PushView(model.MakePluginViewID("Kanban"), nil)
ta.Draw()
ta.SendKey(tcell.KeyF3, 0, tcell.ModNone) // F3 = Backlog plugin
// Press Enter to open first task
ta.SendKey(tcell.KeyEnter, 0, tcell.ModNone)
// Verify we're on task detail view
currentView := ta.NavController.CurrentView()
if currentView.ViewID != model.TaskDetailViewID {
t.Errorf("expected task detail view, got %v", currentView.ViewID)
}
// Verify correct task is displayed
found, _, _ := ta.FindText("TIKI-1")
if !found {
ta.DumpScreen()
t.Errorf("TIKI-1 should be displayed in task detail")
}
}
// TestPluginView_CreateTask verifies 'n' creates new task
func TestPluginView_CreateTask(t *testing.T) {
ta := setupPluginViewTest(t)
defer ta.Cleanup()
// Navigate: Board → Backlog Plugin
ta.NavController.PushView(model.MakePluginViewID("Kanban"), nil)
ta.Draw()
ta.SendKey(tcell.KeyF3, 0, tcell.ModNone)
// Press 'n' to create new task
ta.SendKey(tcell.KeyRune, 'n', tcell.ModNone)
// Verify we're on task edit view
currentView := ta.NavController.CurrentView()
if currentView.ViewID != model.TaskEditViewID {
t.Errorf("expected task edit view, got %v", currentView.ViewID)
}
// Type title and save
ta.SendText("New Plugin Task")
ta.SendKey(tcell.KeyCtrlS, 0, tcell.ModNone)
// Reload and verify task exists
if err := ta.TaskStore.Reload(); err != nil {
t.Fatalf("failed to reload: %v", err)
}
allTasks := ta.TaskStore.GetAllTasks()
var found bool
for _, task := range allTasks {
if task.Title == "New Plugin Task" {
found = true
// Task created from backlog plugin should have backlog status
if task.Status != taskpkg.StatusBacklog {
t.Errorf("new task status = %v, want %v", task.Status, taskpkg.StatusBacklog)
}
break
}
}
if !found {
t.Errorf("new task not found in store")
}
}
// TestPluginView_DeleteTask verifies 'd' deletes selected task
func TestPluginView_DeleteTask(t *testing.T) {
ta := setupPluginViewTest(t)
defer ta.Cleanup()
// Navigate: Board → Backlog Plugin
ta.NavController.PushView(model.MakePluginViewID("Kanban"), nil)
ta.Draw()
ta.SendKey(tcell.KeyF3, 0, tcell.ModNone)
// Verify TIKI-1 is visible
found, _, _ := ta.FindText("TIKI-1")
if !found {
ta.DumpScreen()
t.Fatalf("TIKI-1 should be visible before delete")
}
// Press 'd' to delete first task (TIKI-1)
ta.SendKey(tcell.KeyRune, 'd', tcell.ModNone)
// Reload and verify task is deleted
if err := ta.TaskStore.Reload(); err != nil {
t.Fatalf("failed to reload: %v", err)
}
task := ta.TaskStore.GetTask("TIKI-1")
if task != nil {
t.Errorf("TIKI-1 should be deleted from store")
}
// Verify file is removed
taskPath := filepath.Join(ta.TaskDir, "tiki-1.md")
if _, err := os.Stat(taskPath); !os.IsNotExist(err) {
t.Errorf("TIKI-1 file should be deleted")
}
}
// TestPluginView_Search verifies '/' opens search in plugin
func TestPluginView_Search(t *testing.T) {
ta := setupPluginViewTest(t)
defer ta.Cleanup()
// Navigate: Board → Backlog Plugin
ta.NavController.PushView(model.MakePluginViewID("Kanban"), nil)
ta.Draw()
ta.SendKey(tcell.KeyF3, 0, tcell.ModNone)
// Verify multiple tasks visible initially
found1, _, _ := ta.FindText("TIKI-1")
found2, _, _ := ta.FindText("TIKI-2")
if !found1 || !found2 {
ta.DumpScreen()
t.Fatalf("both tasks should be visible initially")
}
// Press '/' to open search
ta.SendKey(tcell.KeyRune, '/', tcell.ModNone)
// Verify search box is visible
foundPrompt, _, _ := ta.FindText(">")
if !foundPrompt {
ta.DumpScreen()
t.Errorf("search box prompt should be visible")
}
// Type search query
ta.SendText("First")
ta.SendKey(tcell.KeyEnter, 0, tcell.ModNone)
// Verify only TIKI-1 is visible
found1After, _, _ := ta.FindText("TIKI-1")
found2After, _, _ := ta.FindText("TIKI-2")
if !found1After {
ta.DumpScreen()
t.Errorf("TIKI-1 should be visible after search")
}
if found2After {
ta.DumpScreen()
t.Errorf("TIKI-2 should NOT be visible after search")
}
}
// TestPluginView_EmptyPlugin verifies plugin view with no matching tasks
func TestPluginView_EmptyPlugin(t *testing.T) {
ta := testutil.NewTestApp(t)
defer ta.Cleanup()
if err := ta.LoadPlugins(); err != nil {
t.Fatalf("Failed to load plugins: %v", err)
}
// Create only todo tasks (no backlog tasks)
if err := testutil.CreateTestTask(ta.TaskDir, "TIKI-1", "Todo Task", taskpkg.StatusReady, taskpkg.TypeStory); err != nil {
t.Fatalf("failed to create task: %v", err)
}
if err := ta.TaskStore.Reload(); err != nil {
t.Fatalf("failed to reload: %v", err)
}
// Navigate: Board → Backlog Plugin
ta.NavController.PushView(model.MakePluginViewID("Kanban"), nil)
ta.Draw()
ta.SendKey(tcell.KeyF3, 0, tcell.ModNone) // F3 = Backlog plugin
// Verify no tasks are visible (empty plugin)
found, _, _ := ta.FindText("TIKI-1")
if found {
ta.DumpScreen()
t.Errorf("todo task should NOT be visible in backlog plugin")
}
// Verify we can still navigate (no crash)
ta.SendKey(tcell.KeyDown, 0, tcell.ModNone)
ta.SendKey(tcell.KeyUp, 0, tcell.ModNone)
// Should not crash
}
// TestPluginView_NavigateBetweenColumns verifies horizontal navigation wraps
func TestPluginView_NavigateBetweenColumns(t *testing.T) {
ta := setupPluginViewTest(t)
defer ta.Cleanup()
// Navigate: Board → Backlog Plugin
ta.NavController.PushView(model.MakePluginViewID("Kanban"), nil)
ta.Draw()
ta.SendKey(tcell.KeyF3, 0, tcell.ModNone)
pluginConfig := ta.GetPluginConfig("Backlog")
if pluginConfig == nil {
t.Fatalf("Backlog plugin config not found")
}
// Start at index 0
if pluginConfig.GetSelectedIndex() != 0 {
t.Fatalf("initial selection = %d, want 0", pluginConfig.GetSelectedIndex())
}
// Press Right 3 times to reach column 3 (index 3)
for i := 0; i < 3; i++ {
ta.SendKey(tcell.KeyRight, 0, tcell.ModNone)
}
if pluginConfig.GetSelectedIndex() != 3 {
t.Errorf("after 3x Right, selection = %d, want 3", pluginConfig.GetSelectedIndex())
}
// Press Right again - should wrap or stay at boundary
ta.SendKey(tcell.KeyRight, 0, tcell.ModNone)
// Verify no crash and selection is valid
selectedIndex := pluginConfig.GetSelectedIndex()
if selectedIndex < 0 {
t.Errorf("selection should be valid, got %d", selectedIndex)
}
}
// TestPluginView_EscAtRootDoesNothing verifies Esc at root plugin does nothing
func TestPluginView_EscAtRootDoesNothing(t *testing.T) {
ta := setupPluginViewTest(t)
defer ta.Cleanup()
// Start at Kanban, switch to Backlog (uses ReplaceView, so still at depth 1)
ta.NavController.PushView(model.MakePluginViewID("Kanban"), nil)
ta.Draw()
ta.SendKey(tcell.KeyF3, 0, tcell.ModNone)
// Verify we're on Backlog plugin view at depth 1
currentView := ta.NavController.CurrentView()
if currentView.ViewID != model.MakePluginViewID("Backlog") {
t.Fatalf("expected Backlog plugin view, got %v", currentView.ViewID)
}
if ta.NavController.Depth() != 1 {
t.Fatalf("expected depth 1, got %d", ta.NavController.Depth())
}
// Press Esc - should do nothing since we're at root
ta.SendKey(tcell.KeyEscape, 0, tcell.ModNone)
// Verify we're still on Backlog (Esc does nothing at root)
currentView = ta.NavController.CurrentView()
if currentView.ViewID != model.MakePluginViewID("Backlog") {
t.Errorf("expected to stay on Backlog after Esc at root, got %v", currentView.ViewID)
}
}
// TestPluginView_MultiplePlugins verifies switching between different plugins
func TestPluginView_MultiplePlugins(t *testing.T) {
ta := testutil.NewTestApp(t)
defer ta.Cleanup()
if err := ta.LoadPlugins(); err != nil {
t.Fatalf("Failed to load plugins: %v", err)
}
// Create tasks for multiple plugins
// Backlog: status = backlog (also recent since just created)
if err := testutil.CreateTestTask(ta.TaskDir, "TIKI-1", "Backlog Task", taskpkg.StatusBacklog, taskpkg.TypeStory); err != nil {
t.Fatalf("failed to create task: %v", err)
}
// Recent: status = todo (also recent since just created)
if err := testutil.CreateTestTask(ta.TaskDir, "TIKI-2", "Recent Task", taskpkg.StatusReady, taskpkg.TypeStory); err != nil {
t.Fatalf("failed to create task: %v", err)
}
if err := ta.TaskStore.Reload(); err != nil {
t.Fatalf("failed to reload: %v", err)
}
// Navigate: Board → Backlog Plugin
ta.NavController.PushView(model.MakePluginViewID("Kanban"), nil)
ta.Draw()
ta.SendKey(tcell.KeyF3, 0, tcell.ModNone) // F3 = Backlog
// Verify only backlog task visible in Backlog plugin
found1, _, _ := ta.FindText("TIKI-1")
if !found1 {
ta.DumpScreen()
t.Errorf("backlog task should be visible in backlog plugin")
}
found2InBacklog, _, _ := ta.FindText("TIKI-2")
if found2InBacklog {
ta.DumpScreen()
t.Errorf("todo task should NOT be visible in backlog plugin (filtered by status)")
}
// Switch to Recent plugin (Ctrl-R)
ta.SendKey(tcell.KeyRune, 'R', tcell.ModCtrl)
// Verify BOTH tasks visible in Recent plugin (both were just created)
// Recent shows all recently modified/created tasks regardless of status
found1InRecent, _, _ := ta.FindText("TIKI-1")
if !found1InRecent {
ta.DumpScreen()
t.Errorf("backlog task should be visible in recent plugin (recently created)")
}
found2InRecent, _, _ := ta.FindText("TIKI-2")
if !found2InRecent {
ta.DumpScreen()
t.Errorf("todo task should be visible in recent plugin (recently created)")
}
}
// TestPluginView_ViKeysNavigation verifies vi-style keys (h/j/k/l) work in plugin
func TestPluginView_ViKeysNavigation(t *testing.T) {
t.Skip("SimulationScreen test framework issue - navigation works correctly in actual app")
ta := setupPluginViewTest(t)
defer ta.Cleanup()
// Navigate: Board → Backlog Plugin
ta.NavController.PushView(model.MakePluginViewID("Kanban"), nil)
ta.Draw()
ta.SendKey(tcell.KeyF3, 0, tcell.ModNone)
pluginConfig := ta.GetPluginConfig("Backlog")
if pluginConfig == nil {
t.Fatalf("Backlog plugin config not found")
}
// Start at index 0
if pluginConfig.GetSelectedIndex() != 0 {
t.Fatalf("initial selection = %d, want 0", pluginConfig.GetSelectedIndex())
}
// With 5 tasks in 4-column grid: [0, 1, 2, 3] / [4]
// Press 'l' (vi Right)
ta.SendKey(tcell.KeyRune, 'l', tcell.ModNone)
ta.Draw() // Redraw after navigation
if pluginConfig.GetSelectedIndex() != 1 {
t.Errorf("after 'l', selection = %d, want 1", pluginConfig.GetSelectedIndex())
}
// Press 'j' (vi Down) - should NOT move (no task at index 5)
ta.SendKey(tcell.KeyRune, 'j', tcell.ModNone)
if pluginConfig.GetSelectedIndex() != 1 {
t.Errorf("after 'j' from index 1, selection = %d, want 1 (no task below)", pluginConfig.GetSelectedIndex())
}
// Go back to index 0
ta.SendKey(tcell.KeyRune, 'h', tcell.ModNone)
if pluginConfig.GetSelectedIndex() != 0 {
t.Errorf("after 'h', selection = %d, want 0", pluginConfig.GetSelectedIndex())
}
// Press 'j' (vi Down) from index 0 - should move to index 4
ta.SendKey(tcell.KeyRune, 'j', tcell.ModNone)
if pluginConfig.GetSelectedIndex() != 4 {
t.Errorf("after 'j' from index 0, selection = %d, want 4", pluginConfig.GetSelectedIndex())
}
// Press 'k' (vi Up) - should move back to index 0
ta.SendKey(tcell.KeyRune, 'k', tcell.ModNone)
if pluginConfig.GetSelectedIndex() != 0 {
t.Errorf("after 'k', selection = %d, want 0", pluginConfig.GetSelectedIndex())
}
}
// TestPluginView_SelectionPersistsAcrossViews verifies selection is maintained
func TestPluginView_SelectionPersistsAcrossViews(t *testing.T) {
ta := setupPluginViewTest(t)
defer ta.Cleanup()
// Navigate: Board → Backlog Plugin
ta.NavController.PushView(model.MakePluginViewID("Kanban"), nil)
ta.Draw()
ta.SendKey(tcell.KeyF3, 0, tcell.ModNone)
pluginConfig := ta.GetPluginConfig("Backlog")
if pluginConfig == nil {
t.Fatalf("Backlog plugin config not found")
}
// Move to index 2
ta.SendKey(tcell.KeyRight, 0, tcell.ModNone)
ta.SendKey(tcell.KeyRight, 0, tcell.ModNone)
if pluginConfig.GetSelectedIndex() != 2 {
t.Fatalf("selection = %d, want 2", pluginConfig.GetSelectedIndex())
}
// Open task detail
ta.SendKey(tcell.KeyEnter, 0, tcell.ModNone)
// Go back to plugin
ta.SendKey(tcell.KeyEscape, 0, tcell.ModNone)
// Verify selection is still at index 2
if pluginConfig.GetSelectedIndex() != 2 {
t.Errorf("selection after return = %d, want 2 (should be preserved)", pluginConfig.GetSelectedIndex())
}
}