package latest import "testing" // Trimmed from https://asurascans.com/comics/chronicles-of-the-demon-faction-f886a8af // fetched 2026-07-26. The first anchor is the "First Chapter" shortcut: it is a // real chapter link with no "Chapter N" text, and it must not be mistaken for // the latest just because it parses. const asuraSeriesFixture = ` First Chapter Chapter 179 Chapter 181 Chapter 180 ` // A chapter link belonging to a different series, of the kind a "you might also // like" strip would introduce. Slug scoping must exclude it. const asuraCrossSeriesFixture = asuraSeriesFixture + ` Chapter 999 ` // Trimmed from https://demonicscans.org/manga/Catastrophic-Necromancer fetched // 2026-07-26. Note the raw "&", the doubled space after Chapter 0.5 Chapter 294 Chapter 296 Chapter 295 ` // What Cloudflare serves instead of the page when an IP's bot score flips. const challengeFixture = `Just a moment...
Checking your browser
` // Trimmed from the server-rendered HTML of // https://comix.to/title/n8we-dungeons-and-crayons fetched 2026-08-03. comix is // an SPA: the page ships a JSON state blob rather than a list of chapter // anchors, and latestChapterUrl is where the newest chapter actually lives. const comixSeriesFixture = ` {"firstChapterUrl":"/title/n8we-dungeons-and-crayons/5038739-chapter-1","latestChapterUrl":"/title/n8we-dungeons-and-crayons/11139891-chapter-80"}, {""manga","recommended","n8we",1]":{"items":[{"latestChapterUrl":"/title/qqwrm-full-time-awakening/99999999-chapter-999"}]} ` // The kagane branch is fed by the browser fetcher, so the body is API JSON, not // HTML. Trimmed from GET /api/v2/series/ on 2026-08-03. const kaganeAPIFixture = ` {"series_id":"019f84bc-9ba0-7ed9-86f5-8b905ec7c28b","title":"Infinite Decryption", "series_books":[{"book_id":"a","title":"Episode 1","chapter_no":"1","sort_no":1}, {"book_id":"b","title":"Episode 41","chapter_no":"41","sort_no":41}, {"book_id":"c","title":"Episode 40.5","chapter_no":"40.5","sort_no":40}]} ` // Trimmed from https://novelfull.com/reverend-insanity.html fetched 2026-08-05. // The page carries a newest-first "latest chapters" widget above an // oldest-first paginated list, so the newest anchor is deliberately NOT last — // only a maximum finds it. The final anchor belongs to another series and must // be excluded by slug scoping. const novelfullSeriesFixture = ` Chapter 9999 ` // Trimmed from https://lightnovelworld.net/novel/a-will-eternal/ fetched // 2026-08-05. Its chapter anchors are absolute and flat — /-chapter-/ // at the site root, not under /novel/. The last anchor is another series'. const lnwSeriesFixture = ` Chapter 1 Chapter 1317 Chapter 1298 Chapter 9999 ` func TestLatestChapterFrom(t *testing.T) { const asuraURL = "https://asurascans.com/comics/chronicles-of-the-demon-faction-f886a8af" const demonicURL = "https://demonicscans.org/manga/Catastrophic-Necromancer" tests := []struct { name string site string seriesURL string body string wantOK bool wantNum float64 wantLabel string }{ { name: "asura takes the max, not the last listed", site: "asura", seriesURL: asuraURL, body: asuraSeriesFixture, wantOK: true, wantNum: 181, wantLabel: "Chapter 181", }, { name: "asura ignores another series' chapter links", site: "asura", seriesURL: asuraURL, body: asuraCrossSeriesFixture, wantOK: true, wantNum: 181, wantLabel: "Chapter 181", }, { name: "asura scoping survives a build-hash rotation", site: "asura", seriesURL: "https://asurascans.com/comics/chronicles-of-the-demon-faction-059befe1", body: asuraCrossSeriesFixture, wantOK: true, wantNum: 181, wantLabel: "Chapter 181", }, { name: "asura with an unparseable series url", site: "asura", seriesURL: "https://asurascans.com/", body: asuraSeriesFixture, wantOK: false, }, { name: "demonic takes the max across raw and escaped ampersands", site: "demonic", seriesURL: demonicURL, body: demonicSeriesFixture, wantOK: true, wantNum: 296, wantLabel: "Chapter 296", }, { name: "demonic keeps decimal chapters parseable", site: "demonic", seriesURL: demonicURL, body: `Chapter 0.5`, wantOK: true, wantNum: 0.5, wantLabel: "Chapter 0.5", }, { name: "empty body", site: "asura", seriesURL: asuraURL, body: "", wantOK: false, }, { name: "cloudflare challenge page", site: "asura", seriesURL: asuraURL, body: challengeFixture, wantOK: false, }, { name: "demonic markup handed to the asura rule", site: "asura", seriesURL: asuraURL, body: demonicSeriesFixture, wantOK: false, }, { name: "unknown site", site: "mangadex", seriesURL: "https://example.com/x", body: asuraSeriesFixture, wantOK: false, }, { name: "comix reads latestChapterUrl, scoped to this series", site: "comix", seriesURL: "https://comix.to/title/n8we-dungeons-and-crayons", body: comixSeriesFixture, wantOK: true, wantNum: 80, wantLabel: "Chapter 80", }, { name: "comix yields nothing on a challenge page", site: "comix", seriesURL: "https://comix.to/title/n8we-dungeons-and-crayons", body: challengeFixture, wantOK: false, }, { name: "kagane takes the max chapter_no from API json", site: "kagane", seriesURL: "https://kagane.to/series/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b", body: kaganeAPIFixture, wantOK: true, wantNum: 41, wantLabel: "Chapter 41", }, { name: "kagane yields nothing on a challenge page", site: "kagane", seriesURL: "https://kagane.to/series/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b", body: challengeFixture, wantOK: false, }, { name: "novelfull takes the max and ignores another series", site: "novelfull", seriesURL: "https://novelfull.com/reverend-insanity.html", body: novelfullSeriesFixture, wantOK: true, wantNum: 2334, wantLabel: "Chapter 2334", }, { name: "novelfull yields nothing on a challenge page", site: "novelfull", seriesURL: "https://novelfull.com/reverend-insanity.html", body: challengeFixture, wantOK: false, }, { name: "novelfull with an unparseable series url", site: "novelfull", seriesURL: "https://novelfull.com/genre/Fantasy", body: novelfullSeriesFixture, wantOK: false, }, { name: "lightnovelworld takes the max and ignores another series", site: "lightnovelworld", seriesURL: "https://lightnovelworld.net/novel/a-will-eternal/", body: lnwSeriesFixture, wantOK: true, wantNum: 1317, wantLabel: "Chapter 1317", }, { name: "lightnovelworld tolerates a series url with no trailing slash", site: "lightnovelworld", seriesURL: "https://lightnovelworld.net/novel/a-will-eternal", body: lnwSeriesFixture, wantOK: true, wantNum: 1317, wantLabel: "Chapter 1317", }, { name: "lightnovelworld yields nothing on a challenge page", site: "lightnovelworld", seriesURL: "https://lightnovelworld.net/novel/a-will-eternal/", body: challengeFixture, wantOK: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, ok := latestChapterFrom(tt.site, tt.seriesURL, tt.body) if ok != tt.wantOK { t.Fatalf("ok = %v, want %v (got %+v)", ok, tt.wantOK, got) } if !tt.wantOK { return } if got.Num != tt.wantNum { t.Errorf("Num = %v, want %v", got.Num, tt.wantNum) } if got.Label != tt.wantLabel { t.Errorf("Label = %q, want %q", got.Label, tt.wantLabel) } }) } }