diff options
Diffstat (limited to 'internal/handlers/chains_web_test.go')
| -rw-r--r-- | internal/handlers/chains_web_test.go | 213 |
1 files changed, 213 insertions, 0 deletions
diff --git a/internal/handlers/chains_web_test.go b/internal/handlers/chains_web_test.go new file mode 100644 index 0000000..d27dab9 --- /dev/null +++ b/internal/handlers/chains_web_test.go @@ -0,0 +1,213 @@ +package handlers + +import ( + "net/http" + "net/http/httptest" + "testing" + + "task-dashboard/internal/models" +) + +func TestBuildChainSummaries_ReturnsPositionAndCurrentTask(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + + chain, err := db.CreateChain("Track", []models.ChainTaskInput{ + {Content: "Step 1"}, {Content: "Step 2"}, {Content: "Step 3"}, + }) + if err != nil { + t.Fatal(err) + } + + summaries, err := BuildChainSummaries(db) + if err != nil { + t.Fatalf("BuildChainSummaries: %v", err) + } + if len(summaries) != 1 { + t.Fatalf("len(summaries) = %d, want 1", len(summaries)) + } + s := summaries[0] + if s.Chain.ID != chain.ID || s.ProjectName != "Track" { + t.Errorf("summary = %+v", s) + } + if s.Total != 3 || s.Position != 1 { + t.Errorf("Position/Total = %d/%d, want 1/3", s.Position, s.Total) + } + if s.CurrentTask == nil || s.CurrentTask.Content != "Step 1" { + t.Errorf("CurrentTask = %+v, want Step 1", s.CurrentTask) + } +} + +func TestBuildChainSummaries_ExcludesAbandonedAndCompleted(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + + chain, err := db.CreateChain("Track", []models.ChainTaskInput{{Content: "Only step"}}) + if err != nil { + t.Fatal(err) + } + if err := db.SetChainStatus(chain.ID, "abandoned"); err != nil { + t.Fatal(err) + } + + summaries, err := BuildChainSummaries(db) + if err != nil { + t.Fatal(err) + } + if len(summaries) != 0 { + t.Errorf("summaries = %+v, want none (abandoned chain excluded)", summaries) + } +} + +func TestHandleChainDetailView_RendersFullOrderedList(t *testing.T) { + h, cleanup := setupTestHandler(t) + defer cleanup() + + chain, err := h.store.CreateChain("Track", []models.ChainTaskInput{{Content: "Step 1"}, {Content: "Step 2"}}) + if err != nil { + t.Fatal(err) + } + + req := withURLParam(httptest.NewRequest("GET", "/chains/"+chain.ID, nil), "id", chain.ID) + w := httptest.NewRecorder() + h.HandleChainDetailView(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status = %d, want 200, body=%s", w.Code, w.Body.String()) + } + + mock := h.renderer.(*MockRenderer) + if len(mock.Calls) == 0 { + t.Fatal("expected renderer to be called") + } + lastCall := mock.Calls[len(mock.Calls)-1] + if lastCall.Name != "chain-detail" { + t.Errorf("template = %q, want chain-detail", lastCall.Name) + } + data, ok := lastCall.Data.(struct { + Chain models.Chain + ProjectName string + Tasks []models.Task + }) + if !ok { + t.Fatalf("unexpected data type %T", lastCall.Data) + } + if data.ProjectName != "Track" || len(data.Tasks) != 2 { + t.Errorf("data = %+v", data) + } + if data.Tasks[0].Content != "Step 1" || data.Tasks[1].Content != "Step 2" { + t.Errorf("tasks out of order: %+v", data.Tasks) + } +} + +func TestHandleChainDetailView_UnknownID_Returns404(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + h := &Handler{store: db} + + req := withURLParam(httptest.NewRequest("GET", "/chains/nope", nil), "id", "nope") + w := httptest.NewRecorder() + h.HandleChainDetailView(w, req) + + if w.Code != http.StatusNotFound { + t.Fatalf("status = %d, want 404", w.Code) + } +} + +func TestHandleChainPauseResumeAbandon_Web(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + h := &Handler{store: db} + + chain, err := db.CreateChain("Track", []models.ChainTaskInput{{Content: "Step 1"}, {Content: "Step 2"}}) + if err != nil { + t.Fatal(err) + } + + req := withURLParam(httptest.NewRequest("POST", "/chains/"+chain.ID+"/pause", nil), "id", chain.ID) + w := httptest.NewRecorder() + h.HandleChainPause(w, req) + if w.Code != http.StatusOK { + t.Fatalf("pause status = %d, want 200", w.Code) + } + if w.Header().Get("HX-Trigger") != "refresh-tasks" { + t.Errorf("HX-Trigger = %q, want refresh-tasks", w.Header().Get("HX-Trigger")) + } + paused, err := db.GetChain(chain.ID) + if err != nil || paused.Status != "paused" { + t.Fatalf("chain after pause = %+v, err=%v", paused, err) + } + + req = withURLParam(httptest.NewRequest("POST", "/chains/"+chain.ID+"/resume", nil), "id", chain.ID) + w = httptest.NewRecorder() + h.HandleChainResume(w, req) + if w.Code != http.StatusOK { + t.Fatalf("resume status = %d, want 200", w.Code) + } + + req = withURLParam(httptest.NewRequest("POST", "/chains/"+chain.ID+"/abandon", nil), "id", chain.ID) + w = httptest.NewRecorder() + h.HandleChainAbandon(w, req) + if w.Code != http.StatusOK { + t.Fatalf("abandon status = %d, want 200", w.Code) + } + abandoned, err := db.GetChain(chain.ID) + if err != nil || abandoned.Status != "abandoned" { + t.Fatalf("chain after abandon = %+v, err=%v", abandoned, err) + } +} + +func TestHandleChainPause_UnknownID_Returns404(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + h := &Handler{store: db} + + req := withURLParam(httptest.NewRequest("POST", "/chains/nope/pause", nil), "id", "nope") + w := httptest.NewRecorder() + h.HandleChainPause(w, req) + + if w.Code != http.StatusNotFound { + t.Fatalf("status = %d, want 404", w.Code) + } +} + +func TestHandleTabTasks_IncludesChainSummaries(t *testing.T) { + h, cleanup := setupTestHandler(t) + defer cleanup() + + if _, err := h.store.CreateChain("Track", []models.ChainTaskInput{{Content: "Step 1"}}); err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest("GET", "/tabs/tasks", nil) + w := httptest.NewRecorder() + h.HandleTabTasks(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status = %d, want 200, body=%s", w.Code, w.Body.String()) + } + + mock := h.renderer.(*MockRenderer) + lastCall := mock.Calls[len(mock.Calls)-1] + if lastCall.Name != "tasks-tab" { + t.Errorf("template = %q, want tasks-tab", lastCall.Name) + } + data, ok := lastCall.Data.(struct { + Atoms []models.Atom + FutureAtoms []models.Atom + Boards []models.Board + Chains []models.ChainSummary + Today string + }) + if !ok { + t.Fatalf("unexpected data type %T", lastCall.Data) + } + if len(data.Chains) != 1 || data.Chains[0].CurrentTask == nil || data.Chains[0].CurrentTask.Content != "Step 1" { + t.Errorf("Chains = %+v", data.Chains) + } + for _, a := range data.Atoms { + if a.Title == "Step 1" { + t.Error("chain task should not also appear in the flat Atoms list") + } + } +} |
