Files
gastown/internal/web/templates_test.go
Clay Cantrell aca753296b feat(web): comprehensive dashboard control panel with 13 data panels (#931)
* feat(dashboard): comprehensive control panel with expand/collapse

- Add 13 panels: Convoys, Polecats, Sessions, Activity, Mail, Merge Queue,
  Escalations, Rigs, Dogs, System Health, Open Issues, Hooks, Queues
- Add Mayor status banner and Summary/Alerts section
- Implement instant client-side expand/collapse (no page reload)
- Add responsive grid layout for different window sizes
- Parallel data fetching for faster load times
- Color-coded mail by sender, chronological ordering
- Full titles visible in expanded views (no truncation)
- Auto-refresh every 10 seconds via HTMX

* fix(web): update tests and lint for dashboard control panel

- Update MockConvoyFetcher with 11 new interface methods
- Update MockConvoyFetcherWithErrors with matching methods
- Update test assertions for new template structure:
  - Section headers ("Gas Town Convoys" -> "Convoys")
  - Work status badges (badge-green, badge-yellow, badge-red)
  - CI/merge status display text
  - Empty state messages ("No active convoys")
- Fix linting: explicit _, _ = for fmt.Sscanf returns

Tests and linting now pass with the new dashboard features.

* perf(web): add timeouts and error logging to dashboard

Performance and reliability improvements:

- Add 8-second overall fetch timeout to prevent stuck requests
- Add per-command timeouts: 5s for bd/sqlite3, 10s for gh, 2s for tmux
- Add helper functions runCmd() and runBdCmd() with context timeout
- Add error logging for all 14 fetch operations
- Handler now returns partial data if timeout occurs

This addresses slow loading and "stuck" dashboard issues by ensuring
commands cannot hang indefinitely.
2026-01-25 18:00:46 -08:00

236 lines
5.2 KiB
Go

package web
import (
"bytes"
"strings"
"testing"
"time"
"github.com/steveyegge/gastown/internal/activity"
)
func TestConvoyTemplate_RendersConvoyList(t *testing.T) {
tmpl, err := LoadTemplates()
if err != nil {
t.Fatalf("LoadTemplates() error = %v", err)
}
data := ConvoyData{
Convoys: []ConvoyRow{
{
ID: "hq-cv-abc",
Title: "Feature X",
Status: "open",
Progress: "2/5",
Completed: 2,
Total: 5,
LastActivity: activity.Calculate(time.Now().Add(-1 * time.Minute)),
},
{
ID: "hq-cv-def",
Title: "Bugfix Y",
Status: "open",
Progress: "1/3",
Completed: 1,
Total: 3,
LastActivity: activity.Calculate(time.Now().Add(-3 * time.Minute)),
},
},
}
var buf bytes.Buffer
err = tmpl.ExecuteTemplate(&buf, "convoy.html", data)
if err != nil {
t.Fatalf("ExecuteTemplate() error = %v", err)
}
output := buf.String()
// Check convoy IDs are rendered
if !strings.Contains(output, "hq-cv-abc") {
t.Error("Template should contain convoy ID hq-cv-abc")
}
if !strings.Contains(output, "hq-cv-def") {
t.Error("Template should contain convoy ID hq-cv-def")
}
// The simplified dashboard no longer shows convoy titles in the table,
// only the convoy IDs. Titles are shown in expanded view.
}
func TestConvoyTemplate_LastActivityColors(t *testing.T) {
tmpl, err := LoadTemplates()
if err != nil {
t.Fatalf("LoadTemplates() error = %v", err)
}
tests := []struct {
name string
age time.Duration
wantClass string
}{
{"green for 1 minute", 1 * time.Minute, "activity-green"},
{"yellow for 3 minutes", 3 * time.Minute, "activity-yellow"},
{"red for 10 minutes", 10 * time.Minute, "activity-red"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data := ConvoyData{
Convoys: []ConvoyRow{
{
ID: "hq-cv-test",
Title: "Test",
Status: "open",
LastActivity: activity.Calculate(time.Now().Add(-tt.age)),
},
},
}
var buf bytes.Buffer
err = tmpl.ExecuteTemplate(&buf, "convoy.html", data)
if err != nil {
t.Fatalf("ExecuteTemplate() error = %v", err)
}
output := buf.String()
if !strings.Contains(output, tt.wantClass) {
t.Errorf("Template should contain class %q for %v age", tt.wantClass, tt.age)
}
})
}
}
func TestConvoyTemplate_HtmxAutoRefresh(t *testing.T) {
tmpl, err := LoadTemplates()
if err != nil {
t.Fatalf("LoadTemplates() error = %v", err)
}
data := ConvoyData{
Convoys: []ConvoyRow{
{
ID: "hq-cv-test",
Title: "Test",
Status: "open",
},
},
}
var buf bytes.Buffer
err = tmpl.ExecuteTemplate(&buf, "convoy.html", data)
if err != nil {
t.Fatalf("ExecuteTemplate() error = %v", err)
}
output := buf.String()
// Check for htmx attributes
if !strings.Contains(output, "hx-get") {
t.Error("Template should contain hx-get for auto-refresh")
}
if !strings.Contains(output, "hx-trigger") {
t.Error("Template should contain hx-trigger for auto-refresh")
}
if !strings.Contains(output, "every 10s") {
t.Error("Template should refresh every 10 seconds")
}
}
func TestConvoyTemplate_ProgressDisplay(t *testing.T) {
tmpl, err := LoadTemplates()
if err != nil {
t.Fatalf("LoadTemplates() error = %v", err)
}
data := ConvoyData{
Convoys: []ConvoyRow{
{
ID: "hq-cv-test",
Title: "Test",
Status: "open",
Progress: "3/7",
Completed: 3,
Total: 7,
},
},
}
var buf bytes.Buffer
err = tmpl.ExecuteTemplate(&buf, "convoy.html", data)
if err != nil {
t.Fatalf("ExecuteTemplate() error = %v", err)
}
output := buf.String()
// Check progress is displayed
if !strings.Contains(output, "3/7") {
t.Error("Template should display progress '3/7'")
}
}
func TestConvoyTemplate_StatusIndicators(t *testing.T) {
tmpl, err := LoadTemplates()
if err != nil {
t.Fatalf("LoadTemplates() error = %v", err)
}
data := ConvoyData{
Convoys: []ConvoyRow{
{
ID: "hq-cv-active",
Title: "Active Convoy",
Status: "open",
WorkStatus: "active",
},
{
ID: "hq-cv-stuck",
Title: "Stuck Convoy",
Status: "open",
WorkStatus: "stuck",
},
},
}
var buf bytes.Buffer
err = tmpl.ExecuteTemplate(&buf, "convoy.html", data)
if err != nil {
t.Fatalf("ExecuteTemplate() error = %v", err)
}
output := buf.String()
// Check work status badges are rendered (replaced status-open/closed classes)
if !strings.Contains(output, "badge-green") {
t.Error("Template should contain badge-green class for active status")
}
if !strings.Contains(output, "badge-red") {
t.Error("Template should contain badge-red class for stuck status")
}
}
func TestConvoyTemplate_EmptyState(t *testing.T) {
tmpl, err := LoadTemplates()
if err != nil {
t.Fatalf("LoadTemplates() error = %v", err)
}
data := ConvoyData{
Convoys: []ConvoyRow{},
}
var buf bytes.Buffer
err = tmpl.ExecuteTemplate(&buf, "convoy.html", data)
if err != nil {
t.Fatalf("ExecuteTemplate() error = %v", err)
}
output := buf.String()
// Check for empty state message
if !strings.Contains(output, "No active convoys") {
t.Error("Template should show empty state message when no convoys")
}
}