package monkeyd
import (
"net/url"
"testing"
)
// novelFixture reproduces the two traits that break naive crawlers: the list is
// newest-first, and chapter numbering has a gap (no chapter 4).
const novelFixture = `
TEN TRUYEN
TEN TRUYEN
`
func TestParseNovelPageOrdersChaptersForReading(t *testing.T) {
novel, err := ParseNovelPage([]byte(novelFixture), "https://monkeydd.com/n.html")
if err != nil {
t.Fatalf("ParseNovelPage: %v", err)
}
if novel.Title != "TEN TRUYEN" {
t.Errorf("Title = %q", novel.Title)
}
if novel.Slug != "n" {
t.Errorf("Slug = %q, want %q", novel.Slug, "n")
}
wantLabels := []string{"1", "2", "3", "5"}
if len(novel.Chapters) != len(wantLabels) {
t.Fatalf("got %d chapters, want %d", len(novel.Chapters), len(wantLabels))
}
for i, want := range wantLabels {
if novel.Chapters[i].Label != want {
t.Errorf("chapter %d label = %q, want %q", i, novel.Chapters[i].Label, want)
}
}
// Relative hrefs must resolve against the novel URL.
if got, want := novel.Chapters[1].URL, "https://monkeydd.com/n/2.html"; got != want {
t.Errorf("chapter 2 URL = %q, want %q", got, want)
}
}
// tagsFixture mirrors the real page: the novel's own genres carry
// itemprop="genre", while a site-wide menu links every category without it. A
// parser matching on the /the-loai/ href shape would swallow the whole menu.
const tagsFixture = `TEN TRUYEN
TEN TRUYEN
Thể loại
Trọng Sinh
Cổ Đại
Cổ Đại
`
func TestParseNovelPageReadsTags(t *testing.T) {
novel, err := ParseNovelPage([]byte(tagsFixture), "https://monkeydd.com/n.html")
if err != nil {
t.Fatalf("ParseNovelPage: %v", err)
}
// "Cổ Đại" has its inner whitespace collapsed, the empty anchor falls back
// to its title attribute, and the repeated genre appears once.
want := []string{"Trọng Sinh", "Cổ Đại", "Gia Đình"}
if len(novel.Tags) != len(want) {
t.Fatalf("Tags = %q, want %q", novel.Tags, want)
}
for i := range want {
if novel.Tags[i] != want[i] {
t.Errorf("Tags[%d] = %q, want %q", i, novel.Tags[i], want[i])
}
}
}
func TestParseNovelPageWithoutTags(t *testing.T) {
novel, err := ParseNovelPage([]byte(novelFixture), "https://monkeydd.com/n.html")
if err != nil {
t.Fatalf("ParseNovelPage: %v", err)
}
if len(novel.Tags) != 0 {
t.Errorf("Tags = %q, want none", novel.Tags)
}
}
func TestParseNovelPageNoChapters(t *testing.T) {
if _, err := ParseNovelPage([]byte(`x `),
"https://monkeydd.com/n.html"); err == nil {
t.Fatal("want an error when no chapters are listed")
}
}
const selectFixture = `
5
Chương 3
1
malformed
`
func TestChapterRefsFromSelect(t *testing.T) {
base, err := url.Parse("https://monkeydd.com/n.html")
if err != nil {
t.Fatal(err)
}
refs, err := ChapterRefsFromSelect([]byte(selectFixture), base)
if err != nil {
t.Fatalf("ChapterRefsFromSelect: %v", err)
}
if len(refs) != 3 {
t.Fatalf("got %d refs %+v, want 3 (malformed option skipped)", len(refs), refs)
}
if got, want := refs[0].URL, "https://monkeydd.com/n/1.html"; got != want {
t.Errorf("first ref URL = %q, want %q", got, want)
}
if got, want := refs[1].URL, "https://monkeydd.com/n/chuong-3.html"; got != want {
t.Errorf("second ref URL = %q, want %q", got, want)
}
}
func TestReconcileChapterRefs(t *testing.T) {
ref := func(u string) ChapterRef { return ChapterRef{Label: u, URL: u} }
t.Run("dropdown superset wins", func(t *testing.T) {
list := []ChapterRef{ref("a"), ref("b")}
sel := []ChapterRef{ref("a"), ref("b"), ref("c")}
final, extra := ReconcileChapterRefs(list, sel)
if len(final) != 3 {
t.Errorf("got %d chapters, want the 3 from the dropdown", len(final))
}
if len(extra) != 0 {
t.Errorf("got %d extra, want 0", len(extra))
}
})
t.Run("disagreement is reported not silently merged", func(t *testing.T) {
list := []ChapterRef{ref("a"), ref("z")}
sel := []ChapterRef{ref("a"), ref("b")}
final, extra := ReconcileChapterRefs(list, sel)
if len(final) != 2 || final[1].URL != "z" {
t.Errorf("final = %+v, want the landing page list", final)
}
if len(extra) != 1 || extra[0].URL != "b" {
t.Errorf("extra = %+v, want [b] so the caller can warn", extra)
}
})
t.Run("empty dropdown falls back to the list", func(t *testing.T) {
list := []ChapterRef{ref("a")}
final, extra := ReconcileChapterRefs(list, nil)
if len(final) != 1 || len(extra) != 0 {
t.Errorf("final = %+v, extra = %+v", final, extra)
}
})
}