tiki/controller/view_stack_test.go
2026-01-17 11:08:53 -05:00

371 lines
9.3 KiB
Go

package controller
import (
"testing"
"github.com/boolean-maybe/tiki/model"
)
func TestNavigationState_PushPop(t *testing.T) {
nav := newViewStack()
// Push first view
nav.push(model.BoardViewID, nil)
// Verify depth
if nav.depth() != 1 {
t.Errorf("depth = %d, want 1", nav.depth())
}
// Push second view with params
params := model.EncodeTaskDetailParams(model.TaskDetailParams{TaskID: "TIKI-1"})
nav.push(model.TaskDetailViewID, params)
// Verify depth
if nav.depth() != 2 {
t.Errorf("depth = %d, want 2", nav.depth())
}
// Pop should return task detail view
entry := nav.pop()
if entry == nil {
t.Fatal("pop() returned nil, want ViewEntry")
}
if entry.ViewID != model.TaskDetailViewID {
t.Errorf("ViewID = %v, want %v", entry.ViewID, model.TaskDetailViewID)
}
if model.DecodeTaskDetailParams(entry.Params).TaskID != "TIKI-1" {
t.Errorf("taskID param = %v, want TIKI-1", model.DecodeTaskDetailParams(entry.Params).TaskID)
}
// Verify depth decreased
if nav.depth() != 1 {
t.Errorf("depth after pop = %d, want 1", nav.depth())
}
// Pop should return board view
entry = nav.pop()
if entry == nil {
t.Fatal("pop() returned nil, want ViewEntry")
}
if entry.ViewID != model.BoardViewID {
t.Errorf("ViewID = %v, want %v", entry.ViewID, model.BoardViewID)
}
// Stack should be empty
if nav.depth() != 0 {
t.Errorf("depth after second pop = %d, want 0", nav.depth())
}
// Pop on empty stack should return nil
entry = nav.pop()
if entry != nil {
t.Error("pop() on empty stack should return nil")
}
}
func TestNavigationState_CurrentView(t *testing.T) {
nav := newViewStack()
// CurrentView on empty stack should return nil
entry := nav.currentView()
if entry != nil {
t.Error("currentView() on empty stack should return nil")
}
// Push views
nav.push(model.BoardViewID, nil)
nav.push(model.TaskEditViewID, nil)
// CurrentView should return task edit (top) without removing it
entry = nav.currentView()
if entry == nil {
t.Fatal("currentView() returned nil")
}
if entry.ViewID != model.TaskEditViewID {
t.Errorf("ViewID = %v, want %v", entry.ViewID, model.TaskEditViewID)
}
// Depth should not change
if nav.depth() != 2 {
t.Error("currentView() should not modify stack")
}
// Calling CurrentView again should return same view
entry2 := nav.currentView()
if entry2.ViewID != model.TaskEditViewID {
t.Error("currentView() should consistently return top view")
}
}
func TestNavigationState_CurrentViewID(t *testing.T) {
nav := newViewStack()
// Empty stack
if nav.currentViewID() != "" {
t.Errorf("currentViewID() on empty stack = %v, want empty string", nav.currentViewID())
}
// With views
nav.push(model.BoardViewID, nil)
if nav.currentViewID() != model.BoardViewID {
t.Errorf("currentViewID() = %v, want %v", nav.currentViewID(), model.BoardViewID)
}
nav.push(model.TaskDetailViewID, nil)
if nav.currentViewID() != model.TaskDetailViewID {
t.Errorf("currentViewID() = %v, want %v", nav.currentViewID(), model.TaskDetailViewID)
}
}
func TestNavigationState_PreviousView(t *testing.T) {
nav := newViewStack()
// Empty stack
entry := nav.previousView()
if entry != nil {
t.Error("previousView() on empty stack should return nil")
}
// Single view - no previous
nav.push(model.BoardViewID, nil)
entry = nav.previousView()
if entry != nil {
t.Error("previousView() with depth 1 should return nil")
}
// Two views - should return first
nav.push(model.TaskDetailViewID, nil)
entry = nav.previousView()
if entry == nil {
t.Fatal("previousView() returned nil, want ViewEntry")
}
if entry.ViewID != model.BoardViewID {
t.Errorf("previousView() ViewID = %v, want %v", entry.ViewID, model.BoardViewID)
}
// Three views - should return second
nav.push(model.TaskEditViewID, model.EncodeTaskEditParams(model.TaskEditParams{TaskID: "TIKI-5"}))
entry = nav.previousView()
if entry == nil {
t.Fatal("previousView() returned nil")
}
if entry.ViewID != model.TaskDetailViewID {
t.Errorf("previousView() ViewID = %v, want %v", entry.ViewID, model.TaskDetailViewID)
}
// Stack should not be modified
if nav.depth() != 3 {
t.Error("previousView() should not modify stack")
}
}
func TestNavigationState_CanGoBack(t *testing.T) {
nav := newViewStack()
// Empty stack - cannot go back
if nav.canGoBack() {
t.Error("canGoBack() on empty stack should return false")
}
// Single view - cannot go back
nav.push(model.BoardViewID, nil)
if nav.canGoBack() {
t.Error("canGoBack() with depth 1 should return false")
}
// Two views - can go back
nav.push(model.TaskDetailViewID, nil)
if !nav.canGoBack() {
t.Error("canGoBack() with depth 2 should return true")
}
// After pop - cannot go back
nav.pop()
if nav.canGoBack() {
t.Error("canGoBack() after pop to depth 1 should return false")
}
}
func TestNavigationState_Depth(t *testing.T) {
nav := newViewStack()
tests := []struct {
name string
action func()
expectedDepth int
}{
{
name: "initial empty",
action: func() {},
expectedDepth: 0,
},
{
name: "after first push",
action: func() { nav.push(model.BoardViewID, nil) },
expectedDepth: 1,
},
{
name: "after second push",
action: func() { nav.push(model.TaskDetailViewID, nil) },
expectedDepth: 2,
},
{
name: "after third push",
action: func() { nav.push(model.TaskDetailViewID, nil) },
expectedDepth: 3,
},
{
name: "after one pop",
action: func() { nav.pop() },
expectedDepth: 2,
},
{
name: "after two pops",
action: func() { nav.pop(); nav.pop() },
expectedDepth: 0,
},
{
name: "pop on empty stays zero",
action: func() { nav.pop() },
expectedDepth: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.action()
if nav.depth() != tt.expectedDepth {
t.Errorf("depth = %d, want %d", nav.depth(), tt.expectedDepth)
}
})
}
}
func TestNavigationState_Clear(t *testing.T) {
nav := newViewStack()
// Push multiple views
nav.push(model.BoardViewID, nil)
nav.push(model.TaskDetailViewID, nil)
nav.push(model.TaskEditViewID, nil)
// Verify stack has items
if nav.depth() != 3 {
t.Fatalf("depth = %d, want 3", nav.depth())
}
// Clear stack
nav.clear()
// Verify empty
if nav.depth() != 0 {
t.Errorf("depth after clear() = %d, want 0", nav.depth())
}
// Verify operations on cleared stack work correctly
if nav.currentView() != nil {
t.Error("currentView() after clear() should return nil")
}
if nav.canGoBack() {
t.Error("canGoBack() after clear() should return false")
}
// Should be able to push again
nav.push(model.BoardViewID, nil)
if nav.depth() != 1 {
t.Errorf("depth after push on cleared stack = %d, want 1", nav.depth())
}
}
func TestNavigationState_ParameterPassing(t *testing.T) {
nav := newViewStack()
// Push view with nil params
nav.push(model.BoardViewID, nil)
entry := nav.currentView()
if entry.Params != nil {
t.Error("nil params should remain nil")
}
// Push view with empty params
nav.push(model.TaskDetailViewID, map[string]interface{}{})
entry = nav.currentView()
if entry.Params == nil {
t.Error("empty params map should not be nil")
}
if len(entry.Params) != 0 {
t.Errorf("empty params length = %d, want 0", len(entry.Params))
}
// Push view with multiple params
params := model.EncodeTaskDetailParams(model.TaskDetailParams{TaskID: "TIKI-42"})
params["readOnly"] = true
params["index"] = 123
nav.push(model.TaskEditViewID, params)
entry = nav.currentView()
if model.DecodeTaskDetailParams(entry.Params).TaskID != "TIKI-42" {
t.Errorf("taskID param = %v, want TIKI-42", model.DecodeTaskDetailParams(entry.Params).TaskID)
}
if entry.Params["readOnly"] != true {
t.Errorf("readOnly param = %v, want true", entry.Params["readOnly"])
}
if entry.Params["index"] != 123 {
t.Errorf("index param = %v, want 123", entry.Params["index"])
}
// Pop and verify params are preserved
entry = nav.pop()
if model.DecodeTaskDetailParams(entry.Params).TaskID != "TIKI-42" {
t.Error("params should be preserved through pop()")
}
}
func TestNavigationState_ComplexNavigationFlow(t *testing.T) {
nav := newViewStack()
// Simulate: Board -> open task -> back to board -> edit task -> back to board
nav.push(model.BoardViewID, nil)
if nav.currentViewID() != model.BoardViewID {
t.Fatal("should start on board")
}
// Open task from board
nav.push(model.TaskDetailViewID, model.EncodeTaskDetailParams(model.TaskDetailParams{TaskID: "TIKI-1"}))
if nav.depth() != 2 {
t.Error("should have 2 views after opening task")
}
if !nav.canGoBack() {
t.Error("should be able to go back")
}
// Back to board
entry := nav.pop()
if entry.ViewID != model.TaskDetailViewID {
t.Error("should pop task detail")
}
if nav.currentViewID() != model.BoardViewID {
t.Error("should return to board")
}
// Switch to task edit
nav.push(model.TaskEditViewID, nil)
if nav.currentViewID() != model.TaskEditViewID {
t.Error("should be on task edit")
}
// Back to board again
nav.pop()
if nav.currentViewID() != model.BoardViewID {
t.Error("should return to board again")
}
// Final state
if nav.depth() != 1 {
t.Errorf("final depth = %d, want 1", nav.depth())
}
if nav.canGoBack() {
t.Error("should not be able to go back from single view")
}
}