|
1 | 1 | package main |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "fmt" |
4 | 5 | "os" |
5 | 6 | "path/filepath" |
6 | 7 | "strings" |
7 | 8 | "testing" |
| 9 | + "time" |
| 10 | + |
| 11 | + tea "github.com/charmbracelet/bubbletea" |
8 | 12 |
|
9 | 13 | "github.com/bborn/workflow/internal/db" |
10 | 14 | ) |
@@ -1368,6 +1372,242 @@ func TestUnescapeNewlines(t *testing.T) { |
1368 | 1372 | } |
1369 | 1373 | } |
1370 | 1374 |
|
| 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 | + |
1371 | 1611 | func TestFormatPermissionDetail(t *testing.T) { |
1372 | 1612 | tests := []struct { |
1373 | 1613 | name string |
|
0 commit comments