summaryrefslogtreecommitdiff
path: root/internal/handlers/chains_web_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/handlers/chains_web_test.go')
-rw-r--r--internal/handlers/chains_web_test.go213
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")
+ }
+ }
+}