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 ` // Trimmed from https://asurascans.com/comics/chronicles-of-the-demon-faction-00dcbf97 on 2026-08-10. const asuraCoverFixture = `` // Trimmed from https://demonicscans.org/manga/Catastrophic-Necromancer on 2026-08-10. // The source publishes the raw space in this URL. const demonicCoverFixture = `` // Trimmed from https://comix.to/title/n8we-dungeons-and-crayons on 2026-08-10. // comix publishes medium and large renditions in its state blob, with no og:image. const comixCoverFixture = `{"hid":"recommended","poster":{"medium":"https://static.comix.to/recommended@280.jpg","large":"https://static.comix.to/recommended.jpg"},"hid":"n8we","poster":{"medium":"https://static.comix.to/039d/i/1/34/6a6742bf15736@280.jpg","large":"https://static.comix.to/039d/i/1/34/6a6742bf15736.jpg"}}` // Trimmed from GET https://kagane.to/api/v2/series/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b on 2026-08-09. const kaganeCoverFixture = `{"image":"https://kagane.to/api/v2/image/00000000-0000-0000-0000-000000000000/compressed","cover":"https://kagane.to/api/v2/image/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b/compressed"}` // Trimmed from https://novelfull.com/reverend-insanity.html on 2026-08-10. const novelfullCoverFixture = `` // Trimmed from https://lightnovelworld.net/novel/a-will-eternal/ on 2026-08-10. const lnwCoverFixture = `` func TestCoverFrom(t *testing.T) { const comixURL = "https://comix.to/title/n8we-dungeons-and-crayons" tests := []struct { name string site string seriesURL string body string wantOK bool wantCover string }{ { name: "asura uses published metadata URL", site: "asura", body: asuraCoverFixture, wantOK: true, wantCover: "https://cdn.asurascans.com/asura-images/covers/chronicles-of-the-demon-faction.d4dcb8.webp", }, { name: "demonic escapes raw spaces", site: "demonic", body: demonicCoverFixture, wantOK: true, wantCover: "https://readermc.org/images/thumbnails/Catastrophic%20Necromancer.webp", }, { name: "comix takes target medium poster", site: "comix", seriesURL: comixURL, body: comixCoverFixture, wantOK: true, wantCover: "https://static.comix.to/039d/i/1/34/6a6742bf15736@280.jpg", }, { name: "kagane reads API cover field", site: "kagane", body: kaganeCoverFixture, wantOK: true, wantCover: "https://kagane.to/api/v2/image/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b/compressed", }, { name: "novelfull reads image metadata", site: "novelfull", body: novelfullCoverFixture, wantOK: true, wantCover: "https://novelfull.com/uploads/webp/novel/reverend-insanity-82661d911a.webp", }, { name: "lightnovelworld reads og image", site: "lightnovelworld", body: lnwCoverFixture, wantOK: true, wantCover: "https://lightnovelworld.net/wp-content/uploads/2026/03/a-will-eternal-1.webp", }, { name: "page without cover is empty", site: "asura", body: ``, }, { name: "unknown site is empty", site: "unknown", body: asuraCoverFixture, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, ok := coverFrom(tt.site, tt.seriesURL, tt.body) if ok != tt.wantOK { t.Fatalf("ok = %v, want %v (got %q)", ok, tt.wantOK, got) } if got != tt.wantCover { t.Errorf("cover = %q, want %q", got, tt.wantCover) } }) } } func TestCoverFromChallenge(t *testing.T) { seriesURLs := map[string]string{ "asura": "https://asurascans.com/comics/chronicles-of-the-demon-faction-f886a8af", "demonic": "https://demonicscans.org/manga/Catastrophic-Necromancer", "comix": "https://comix.to/title/n8we-dungeons-and-crayons", "kagane": "https://kagane.to/series/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b", "novelfull": "https://novelfull.com/reverend-insanity.html", "lightnovelworld": "https://lightnovelworld.net/novel/a-will-eternal/", } for site, seriesURL := range seriesURLs { t.Run(site, func(t *testing.T) { if got, ok := coverFrom(site, seriesURL, challengeFixture); ok || got != "" { t.Fatalf("cover = %q, ok = %v, want empty", got, ok) } }) } } 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) } }) } }