Skip to content

Commit 0e3dad8

Browse files
authored
Merge pull request #501 from bborn/task/2008-implement-efficient-live-tail-display-wi
Rewrite ty tail to use alternate screen buffer
2 parents 7b9ffb4 + e4b4742 commit 0e3dad8

File tree

2 files changed

+451
-159
lines changed

2 files changed

+451
-159
lines changed

cmd/task/cli_test.go

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package main
22

33
import (
4+
"fmt"
45
"os"
56
"path/filepath"
67
"strings"
78
"testing"
9+
"time"
10+
11+
tea "github.com/charmbracelet/bubbletea"
812

913
"github.com/bborn/workflow/internal/db"
1014
)
@@ -1368,6 +1372,242 @@ func TestUnescapeNewlines(t *testing.T) {
13681372
}
13691373
}
13701374

1375+
// TestTailModelUpdate tests the tailModel Update method
1376+
func TestTailModelUpdate(t *testing.T) {
1377+
tmpDir := t.TempDir()
1378+
dbPath := filepath.Join(tmpDir, "test.db")
1379+
database, err := db.Open(dbPath)
1380+
if err != nil {
1381+
t.Fatalf("failed to open database: %v", err)
1382+
}
1383+
defer database.Close()
1384+
1385+
m := tailModel{
1386+
db: database,
1387+
interval: 2 * time.Second,
1388+
showDone: false,
1389+
width: 80,
1390+
height: 24,
1391+
}
1392+
1393+
t.Run("q quits", func(t *testing.T) {
1394+
msgs := parseKeyEvents("q")
1395+
_, cmd := m.Update(msgs[0])
1396+
if cmd == nil {
1397+
t.Error("expected quit command, got nil")
1398+
}
1399+
})
1400+
1401+
t.Run("esc quits", func(t *testing.T) {
1402+
msgs := parseKeyEvents("esc")
1403+
_, cmd := m.Update(msgs[0])
1404+
if cmd == nil {
1405+
t.Error("expected quit command, got nil")
1406+
}
1407+
})
1408+
1409+
t.Run("window resize updates dimensions", func(t *testing.T) {
1410+
updated, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 40})
1411+
um := updated.(tailModel)
1412+
if um.width != 120 || um.height != 40 {
1413+
t.Errorf("expected 120x40, got %dx%d", um.width, um.height)
1414+
}
1415+
})
1416+
1417+
t.Run("tick returns new tick command", func(t *testing.T) {
1418+
_, cmd := m.Update(tailTickMsg(time.Now()))
1419+
if cmd == nil {
1420+
t.Error("expected tick command, got nil")
1421+
}
1422+
})
1423+
}
1424+
1425+
// TestTailModelView tests the tailModel View method
1426+
func TestTailModelView(t *testing.T) {
1427+
t.Run("empty database shows no tasks", func(t *testing.T) {
1428+
tmpDir := t.TempDir()
1429+
dbPath := filepath.Join(tmpDir, "test.db")
1430+
database, err := db.Open(dbPath)
1431+
if err != nil {
1432+
t.Fatalf("failed to open database: %v", err)
1433+
}
1434+
defer database.Close()
1435+
1436+
m := tailModel{
1437+
db: database,
1438+
interval: 2 * time.Second,
1439+
width: 80,
1440+
height: 24,
1441+
}
1442+
1443+
view := m.View()
1444+
if !strings.Contains(view, "Live Tail") {
1445+
t.Error("expected header with 'Live Tail'")
1446+
}
1447+
if !strings.Contains(view, "No tasks found") {
1448+
t.Error("expected 'No tasks found' message")
1449+
}
1450+
if !strings.Contains(view, "q/esc to quit") {
1451+
t.Error("expected quit hint in footer")
1452+
}
1453+
})
1454+
1455+
t.Run("renders tasks grouped by project", func(t *testing.T) {
1456+
tmpDir := t.TempDir()
1457+
dbPath := filepath.Join(tmpDir, "test.db")
1458+
database, err := db.Open(dbPath)
1459+
if err != nil {
1460+
t.Fatalf("failed to open database: %v", err)
1461+
}
1462+
defer database.Close()
1463+
1464+
if err := database.CreateProject(&db.Project{Name: "myproject", Path: tmpDir}); err != nil {
1465+
t.Fatalf("failed to create project: %v", err)
1466+
}
1467+
1468+
tasks := []*db.Task{
1469+
{Title: "Processing task", Status: db.StatusProcessing, Type: db.TypeCode, Project: "myproject"},
1470+
{Title: "Queued task", Status: db.StatusQueued, Type: db.TypeCode, Project: "myproject"},
1471+
{Title: "Backlog task", Status: db.StatusBacklog, Type: db.TypeCode},
1472+
}
1473+
for _, task := range tasks {
1474+
if err := database.CreateTask(task); err != nil {
1475+
t.Fatalf("failed to create task: %v", err)
1476+
}
1477+
}
1478+
1479+
m := tailModel{
1480+
db: database,
1481+
interval: 2 * time.Second,
1482+
width: 80,
1483+
height: 50,
1484+
}
1485+
1486+
view := m.View()
1487+
if !strings.Contains(view, "myproject") {
1488+
t.Error("expected 'myproject' in view")
1489+
}
1490+
if !strings.Contains(view, "Processing task") {
1491+
t.Error("expected 'Processing task' in view")
1492+
}
1493+
if !strings.Contains(view, "Queued task") {
1494+
t.Error("expected 'Queued task' in view")
1495+
}
1496+
if !strings.Contains(view, "Backlog task") {
1497+
t.Error("expected 'Backlog task' in view")
1498+
}
1499+
if !strings.Contains(view, "In Progress") {
1500+
t.Error("expected 'In Progress' status label")
1501+
}
1502+
})
1503+
1504+
t.Run("truncates to terminal height", func(t *testing.T) {
1505+
tmpDir := t.TempDir()
1506+
dbPath := filepath.Join(tmpDir, "test.db")
1507+
database, err := db.Open(dbPath)
1508+
if err != nil {
1509+
t.Fatalf("failed to open database: %v", err)
1510+
}
1511+
defer database.Close()
1512+
1513+
for i := 0; i < 30; i++ {
1514+
task := &db.Task{
1515+
Title: fmt.Sprintf("Task %d", i),
1516+
Status: db.StatusBacklog,
1517+
Type: db.TypeCode,
1518+
}
1519+
if err := database.CreateTask(task); err != nil {
1520+
t.Fatalf("failed to create task: %v", err)
1521+
}
1522+
}
1523+
1524+
m := tailModel{
1525+
db: database,
1526+
interval: 2 * time.Second,
1527+
width: 80,
1528+
height: 10,
1529+
}
1530+
1531+
view := m.View()
1532+
lines := strings.Split(view, "\n")
1533+
if len(lines) > 10 {
1534+
t.Errorf("expected at most 10 lines, got %d", len(lines))
1535+
}
1536+
if !strings.Contains(view, "resize terminal") {
1537+
t.Error("expected truncation message")
1538+
}
1539+
})
1540+
1541+
t.Run("done tasks hidden by default", func(t *testing.T) {
1542+
tmpDir := t.TempDir()
1543+
dbPath := filepath.Join(tmpDir, "test.db")
1544+
database, err := db.Open(dbPath)
1545+
if err != nil {
1546+
t.Fatalf("failed to open database: %v", err)
1547+
}
1548+
defer database.Close()
1549+
1550+
tasks := []*db.Task{
1551+
{Title: "Active task", Status: db.StatusQueued, Type: db.TypeCode},
1552+
{Title: "Done task", Status: db.StatusDone, Type: db.TypeCode},
1553+
}
1554+
for _, task := range tasks {
1555+
if err := database.CreateTask(task); err != nil {
1556+
t.Fatalf("failed to create task: %v", err)
1557+
}
1558+
}
1559+
1560+
m := tailModel{
1561+
db: database,
1562+
interval: 2 * time.Second,
1563+
showDone: false,
1564+
width: 80,
1565+
height: 50,
1566+
}
1567+
1568+
view := m.View()
1569+
if !strings.Contains(view, "Active task") {
1570+
t.Error("expected active task in view")
1571+
}
1572+
if strings.Contains(view, "Done task") {
1573+
t.Error("done task should be hidden when showDone is false")
1574+
}
1575+
})
1576+
1577+
t.Run("done tasks shown when enabled", func(t *testing.T) {
1578+
tmpDir := t.TempDir()
1579+
dbPath := filepath.Join(tmpDir, "test.db")
1580+
database, err := db.Open(dbPath)
1581+
if err != nil {
1582+
t.Fatalf("failed to open database: %v", err)
1583+
}
1584+
defer database.Close()
1585+
1586+
tasks := []*db.Task{
1587+
{Title: "Active task", Status: db.StatusQueued, Type: db.TypeCode},
1588+
{Title: "Done task", Status: db.StatusDone, Type: db.TypeCode},
1589+
}
1590+
for _, task := range tasks {
1591+
if err := database.CreateTask(task); err != nil {
1592+
t.Fatalf("failed to create task: %v", err)
1593+
}
1594+
}
1595+
1596+
m := tailModel{
1597+
db: database,
1598+
interval: 2 * time.Second,
1599+
showDone: true,
1600+
width: 80,
1601+
height: 50,
1602+
}
1603+
1604+
view := m.View()
1605+
if !strings.Contains(view, "Done task") {
1606+
t.Error("done task should be visible when showDone is true")
1607+
}
1608+
})
1609+
}
1610+
13711611
func TestFormatPermissionDetail(t *testing.T) {
13721612
tests := []struct {
13731613
name string

0 commit comments

Comments
 (0)