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, "