Files
projax/web/dashboard_edit_test.go
mAi 7ed0a4d46c feat(phase 4a): chronological timeline at /timeline + dashboard VTODO edit/delete
/timeline braids every dated thing in projax into a single chronological spine:
CalDAV VTODOs (DUE anchor), VEVENTs (DTSTART), dated item_links (event_date),
and item-creation markers. Default window past-30d to future-90d; ?order=
toggles asc/desc; ?kind= narrows by row type; tree filter (?tag/?mgmt/?has)
applies across kinds. Today / Tomorrow get sticky pills; rows > today+30d
fade. 90s in-memory TTL cache keyed by (filter, window, order, kinds);
busted on any VTODO writeback or dated-link change.

Scope expansion (per head message during 4a): the dashboard Tasks card now
has edit + delete affordances on every row, matching the detail page. New
/dashboard/task/{edit,delete} endpoints share a writeback path with /done.
Timeline VTODO rows reuse the same handlers; HX-Target=timeline-section
selects the re-render surface. Timeline item_link rows reuse the existing
/i/{path}/links/remove handler with the same surface-switch.

VEVENT rows on the timeline remain read-only at v1 (3l decision stands).
Item-creation events render as muted "added X to projax" markers.

Tests cover empty state, dated-doc surfacing, kind-filter narrowing, order
toggle, mixed CalDAV todos + all-day events (with the (2 days) duration
hint), and tag-filter cross-kind. New dashboard test asserts the edit/
delete affordances are wired up.

docs/design.md gains §12 with the full source list, layout rules, time
window, filter integration, cache TTL, and deferred items.
2026-05-16 15:52:32 +02:00

89 lines
2.7 KiB
Go

package web_test
import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/m/projax/caldav"
"github.com/m/projax/web"
)
// TestDashboardTaskRowHasEditAndDelete wires a fake CalDAV server with one
// open VTODO, surfaces it on the dashboard Tasks card, and asserts the row
// gained the edit + delete affordances added in Phase 4a's scope expansion.
func TestDashboardTaskRowHasEditAndDelete(t *testing.T) {
srv, pool := mustServer(t)
defer pool.Close()
icsTodo := `BEGIN:VCALENDAR
BEGIN:VTODO
UID:dash-edit-1@fake
SUMMARY:Edit me please
STATUS:NEEDS-ACTION
DUE;VALUE=DATE:` + time.Now().UTC().Format("20060102") + `
END:VTODO
END:VCALENDAR`
mux := http.NewServeMux()
mux.HandleFunc("/dav/calendars/m/Edit/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(207)
_, _ = io.WriteString(w, `<?xml version="1.0"?><d:multistatus xmlns:d="DAV:" xmlns:cal="urn:ietf:params:xml:ns:caldav">
<d:response><d:href>/dav/calendars/m/Edit/td-1.ics</d:href><d:propstat><d:prop>
<d:getetag>"t1"</d:getetag>
<cal:calendar-data>`+icsTodo+`</cal:calendar-data>
</d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response>
</d:multistatus>`)
})
fake := httptest.NewServer(mux)
defer fake.Close()
srv.CalDAV = &web.CalDAVDeps{Client: caldav.New(fake.URL+"/dav/calendars/m/", "u", "p")}
stamp := strings.ReplaceAll(time.Now().UTC().Format("150405.000000"), ".", "")
slug := "dash-edit-" + stamp
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var dev, id string
if err := pool.QueryRow(ctx, `select id from projax.items where slug='dev' and cardinality(parent_ids)=0`).Scan(&dev); err != nil {
t.Fatalf("dev: %v", err)
}
if err := pool.QueryRow(ctx,
`insert into projax.items (kind, title, slug, parent_ids)
values (array['project']::text[], 'edit', $1, ARRAY[$2]::uuid[])
returning id`,
slug, dev,
).Scan(&id); err != nil {
t.Fatalf("seed item: %v", err)
}
defer pool.Exec(context.Background(), `delete from projax.items where id=$1`, id)
if _, err := pool.Exec(ctx,
`insert into projax.item_links (item_id, ref_type, ref_id, rel)
values ($1, 'caldav-list', $2, 'tracks')`,
id, fake.URL+"/dav/calendars/m/Edit/",
); err != nil {
t.Fatalf("seed link: %v", err)
}
h := srv.Routes()
code, body := get(t, h, "/dashboard")
if code != 200 {
t.Fatalf("GET /dashboard → %d", code)
}
for _, want := range []string{
`Edit me please`,
`hx-post="/dashboard/task/edit"`,
`hx-post="/dashboard/task/delete"`,
`hx-post="/dashboard/task/done"`,
`data-vtodo-uid="dash-edit-1@fake"`,
`hx-confirm="Delete this task`,
} {
if !strings.Contains(body, want) {
t.Errorf("dashboard task row missing %q", want)
}
}
}