Add finished Series filter; clock-driven hygiene predicates exclude it (#159)
This commit is contained in:
+98
-2
@@ -2139,7 +2139,6 @@ func TestNovelPageOmitsUpdatedTab(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func TestMangaPageKeepsUpdatedTab(t *testing.T) {
|
||||
cfg := testConfig()
|
||||
srv, st := newWebTestServer(t, cfg)
|
||||
@@ -2308,6 +2307,42 @@ func TestSeriesListFilterWiring(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The finished filter is a first-class option: the select carries it with
|
||||
// its label and library-wide count, and entering it lists exactly the
|
||||
// retired rows — picking it up from order-plus-label like every other
|
||||
// filter, with no per-filter branch in the handler.
|
||||
func TestSeriesListFinishedFilterOption(t *testing.T) {
|
||||
st, dsn := newTestStoreURL(t)
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("open %s: %v", dsn, err)
|
||||
}
|
||||
defer db.Close()
|
||||
seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:healthy", url: "u", cover: "c", checkedAt: time.Now().UnixMilli(), latestNum: floatPtr(10), bookmarks: 1})
|
||||
seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:retired", url: "u", cover: "c", checkedAt: time.Now().UnixMilli(), latestNum: floatPtr(1), bookmarks: 1})
|
||||
if err := st.SetSeriesFinished("asura", "retired", time.Now().UnixMilli()); err != nil {
|
||||
t.Fatalf("finish asura:retired: %v", err)
|
||||
}
|
||||
srv := newRouter(st, testConfig())
|
||||
|
||||
body := adminSeriesPage(t, srv, st, "?filter=finished")
|
||||
if !strings.Contains(body, "Title of asura:retired") {
|
||||
t.Errorf("finished list misses its row:\n%s", body)
|
||||
}
|
||||
if strings.Contains(body, "Title of asura:healthy") {
|
||||
t.Errorf("finished list renders an unfinished row:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, "1 series") || !strings.Contains(body, "Finished") {
|
||||
t.Errorf("finished heading lacks the count and label:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, "Finished (1)") {
|
||||
t.Errorf("the finished option lacks its count:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `<option value="finished" selected>`) {
|
||||
t.Errorf("the finished option is not selected:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
// ?filter=stale&site=kagane&kind=manga narrows on all three at once: only the
|
||||
// kagane manga stale row renders, and every link the page emits carries the
|
||||
// filter and Site so the narrowing survives in the URL.
|
||||
@@ -2992,6 +3027,67 @@ func TestOverviewZeroIsAnUnlinkedDigit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The finished figure rides last in the Hygiene block: seriesFilterOrder
|
||||
// appends it at the tail, the block renders order[1:], and the count comes
|
||||
// from the same aggregate the select is numbered from. Asserting on the body
|
||||
// order catches a render that merely shows the figure somewhere.
|
||||
func TestOverviewFinishedFigureRidesLast(t *testing.T) {
|
||||
st, dsn := newTestStoreURL(t)
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("open %s: %v", dsn, err)
|
||||
}
|
||||
defer db.Close()
|
||||
seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:healthy", url: "https://asurascans.com/comics/healthy", cover: "aaa", checkedAt: time.Now().UnixMilli(), latestNum: floatPtr(10), bookmarks: 1})
|
||||
seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:retired", url: "https://asurascans.com/comics/retired", cover: "bbb", checkedAt: time.Now().UnixMilli(), latestNum: floatPtr(1), bookmarks: 1})
|
||||
if err := st.SetSeriesFinished("asura", "retired", time.Now().UnixMilli()); err != nil {
|
||||
t.Fatalf("finish asura:retired: %v", err)
|
||||
}
|
||||
srv := newRouter(st, testConfig())
|
||||
body := overviewBody(t, srv, st)
|
||||
|
||||
stat := regexp.MustCompile(`<div class="stat"><span class="lbl">(.*?)</span>(?:<a class="fig"[^>]*>(\d+)</a>|<span class="fig zero">(\d+)</span>)</div>`)
|
||||
var labels []string
|
||||
for _, m := range stat.FindAllStringSubmatch(body, -1) {
|
||||
switch m[1] {
|
||||
case "Series", "Manga", "Novels", "Readers":
|
||||
continue // the Library block, not the hygiene figures
|
||||
}
|
||||
labels = append(labels, m[1])
|
||||
}
|
||||
if len(labels) != 8 {
|
||||
t.Fatalf("hygiene block renders %d figures, want 8 (seven problems + finished):\n%s", len(labels), body)
|
||||
}
|
||||
if labels[len(labels)-1] != "Finished" {
|
||||
t.Errorf("finished figure does not ride last; hygiene order = %v:\n%s", labels, body)
|
||||
}
|
||||
if !strings.Contains(body, `href="/admin/series?filter=finished">1</a>`) {
|
||||
t.Errorf("the finished figure lacks its count and door:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
// A measured zero finished count stays on the page as a muted digit, never a
|
||||
// link: following it would land on an empty list, exactly like every other
|
||||
// zero figure.
|
||||
func TestOverviewFinishedZeroIsAnUnlinkedDigit(t *testing.T) {
|
||||
st, dsn := newTestStoreURL(t)
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("open %s: %v", dsn, err)
|
||||
}
|
||||
defer db.Close()
|
||||
seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:one", url: "https://asurascans.com/comics/one", cover: "aaa", checkedAt: time.Now().UnixMilli(), latestNum: floatPtr(1), bookmarks: 1})
|
||||
srv := newRouter(st, testConfig())
|
||||
body := overviewBody(t, srv, st)
|
||||
|
||||
if !strings.Contains(body, `Finished</span><span class="fig zero">0</span>`) {
|
||||
t.Errorf("a zero finished figure does not render as a muted digit:\n%s", body)
|
||||
}
|
||||
if strings.Contains(body, `href="/admin/series?filter=finished"`) {
|
||||
t.Errorf("a zero finished figure is still a link:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
// The waiting figure sums Due over the latest pass per Site — older passes
|
||||
// for the same Site must not double-count, so the verdict reads the same
|
||||
// latest-per-Site projection the Lanes page reads.
|
||||
@@ -3495,7 +3591,7 @@ func TestSeriesFinishRoute(t *testing.T) {
|
||||
cookie := sessionCookie(t, st)
|
||||
|
||||
for path, want := range map[string]int{
|
||||
"/admin/series/solo/finish": http.StatusBadRequest,
|
||||
"/admin/series/solo/finish": http.StatusBadRequest,
|
||||
"/admin/series/ghost:x/finish": http.StatusNotFound,
|
||||
} {
|
||||
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(""))
|
||||
|
||||
Reference in New Issue
Block a user