extract_test.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. package extract
  2. import (
  3. "errors"
  4. "net/url"
  5. "os"
  6. "strings"
  7. "testing"
  8. "time"
  9. "golang.org/x/net/html"
  10. )
  11. func extractFile(t *testing.T, name, pageURL string) (*Article, error) {
  12. t.Helper()
  13. data, err := os.ReadFile("../../testdata/pages/" + name)
  14. if err != nil {
  15. t.Fatal(err)
  16. }
  17. u, err := url.Parse(pageURL)
  18. if err != nil {
  19. t.Fatal(err)
  20. }
  21. return Extract(parse(t, string(data)), u)
  22. }
  23. func parse(t *testing.T, page string) *html.Node {
  24. t.Helper()
  25. doc, err := html.Parse(strings.NewReader(page))
  26. if err != nil {
  27. t.Fatal(err)
  28. }
  29. return doc
  30. }
  31. func render(t *testing.T, n *html.Node) string {
  32. t.Helper()
  33. var b strings.Builder
  34. for c := n.FirstChild; c != nil; c = c.NextSibling {
  35. if err := html.Render(&b, c); err != nil {
  36. t.Fatal(err)
  37. }
  38. }
  39. return b.String()
  40. }
  41. func TestExtractBlog(t *testing.T) {
  42. a, err := extractFile(t, "blog.html", "https://reading.example.com/posts/eink")
  43. if err != nil {
  44. t.Fatal(err)
  45. }
  46. checks := []struct{ field, got, want string }{
  47. {"Title", a.Title, "Why E-Ink Is Easier on the Eyes"},
  48. {"Author", a.Author, "Ada Lovelace"},
  49. {"SiteName", a.SiteName, "The Reading Desk"},
  50. {"Language", a.Language, "en"},
  51. {"URL", a.URL, "https://reading.example.com/posts/eink"},
  52. }
  53. for _, c := range checks {
  54. if c.got != c.want {
  55. t.Errorf("%s = %q, want %q", c.field, c.got, c.want)
  56. }
  57. }
  58. if !strings.HasPrefix(a.Description, "A look at how electronic paper") {
  59. t.Errorf("Description = %q", a.Description)
  60. }
  61. if want := time.Date(2025, 3, 14, 0, 0, 0, 0, time.UTC); !a.Published.Equal(want) {
  62. t.Errorf("Published = %v, want %v", a.Published, want)
  63. }
  64. content := render(t, a.Content)
  65. for _, want := range []string{
  66. "Electronic paper does not shine light into your eyes.",
  67. "<h2>How the display works</h2>",
  68. "<blockquote>",
  69. "<li>Readable in direct sunlight</li>",
  70. "hard to beat &amp; well worth",
  71. // Links are made absolute.
  72. `<a href="https://reading.example.com/guides/choosing-an-ereader">`,
  73. } {
  74. if !strings.Contains(content, want) {
  75. t.Errorf("content is missing %q", want)
  76. }
  77. }
  78. for _, unwanted := range []string{
  79. "Subscribe to our newsletter", // navigation
  80. "We use cookies", // cookie banner
  81. "Related posts", // sidebar
  82. "Great article, thanks!", // comments
  83. "All rights reserved", // footer
  84. "window.dataLayer", // script
  85. "<h1>", // title repeated in the content
  86. } {
  87. if strings.Contains(content, unwanted) {
  88. t.Errorf("content contains %q", unwanted)
  89. }
  90. }
  91. }
  92. func TestExtractNoContent(t *testing.T) {
  93. pages := map[string]string{
  94. "JavaScript shell": `<!DOCTYPE html><html><head><title>App</title></head>
  95. <body><div id="root"></div><script src="/app.js"></script></body></html>`,
  96. "too short": `<html><head><title>Short</title></head>
  97. <body><article><p>Just a sentence.</p></article></body></html>`,
  98. "empty": ``,
  99. }
  100. u, _ := url.Parse("https://example.com/")
  101. for name, page := range pages {
  102. if _, err := Extract(parse(t, page), u); !errors.Is(err, ErrNoContent) {
  103. t.Errorf("%s: error = %v, want ErrNoContent", name, err)
  104. }
  105. }
  106. }
  107. func TestLanguageTag(t *testing.T) {
  108. tests := map[string]string{
  109. "en": "en", "en-US": "en-US", "en_GB": "en-GB", " fr ": "fr", "pt-br": "pt-BR",
  110. "": "en", "not a language": "en", "und": "en",
  111. }
  112. for in, want := range tests {
  113. if got := languageTag(in); got != want {
  114. t.Errorf("languageTag(%q) = %q, want %q", in, got, want)
  115. }
  116. }
  117. }
  118. func TestRemoveLeadingTitle(t *testing.T) {
  119. tests := []struct {
  120. name, body, title, want string
  121. }{
  122. {"matching h1", "<h1>The Title</h1><p>text</p>", "The Title", "<p>text</p>"},
  123. {"case and spacing", "\n <h1> the TITLE </h1><p>text</p>", "The Title", "<p>text</p>"},
  124. {"matching h2", "<h2>The Title</h2><p>text</p>", "The Title", "<p>text</p>"},
  125. {"different heading", "<h1>Other</h1><p>text</p>", "The Title", "<h1>Other</h1><p>text</p>"},
  126. {"not first", "<p>intro</p><h1>The Title</h1>", "The Title", "<p>intro</p><h1>The Title</h1>"},
  127. {"paragraph", "<p>The Title</p>", "The Title", "<p>The Title</p>"},
  128. }
  129. for _, tt := range tests {
  130. t.Run(tt.name, func(t *testing.T) {
  131. doc, err := html.Parse(strings.NewReader("<body>" + tt.body + "</body>"))
  132. if err != nil {
  133. t.Fatal(err)
  134. }
  135. body := doc.FirstChild.LastChild // html > body
  136. removeLeadingTitle(body, tt.title)
  137. got := strings.TrimSpace(render(t, body))
  138. if got != tt.want {
  139. t.Errorf("got %q, want %q", got, tt.want)
  140. }
  141. })
  142. }
  143. }