feat(web): unify card states — one 'Summarize now' verb, honest no-captions state
CI / Lint / Test / Vet (push) Successful in 13s
CI / Build & Import (push) Successful in 10s

Five explicit footer states, status-primary:
1. Summarized — chip + actions, no button (unchanged)
2. No captions (TranscriptStatus=="none") — NEW: 'No transcript available' muted
   text, no button, no POST URL. Removes the dead-end 'Summarize' button that
   tried and failed when there were no captions to fetch.
3. Queued (SummarizeRequested) — chip + muted text, no button (unchanged)
4. Rate-limited — 'Fetching soon…' + quiet 'Summarize now' → /retry-now
5. Pending — 'Not summarized' + quiet 'Summarize now' → /summarize

One verb ('Summarize now'), one quiet style (.btn-quiet, renamed from .btn-retry
which was state-specific). User doesn't see the internal pipeline distinction;
both buttons post to their existing handlers unchanged. Form class renamed
card-nudge-form. Dropped engineer-facing tooltip; user-facing hint added.
'Try now' wording removed entirely.
This commit is contained in:
2026-06-06 22:31:41 +02:00
parent 58cd68c1eb
commit 2fedc45443
4 changed files with 327 additions and 201 deletions
+112 -14
View File
@@ -17,9 +17,75 @@ func renderVideoCard(t *testing.T, r store.SummaryRow) string {
return sb.String()
}
// A rate-limited, unsummarized video shows an active "Try now" button so the user
// can manually trigger an immediate fetch through the shared rate gate.
func TestVideoCard_RateLimitedShowsRetryingBadge(t *testing.T) {
// noActionButton asserts the rendered HTML contains no form POST (no action URL
// of the form /v/.../summarize or /v/.../retry-now) and no btn-quiet button.
func noActionButton(t *testing.T, html, state string) {
t.Helper()
if strings.Contains(html, "/summarize") {
t.Errorf("state %q: expected no summarize URL, got:\n%s", state, html)
}
if strings.Contains(html, "/retry-now") {
t.Errorf("state %q: expected no retry-now URL, got:\n%s", state, html)
}
if strings.Contains(html, "btn-quiet") {
t.Errorf("state %q: expected no action button, got:\n%s", state, html)
}
}
// TestVideoCard_State1_Summarized — chip + no nudge button.
func TestVideoCard_State1_Summarized(t *testing.T) {
html := renderVideoCard(t, store.SummaryRow{
VideoID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
Title: "Done Video",
Summarized: true,
Summary: "A great talk about Go.",
AIProvider: "local",
})
if !strings.Contains(html, "local") {
t.Errorf("state 1: expected AI provider chip, got:\n%s", html)
}
noActionButton(t, html, "summarized")
if strings.Contains(html, "Summarize now") {
t.Errorf("state 1: no nudge button on a summarized card, got:\n%s", html)
}
}
// TestVideoCard_State2_NoTranscript — terminal; muted status, NO button, NO POST URL.
func TestVideoCard_State2_NoTranscript(t *testing.T) {
html := renderVideoCard(t, store.SummaryRow{
VideoID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
Title: "Silent Video",
Summarized: false,
TranscriptStatus: "none",
})
if !strings.Contains(html, "No transcript available") {
t.Errorf("state 2: expected 'No transcript available' text, got:\n%s", html)
}
noActionButton(t, html, "none-transcript")
if strings.Contains(html, "Summarize now") {
t.Errorf("state 2: no nudge button when there are no captions, got:\n%s", html)
}
}
// TestVideoCard_State3_Queued — "Queued" chip, no button.
func TestVideoCard_State3_Queued(t *testing.T) {
html := renderVideoCard(t, store.SummaryRow{
VideoID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
Title: "Queued Video",
Summarized: false,
SummarizeRequested: true,
})
if !strings.Contains(html, "Queued") {
t.Errorf("state 3: expected 'Queued' chip, got:\n%s", html)
}
noActionButton(t, html, "queued")
}
// TestVideoCard_State4_RateLimited — quiet status + "Summarize now" → retry-now URL.
func TestVideoCard_State4_RateLimited(t *testing.T) {
html := renderVideoCard(t, store.SummaryRow{
VideoID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
Title: "Throttled Video",
@@ -27,29 +93,61 @@ func TestVideoCard_RateLimitedShowsRetryingBadge(t *testing.T) {
TranscriptStatus: "rate_limited",
})
if !strings.Contains(html, "Try now") {
t.Errorf("expected a 'Try now' button, got:\n%s", html)
if !strings.Contains(html, "Fetching soon") {
t.Errorf("state 4: expected 'Fetching soon' status text, got:\n%s", html)
}
if !strings.Contains(html, "Summarize now") {
t.Errorf("state 4: expected 'Summarize now' button, got:\n%s", html)
}
if !strings.Contains(html, "retry-now") {
t.Errorf("expected the retry-now route in the form action, got:\n%s", html)
t.Errorf("state 4: expected retry-now URL in form action, got:\n%s", html)
}
if strings.Contains(html, ">Summarize<") {
t.Errorf("the Summarize button must be hidden for a rate-limited video, got:\n%s", html)
if strings.Contains(html, "/summarize\"") {
t.Errorf("state 4: rate-limited card must not POST to /summarize, got:\n%s", html)
}
if strings.Contains(html, "Try now") {
t.Errorf("state 4: 'Try now' verb must not appear, got:\n%s", html)
}
}
// An ordinary unsummarized video still offers the Summarize button.
func TestVideoCard_UnsummarizedShowsSummarize(t *testing.T) {
// TestVideoCard_State5_Pending — quiet status + "Summarize now" → summarize URL.
func TestVideoCard_State5_Pending(t *testing.T) {
html := renderVideoCard(t, store.SummaryRow{
VideoID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
Title: "Fresh Video",
Summarized: false,
})
if !strings.Contains(html, ">Summarize<") {
t.Errorf("expected a Summarize button, got:\n%s", html)
if !strings.Contains(html, "Not summarized") {
t.Errorf("state 5: expected 'Not summarized' status text, got:\n%s", html)
}
if strings.Contains(html, "Retrying later") {
t.Errorf("no retry badge for a non-rate-limited video, got:\n%s", html)
if !strings.Contains(html, "Summarize now") {
t.Errorf("state 5: expected 'Summarize now' button, got:\n%s", html)
}
if !strings.Contains(html, "/summarize") {
t.Errorf("state 5: expected summarize URL in form action, got:\n%s", html)
}
if strings.Contains(html, "retry-now") {
t.Errorf("state 5: pending card must not POST to /retry-now, got:\n%s", html)
}
if strings.Contains(html, "Try now") {
t.Errorf("state 5: 'Try now' verb must not appear, got:\n%s", html)
}
}
// TestVideoCard_TryNowAbsent — "Try now" must not appear in any rendered card.
func TestVideoCard_TryNowAbsent(t *testing.T) {
cases := []store.SummaryRow{
{VideoID: "a", Summarized: true, Summary: "s", AIProvider: "local"},
{VideoID: "b", TranscriptStatus: "none"},
{VideoID: "c", SummarizeRequested: true},
{VideoID: "d", TranscriptStatus: "rate_limited"},
{VideoID: "e"},
}
for _, r := range cases {
html := renderVideoCard(t, r)
if strings.Contains(html, "Try now") {
t.Errorf("'Try now' must not appear in any card state; video %q got:\n%s", r.VideoID, html)
}
}
}