summaryrefslogtreecommitdiff
path: root/internal/handlers/handlers_test.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-08-07 10:39:28 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-08-07 10:39:28 +0000
commiteadd17df2946a1219fdf02f2ee0a0ac19734e56d (patch)
treee0460d1cdd9c17fd47ce7cce2fe7845378d8011e /internal/handlers/handlers_test.go
parent06450fe69ade2928deb9274bb67b7ba60d394b4f (diff)
Wire the Tasks tab into nav; fold in buckets/projects/labels and recurrence
The Tasks tab (/tabs/tasks) existed server-side and was tested, but nothing in the nav linked to it -- it was pure dead weight in the other direction. Wiring it up as the natural home for everything that was either misplaced in Settings or missing a web UI entirely: - Maintenance Buckets, Projects, and Labels moved out of Settings and into the Tasks tab (restyled from Settings' opaque slate cards to the glass/backdrop-blur look already used by the tab's chain/atom cards -- they're now embedded in index.html's page shell, not a standalone page, so the shared bg-card/bg-input classes from that shell apply). Settings keeps only what's actually settings: Passkeys, Trusted Agents, Data Sources. - Added a Recurrence section to the task-detail modal (freq/interval/ weekday form, posting to a new POST /tasks/recurrence -- the HTMX counterpart to the widget API's HandleWidgetTaskRecurrence). Native task recurrence previously had zero web UI at all, only reachable via the Android widget's RecurrenceEditDialog. Also fixed a real bug found while touching this code: HandleGetTaskDetail's source switch only had a case for "trello" -- opening any native ("doot") task's detail modal, which is most tasks in this tab, showed a blank title and description. Factored both call sites (initial GET and the re-render after a recurrence edit) through one loadTaskDetailData helper and added the missing "doot" case. Also fixed task-detail.html's styling, which was still using pre-dark-theme classes (text-gray-900 etc.) -- functionally invisible text on the modal's dark background. Verified with a throwaway local server (real templates + real DB, not the MockRenderer the unit tests use) seeded with a recurring task, a bucket, a project, and a label -- confirmed all five touched routes render 200 with the expected content, including the populated Buckets/Projects/Labels sections and a real weekly-recurrence form with the correct weekdays pre-checked. Caught and fixed a copy bug this way too ("every 2 weeklys" -> "every 2 weeks"). Not committed; deleted after use. go build ./..., go vet ./..., and go test ./... all clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
Diffstat (limited to 'internal/handlers/handlers_test.go')
-rw-r--r--internal/handlers/handlers_test.go107
1 files changed, 107 insertions, 0 deletions
diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go
index 4c776b0..a43dec2 100644
--- a/internal/handlers/handlers_test.go
+++ b/internal/handlers/handlers_test.go
@@ -1869,6 +1869,113 @@ func TestHandleGetTaskDetail_RendersTemplate(t *testing.T) {
}
}
+// TestHandleGetTaskDetail_DootSource_LoadsRealTaskFields guards against a
+// regression where source=="doot" fell through the switch in
+// loadTaskDetailData with no case, silently leaving Title/Description blank
+// -- native tasks are the primary type the Tasks tab and its detail modal
+// are built around, so this previously meant opening any native task's
+// detail showed an empty modal.
+func TestHandleGetTaskDetail_DootSource_LoadsRealTaskFields(t *testing.T) {
+ h, cleanup := setupTestHandler(t)
+ defer cleanup()
+
+ if err := h.store.CreateNativeTask(models.Task{
+ ID: "task-1", Content: "Clean gutters", Description: "Ladder's in the garage", Priority: 1,
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ req := httptest.NewRequest("GET", "/tasks/detail?id=task-1&source=doot", nil)
+ w := httptest.NewRecorder()
+ h.HandleGetTaskDetail(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]
+ data, ok := lastCall.Data.(taskDetailData)
+ if !ok {
+ t.Fatalf("unexpected data type %T", lastCall.Data)
+ }
+ if !data.IsDoot {
+ t.Error("IsDoot = false, want true")
+ }
+ if data.Title != "Clean gutters" {
+ t.Errorf("Title = %q, want %q", data.Title, "Clean gutters")
+ }
+ if data.Description != "Ladder's in the garage" {
+ t.Errorf("Description = %q, want %q", data.Description, "Ladder's in the garage")
+ }
+}
+
+func TestHandleSetTaskRecurrence_SetsAndClears(t *testing.T) {
+ h, cleanup := setupTestHandler(t)
+ defer cleanup()
+
+ if err := h.store.CreateNativeTask(models.Task{ID: "task-1", Content: "Water plants", Priority: 1}); err != nil {
+ t.Fatal(err)
+ }
+
+ req := httptest.NewRequest("POST", "/tasks/recurrence", strings.NewReader("id=task-1&freq=weekly&interval=2&weekdays=1&weekdays=3"))
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ w := httptest.NewRecorder()
+ h.HandleSetTaskRecurrence(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200, body=%s", w.Code, w.Body.String())
+ }
+ task, err := h.store.GetNativeTaskByID("task-1")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if task.RecurrenceFreq != "weekly" || task.RecurrenceInterval != 2 {
+ t.Errorf("recurrence = freq=%q interval=%d, want weekly/2", task.RecurrenceFreq, task.RecurrenceInterval)
+ }
+ if len(task.RecurrenceWeekdays) != 2 {
+ t.Errorf("weekdays = %v, want [1 3]", task.RecurrenceWeekdays)
+ }
+ if task.RecurrenceSeriesID == "" {
+ t.Error("RecurrenceSeriesID should be set once a recurrence pattern is active")
+ }
+
+ // Clearing (freq="") should drop the pattern.
+ req2 := httptest.NewRequest("POST", "/tasks/recurrence", strings.NewReader("id=task-1&freq=&interval=1"))
+ req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ w2 := httptest.NewRecorder()
+ h.HandleSetTaskRecurrence(w2, req2)
+
+ if w2.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200, body=%s", w2.Code, w2.Body.String())
+ }
+ task, err = h.store.GetNativeTaskByID("task-1")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if task.RecurrenceFreq != "" {
+ t.Errorf("RecurrenceFreq = %q after clear, want empty", task.RecurrenceFreq)
+ }
+}
+
+func TestHandleSetTaskRecurrence_InvalidFreq_Returns400(t *testing.T) {
+ h, cleanup := setupTestHandler(t)
+ defer cleanup()
+
+ if err := h.store.CreateNativeTask(models.Task{ID: "task-1", Content: "Water plants", Priority: 1}); err != nil {
+ t.Fatal(err)
+ }
+
+ req := httptest.NewRequest("POST", "/tasks/recurrence", strings.NewReader("id=task-1&freq=fortnightly"))
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ w := httptest.NewRecorder()
+ h.HandleSetTaskRecurrence(w, req)
+
+ if w.Code != http.StatusBadRequest {
+ t.Fatalf("status = %d, want 400", w.Code)
+ }
+}
+
// =============================================================================
// HandleGetListsOptions template tests
// =============================================================================