package web
import (
"bytes"
"context"
"errors"
"io"
"log"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"gitea.kazu634.com/kazu634/tver/internal/tver"
)
type fakeFetcher struct {
episodes []tver.Episode
episodesByGenre map[string][]tver.Episode
err error
calls []string
}
func (f *fakeFetcher) FetchNewer(_ context.Context, genre string) ([]tver.Episode, error) {
f.calls = append(f.calls, genre)
if f.episodesByGenre != nil {
return f.episodesByGenre[genre], f.err
}
return f.episodes, f.err
}
func newTestHandler(fetcher *fakeFetcher, now func() time.Time) http.Handler {
return newTestHandlerWithStore(fetcher, &fakeJobStore{}, now)
}
func newTestHandlerWithStore(fetcher *fakeFetcher, jobs *fakeJobStore, now func() time.Time) http.Handler {
if now == nil {
now = func() time.Time { return time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC) }
}
return NewHandler(fetcher, jobs, now, log.New(io.Discard, "", 0))
}
func getPage(handler http.Handler, path string) *httptest.ResponseRecorder {
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil))
return response
}
func TestNewerPageShowsEpisodeFieldsAndJST(t *testing.T) {
endAt := time.Date(2026, 10, 3, 21, 34, 0, 0, time.UTC).Unix()
fetcher := &fakeFetcher{episodes: []tver.Episode{{
ID: "ep123", SeriesTitle: "番組名", Title: "第1話", BroadcasterName: "放送局",
Duration: 2712, EndAt: endAt, ThumbnailPath: "/images/ep123.jpg",
BroadcastDateLabel: "10月3日放送分",
}}}
response := getPage(newTestHandler(fetcher, nil), "/")
if response.Code != http.StatusOK {
t.Fatalf("status = %d", response.Code)
}
body := response.Body.String()
for _, want := range []string{
"番組名", "第1話", "放送局", "10月3日放送分",
"長さ: 46分",
"https://statics.tver.jp/images/ep123.jpg",
"https://tver.jp/episodes/ep123",
"2026-10-04 06:34 JST", `datetime="2026-10-04T06:34:00+09:00"`,
`value="drama" selected`, `name="hide_previews" value="1" checked`,
`hx-get="/newer"`, `hx-target="#newer-results"`,
} {
if !strings.Contains(body, want) {
t.Errorf("page missing %q", want)
}
}
if len(fetcher.calls) != 1 || fetcher.calls[0] != "drama" {
t.Errorf("calls = %v", fetcher.calls)
}
}
func TestDurationMinutesRoundsUp(t *testing.T) {
for _, tt := range []struct {
seconds int64
minutes int64
}{
{0, 1}, {1, 1}, {59, 1}, {60, 1}, {61, 2}, {2712, 46},
} {
if got := durationMinutes(tt.seconds); got != tt.minutes {
t.Errorf("%d seconds = %d minutes; want %d", tt.seconds, got, tt.minutes)
}
}
}
func TestNewerFragmentUpdatesURLAndFullPageRestoresState(t *testing.T) {
fetcher := &fakeFetcher{episodes: []tver.Episode{{ID: "variety-1", Duration: 200}}}
handler := newTestHandler(fetcher, nil)
response := getPage(handler, "/newer?genre=variety&hide_previews=0")
if response.Code != http.StatusOK {
t.Fatalf("fragment status = %d", response.Code)
}
if got := response.Header().Get("HX-Push-Url"); got != "/?genre=variety&hide_previews=0" {
t.Errorf("HX-Push-Url = %q", got)
}
if body := response.Body.String(); !strings.Contains(body, `id="newer-results"`) || !strings.Contains(body, "variety-1") || strings.Contains(body, "alert(1)`, Title: `title`,
BroadcasterName: `
`, BroadcastDateLabel: `