Skip to content

Commit 805cd52

Browse files
b0bbywanclaude
andcommitted
test(ui): cover upgrade badge rendering and SSE mapping
Assert the badge label and tooltip per state (up-to-date, available, unknown), that every state is a re-check button, and that upgrade.info maps to a section-upgrade fragment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7e7ce39 commit 805cd52

2 files changed

Lines changed: 128 additions & 0 deletions

File tree

ui/handler_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ func TestLoadTemplates(t *testing.T) {
3434
"section-pulseaudio",
3535
"section-systemd",
3636
"section-bluetooth",
37+
"section-upgrade",
38+
"upgrade-ring",
3739
"mpris-player",
3840
"pulseaudio-sink",
3941
"systemd-unit",
@@ -109,6 +111,21 @@ func TestSectionTemplates(t *testing.T) {
109111
template: "section-bluetooth",
110112
data: &BluetoothView{Powered: true, PairingActive: true, PairingUntilMs: 1_700_000_000_000},
111113
},
114+
{
115+
name: "Upgrade badge up to date",
116+
template: "section-upgrade",
117+
data: &UpgradeStatus{Current: "1.0", Latest: "1.0", UpgradeAvailable: false},
118+
},
119+
{
120+
name: "Upgrade badge update available",
121+
template: "section-upgrade",
122+
data: &UpgradeStatus{Current: "1.0", Latest: "1.1", UpgradeAvailable: true},
123+
},
124+
{
125+
name: "Upgrade badge unknown (no detection)",
126+
template: "section-upgrade",
127+
data: (*UpgradeStatus)(nil),
128+
},
112129
}
113130

114131
for _, tt := range tests {
@@ -131,6 +148,103 @@ func TestSectionTemplates(t *testing.T) {
131148
}
132149
}
133150

151+
// TestUpgradeBadgeTemplate asserts the badge label per state and that the
152+
// last-check time is surfaced in the tooltip; every state is a re-check button.
153+
func TestUpgradeBadgeTemplate(t *testing.T) {
154+
tmpl := LoadTemplates()
155+
checked := time.Date(2026, 6, 15, 20, 46, 34, 0, time.UTC)
156+
157+
// Badge is icon-only; state is asserted via the tooltip and a distinguishing
158+
// SVG path fragment of the expected icon.
159+
tests := []struct {
160+
name string
161+
status *UpgradeStatus
162+
wantInTitle string
163+
wantIconSVG string // path fragment unique to the expected icon
164+
wantPost string // hx-post target for the click
165+
wantConfirm bool // destructive action must be confirmed
166+
}{
167+
{
168+
name: "up to date surfaces checked-at in tooltip, check icon, re-checks",
169+
status: &UpgradeStatus{Current: "1.0", Latest: "1.0", UpgradeAvailable: false, CheckedAt: checked},
170+
wantInTitle: "Up to date · checked " + (&UpgradeStatus{CheckedAt: checked}).CheckedAtLabel(),
171+
wantIconSVG: "M20 6 9 17l-5-5", // icon-check
172+
wantPost: "/upgrade/check",
173+
},
174+
{
175+
name: "update available surfaces latest version, arrow-up icon, installs with confirm",
176+
status: &UpgradeStatus{Current: "1.0", Latest: "1.1", UpgradeAvailable: true, CheckedAt: checked},
177+
wantInTitle: "Upgrade available: 1.1",
178+
wantIconSVG: "M12 19V5", // icon-arrow-up
179+
wantPost: "/upgrade/start",
180+
wantConfirm: true,
181+
},
182+
{
183+
name: "unknown shows check prompt, refresh icon, re-checks",
184+
status: nil,
185+
wantInTitle: "Check for upgrades",
186+
wantIconSVG: "M21 3v5h-5", // icon-rotate-cw
187+
wantPost: "/upgrade/check",
188+
},
189+
}
190+
191+
for _, tt := range tests {
192+
t.Run(tt.name, func(t *testing.T) {
193+
var buf bytes.Buffer
194+
if err := tmpl.ExecuteTemplate(&buf, "section-upgrade", tt.status); err != nil {
195+
t.Fatalf("execute section-upgrade: %v", err)
196+
}
197+
out := buf.String()
198+
if !strings.Contains(out, tt.wantInTitle) {
199+
t.Errorf("expected tooltip %q in output, got: %s", tt.wantInTitle, out)
200+
}
201+
if !strings.Contains(out, tt.wantIconSVG) {
202+
t.Errorf("expected icon path %q in output, got: %s", tt.wantIconSVG, out)
203+
}
204+
if !strings.Contains(out, `hx-post="`+tt.wantPost+`"`) {
205+
t.Errorf("expected hx-post %q in output, got: %s", tt.wantPost, out)
206+
}
207+
if gotConfirm := strings.Contains(out, "hx-confirm="); gotConfirm != tt.wantConfirm {
208+
t.Errorf("hx-confirm present = %v, want %v; output: %s", gotConfirm, tt.wantConfirm, out)
209+
}
210+
})
211+
}
212+
}
213+
214+
func TestUpgradeBadgeRunning(t *testing.T) {
215+
tmpl := LoadTemplates()
216+
pct := 42
217+
218+
render := func(status *UpgradeStatus) string {
219+
t.Helper()
220+
var buf bytes.Buffer
221+
if err := tmpl.ExecuteTemplate(&buf, "section-upgrade", status); err != nil {
222+
t.Fatalf("execute section-upgrade: %v", err)
223+
}
224+
return buf.String()
225+
}
226+
227+
// Running badge is the ring's sse-swap target, no click action.
228+
out := render(&UpgradeStatus{Latest: "1.1", Run: &UpgradeRun{State: "running", Percent: &pct}})
229+
if !strings.Contains(out, `sse-swap="upgrade-progress"`) {
230+
t.Errorf("running badge should be the ring sse-swap target, got: %s", out)
231+
}
232+
if strings.Contains(out, "hx-post=") {
233+
t.Errorf("running badge should not be clickable, got: %s", out)
234+
}
235+
236+
// Ring fill: stroke-dashoffset = 100 - percent.
237+
if !strings.Contains(out, `stroke-dashoffset="58"`) {
238+
t.Errorf("ring should reflect 42%% (offset 58), got: %s", out)
239+
}
240+
241+
// No percent yet → spinner, no ring.
242+
noPct := render(&UpgradeStatus{Latest: "1.1", Run: &UpgradeRun{State: "running"}})
243+
if !strings.Contains(noPct, "spinner") || strings.Contains(noPct, "upgrade-ring-fill") {
244+
t.Errorf("running without percent should be a spinner, got: %s", noPct)
245+
}
246+
}
247+
134248
// TestComponentTemplates verifies all component templates can be executed without panic
135249
func TestComponentTemplates(t *testing.T) {
136250
tmpl := LoadTemplates()

ui/sse_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,20 @@ func TestSSEEvents_EventMapping(t *testing.T) {
192192
apiResponse: "[]",
193193
wantSSEEvent: "event: section-mpris",
194194
},
195+
{
196+
name: "upgrade.info maps to section-upgrade",
197+
eventType: events.TypeUpgradeInfo,
198+
apiEndpoint: "/upgrade",
199+
apiResponse: `{"current":"1.0","latest":"1.1","upgrade_available":true,"checked_at":"2026-06-15T20:46:34Z"}`,
200+
wantSSEEvent: "event: section-upgrade",
201+
},
202+
{
203+
name: "upgrade.progress maps to upgrade-progress",
204+
eventType: events.TypeUpgradeProgress,
205+
apiEndpoint: "/upgrade",
206+
apiResponse: `{"current":"1.0","latest":"1.1","upgrade_available":true,"run":{"state":"running","percent":42}}`,
207+
wantSSEEvent: "event: upgrade-progress",
208+
},
195209
}
196210

197211
for _, tt := range tests {

0 commit comments

Comments
 (0)