Replace the plain ASCII spinner with a Charmbracelet-style TUI panel rendered in the browser: a dark terminal card holding a rounded purple box (╭─╮╰─╯│) around a pink block-char tapir (▄▓▀█) with mint eyes (◕ ◕) and a snout that wiggles ∩→∪→~ across three cross-faded frames, plus a lipgloss-style progress bar whose mint fill grows over a dim track via an 8s CSS clip animation. Colours come from named palette consts (CharmPurple/Pink/Mint/Cream/Dim) applied as inline span styles, so the box and CSS share one source of truth. Frames are built by a small run/line helper that pads each row to a fixed interior width — an internal test asserts every box row is the same cell width so the border stays flush. Honours prefers-reduced-motion (static frame + partial bar). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
132 lines
4.2 KiB
Go
132 lines
4.2 KiB
Go
package web_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"gitea.d-ma.be/mathias/tapir/internal/web"
|
|
)
|
|
|
|
// fakeProcessor records ProcessVideo calls. With block set it parks until the
|
|
// channel is closed, so a test can observe the handler return before the
|
|
// background work finishes (proving it ran in a goroutine).
|
|
type fakeProcessor struct {
|
|
block chan struct{}
|
|
done chan struct{}
|
|
calls []string
|
|
}
|
|
|
|
func (f *fakeProcessor) ProcessVideo(_ context.Context, _, videoID string) error {
|
|
if f.block != nil {
|
|
<-f.block
|
|
}
|
|
f.calls = append(f.calls, videoID)
|
|
if f.done != nil {
|
|
close(f.done)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestRequestSummarizeImmediateProcessing(t *testing.T) {
|
|
app := newApp(t)
|
|
p := rawPool(t)
|
|
resetDB(t, p)
|
|
seedVideo(t, p, videoX, "Pending Title", "https://x", time.Time{})
|
|
|
|
fp := &fakeProcessor{block: make(chan struct{}), done: make(chan struct{})}
|
|
app.Processor = fp
|
|
|
|
rec := postSummarize(t, app, videoX, true)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html := body(t, rec)
|
|
|
|
// The processing card came back while ProcessVideo is still parked on block:
|
|
// the work runs in a goroutine, the handler did not wait for it.
|
|
require.Contains(t, html, "Summarizing", "processing card returned")
|
|
require.Contains(t, html, "╭", "charm box rendered")
|
|
require.Contains(t, html, "▓", "tapir body block chars rendered")
|
|
require.Contains(t, html, "∩", "wiggling snout frame rendered")
|
|
require.Contains(t, html, web.CharmPurple, "charm palette applied to the border")
|
|
require.Contains(t, html, "/v/"+videoX+"/status", "card polls the status endpoint")
|
|
require.Contains(t, html, `hx-trigger="every 2s"`, "card auto-polls every 2s")
|
|
require.NotContains(t, html, "Queued", "not the queue-only card")
|
|
|
|
close(fp.block)
|
|
select {
|
|
case <-fp.done:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("ProcessVideo was not called in the background")
|
|
}
|
|
require.Equal(t, []string{videoX}, fp.calls)
|
|
}
|
|
|
|
func TestStatusProcessingThenDone(t *testing.T) {
|
|
ctx := context.Background()
|
|
app := newApp(t)
|
|
p := rawPool(t)
|
|
resetDB(t, p)
|
|
seedVideo(t, p, videoX, "Pending Title", "https://x", time.Time{})
|
|
|
|
// Park ProcessVideo so the video stays in-flight while we poll status.
|
|
fp := &fakeProcessor{block: make(chan struct{})}
|
|
app.Processor = fp
|
|
require.Equal(t, http.StatusOK, postSummarize(t, app, videoX, true).Code)
|
|
|
|
// Processing: status returns the animation card, still polling.
|
|
rec := getStatus(t, app, videoX)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html := body(t, rec)
|
|
require.Contains(t, html, "Summarizing", "in-flight → animation card")
|
|
require.Contains(t, html, `hx-trigger="every 2s"`, "still polling")
|
|
|
|
close(fp.block)
|
|
|
|
// Done: once a summary exists, status returns the summary card with no poll.
|
|
require.NoError(t, deliver(ctx, app, videoX, "the summary body"))
|
|
rec = getStatus(t, app, videoX)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html = body(t, rec)
|
|
require.NotContains(t, html, "Summarizing", "done → no animation")
|
|
require.NotContains(t, html, "every 2s", "done card does not poll (polling stops)")
|
|
require.Contains(t, html, "/v/"+videoX+"\"", "links to the detail page")
|
|
}
|
|
|
|
func TestStatusQueuedWhenNotInFlight(t *testing.T) {
|
|
ctx := context.Background()
|
|
app := newApp(t)
|
|
p := rawPool(t)
|
|
resetDB(t, p)
|
|
seedVideo(t, p, videoX, "Pending Title", "https://x", time.Time{})
|
|
|
|
// Flag set but nothing in-flight (e.g. queue-only, or after a restart).
|
|
require.NoError(t, app.Store.RequestSummarize(ctx, userID, videoX))
|
|
|
|
rec := getStatus(t, app, videoX)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html := body(t, rec)
|
|
require.Contains(t, html, "Queued", "queued chip card")
|
|
require.NotContains(t, html, "Summarizing", "not processing")
|
|
require.NotContains(t, html, "every 2s", "queued card does not poll")
|
|
}
|
|
|
|
func TestStatusNotFound(t *testing.T) {
|
|
app := newApp(t)
|
|
resetDB(t, rawPool(t))
|
|
rec := getStatus(t, app, videoX)
|
|
require.Equal(t, http.StatusNotFound, rec.Code)
|
|
}
|
|
|
|
func getStatus(t *testing.T, app *web.App, videoID string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodGet, "/v/"+videoID+"/status", nil)
|
|
req.Header.Set("HX-Request", "true")
|
|
rec := httptest.NewRecorder()
|
|
app.Router().ServeHTTP(rec, req)
|
|
return rec
|
|
}
|