Files
beads/cmd/bd/list_helpers_test.go
Ryan Snodgrass cfd1f39e1e feat(ux): visual improvements for list tree, graph, and show commands
bd list --tree:
- Use actual parent-child dependencies instead of dotted ID hierarchy
- Treat epic dependencies as parent-child relationships
- Sort children by priority (P0 first)
- Fix tree display in daemon mode with read-only store access

bd graph:
- Add --all flag to show dependency graph of all open issues
- Add --compact flag for tree-style rendering (reduces 44+ lines to 13)
- Fix "needs:N" cognitive noise by using semantic colors
- Add blocks:N indicator with semantic red coloring

bd show:
- Tufte-aligned header with status icon, priority, and type badges
- Add glamour markdown rendering with auto light/dark mode detection
- Cap markdown line width at 100 chars for readability
- Mute entire row for closed dependencies (work done, no attention needed)

Design system:
- Add shared status icons (○ ◐ ● ✓ ❄) with semantic colors
- Implement priority colors: P0 red, P1 orange, P2 muted gold, P3-P4 neutral
- Add TrueColor profile for distinct hex color rendering
- Type badges for epic (purple) and bug (red)

Design principles:
- Semantic colors only for actionable items
- Closed items fade (muted gray)
- Icons > text labels for better scanability

Co-Authored-By: SageOx <ox@sageox.ai>
2026-01-08 20:50:56 -08:00

117 lines
3.4 KiB
Go

package main
import (
"strings"
"testing"
"time"
"github.com/steveyegge/beads/internal/types"
)
func TestListParseTimeFlag(t *testing.T) {
cases := []string{
"2025-12-26",
"2025-12-26T12:34:56",
"2025-12-26 12:34:56",
time.DateOnly,
time.RFC3339,
}
for _, c := range cases {
// Just make sure we accept the expected formats.
var s string
switch c {
case time.DateOnly:
s = "2025-12-26"
case time.RFC3339:
s = "2025-12-26T12:34:56Z"
default:
s = c
}
got, err := parseTimeFlag(s)
if err != nil {
t.Fatalf("parseTimeFlag(%q) error: %v", s, err)
}
if got.Year() != 2025 {
t.Fatalf("parseTimeFlag(%q) year=%d, want 2025", s, got.Year())
}
}
if _, err := parseTimeFlag("not-a-date"); err == nil {
t.Fatalf("expected error")
}
}
func TestListPinIndicator(t *testing.T) {
if pinIndicator(&types.Issue{Pinned: true}) == "" {
t.Fatalf("expected pin indicator")
}
if pinIndicator(&types.Issue{Pinned: false}) != "" {
t.Fatalf("expected empty pin indicator")
}
}
func TestListFormatPrettyIssue_BadgesAndDefaults(t *testing.T) {
iss := &types.Issue{ID: "bd-1", Title: "Hello", Status: "wat", Priority: 99, IssueType: "bug"}
out := formatPrettyIssue(iss)
if !strings.Contains(out, "bd-1") || !strings.Contains(out, "Hello") {
t.Fatalf("unexpected output: %q", out)
}
if !strings.Contains(out, "[bug]") {
t.Fatalf("expected bug badge: %q", out)
}
}
func TestListBuildIssueTree_ParentChildByDotID(t *testing.T) {
parent := &types.Issue{ID: "bd-1", Title: "Parent", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask}
child := &types.Issue{ID: "bd-1.1", Title: "Child", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask}
orphan := &types.Issue{ID: "bd-2.1", Title: "Orphan", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask}
roots, children := buildIssueTree([]*types.Issue{child, parent, orphan})
if len(children["bd-1"]) != 1 || children["bd-1"][0].ID != "bd-1.1" {
t.Fatalf("expected bd-1 to have bd-1.1 child: %+v", children)
}
if len(roots) != 2 {
t.Fatalf("expected 2 roots (parent + orphan), got %d", len(roots))
}
}
func TestListSortIssues_ClosedNilLast(t *testing.T) {
t1 := time.Now().Add(-2 * time.Hour)
t2 := time.Now().Add(-1 * time.Hour)
closedOld := &types.Issue{ID: "bd-1", ClosedAt: &t1}
closedNew := &types.Issue{ID: "bd-2", ClosedAt: &t2}
open := &types.Issue{ID: "bd-3", ClosedAt: nil}
issues := []*types.Issue{open, closedOld, closedNew}
sortIssues(issues, "closed", false)
if issues[0].ID != "bd-2" || issues[1].ID != "bd-1" || issues[2].ID != "bd-3" {
t.Fatalf("unexpected order: %s, %s, %s", issues[0].ID, issues[1].ID, issues[2].ID)
}
}
func TestListDisplayPrettyList(t *testing.T) {
out := captureStdout(t, func() error {
displayPrettyList(nil, false)
return nil
})
if !strings.Contains(out, "No issues found") {
t.Fatalf("unexpected output: %q", out)
}
issues := []*types.Issue{
{ID: "bd-1", Title: "A", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask},
{ID: "bd-2", Title: "B", Status: types.StatusInProgress, Priority: 1, IssueType: types.TypeFeature},
{ID: "bd-1.1", Title: "C", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask},
}
out = captureStdout(t, func() error {
displayPrettyList(issues, false)
return nil
})
if !strings.Contains(out, "bd-1") || !strings.Contains(out, "bd-1.1") || !strings.Contains(out, "Total:") {
t.Fatalf("unexpected output: %q", out)
}
}