Files
Vetting/internal/web/templates/host_tile_test.go
T
josh bb658a8435
CI / Lint + build + test (push) Has been cancelled
Host detail page + pipeline timeline
Click a tile to open /hosts/{id} — the canonical control surface per
host. Timeline renders every pre-stage, stage, and terminal node in
order, with the current one pulsing, failed ones flagged, and
downstream ones dimmed as skipped. Detail page shows summary, hold
card (when holding), all action buttons, spec diffs, a full-height
log pane, and a collapsed expected-spec YAML.

Tile slims to name, last-seen, status, and one primary action; a
CSS-overlay <a> makes the whole card clickable while buttons stay
receptive via z-index.

Runner.publishTileUpdate now also emits pipeline-{runID} fragments,
and CompleteStage wraps Stages.CompleteByName so stage completions
advance the timeline live — without this the dots only moved on
state transitions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 23:59:43 -04:00

89 lines
2.8 KiB
Go

package templates
import (
"context"
"strings"
"testing"
"time"
"vetting/internal/model"
)
func TestHumanAgoFrom(t *testing.T) {
now := time.Date(2026, 4, 17, 12, 0, 0, 0, time.UTC)
cases := []struct {
name string
ago time.Duration
want string
}{
{"just now", 5 * time.Second, "online"},
{"edge-just-under-minute", 59 * time.Second, "online"},
{"one minute", 60 * time.Second, "1m ago"},
{"five minutes", 5 * time.Minute, "5m ago"},
{"fifty-nine minutes", 59 * time.Minute, "59m ago"},
{"one hour", 1 * time.Hour, "1h ago"},
{"eight hours", 8 * time.Hour, "8h ago"},
{"one day", 24 * time.Hour, "1d ago"},
{"three days", 72 * time.Hour, "3d ago"},
// Clock skew: "future" heartbeat clamps to "online" rather than
// printing "-3m ago" or panicking.
{"future clamps to online", -5 * time.Second, "online"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := humanAgoFrom(now, now.Add(-tc.ago))
if got != tc.want {
t.Fatalf("humanAgoFrom(%v) = %q, want %q", tc.ago, got, tc.want)
}
})
}
}
// TestHostTile_OverlayLink asserts the tile includes the tile-link <a>
// that makes the whole card clickable. The action button stays a
// sibling element, so CSS (z-index) keeps it on top of the overlay.
func TestHostTile_OverlayLink(t *testing.T) {
data := TileData{
Host: model.Host{ID: 42, Name: "tile-test", MAC: "aa:bb:cc:dd:ee:ff"},
}
var buf strings.Builder
if err := HostTile(data).Render(context.Background(), &buf); err != nil {
t.Fatalf("render: %v", err)
}
html := buf.String()
if !strings.Contains(html, `href="/hosts/42"`) {
t.Fatalf("tile missing overlay href: %s", html)
}
if !strings.Contains(html, `class="tile-link"`) {
t.Fatalf("tile missing tile-link class: %s", html)
}
// canStart(nil) is true → Start form must be present.
if !strings.Contains(html, `/hosts/42/start`) {
t.Fatalf("expected Start vetting form in tile: %s", html)
}
// Dropped content that used to live on the tile — confirm it has
// actually moved off so the slim-down is real.
for _, dropped := range []string{`tile-meta`, `tile-log`, `tile-actions`, `tile-hold`} {
if strings.Contains(html, dropped) {
t.Errorf("slim tile still contains dropped class %q", dropped)
}
}
}
func TestLastSeenLabelAndClass(t *testing.T) {
if got := lastSeenLabel(nil); got != "never" {
t.Fatalf("label nil = %q, want never", got)
}
if got := lastSeenClass(nil); got != "offline" {
t.Fatalf("class nil = %q, want offline", got)
}
recent := time.Now().Add(-5 * time.Second)
if got := lastSeenClass(&recent); got != "online" {
t.Fatalf("class recent = %q, want online", got)
}
stale := time.Now().Add(-10 * time.Minute)
if got := lastSeenClass(&stale); got != "stale" {
t.Fatalf("class stale = %q, want stale", got)
}
}