images_test.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. package clean
  2. import (
  3. "reflect"
  4. "strings"
  5. "testing"
  6. "golang.org/x/net/html"
  7. "golang.org/x/net/html/atom"
  8. )
  9. func TestParseSrcset(t *testing.T) {
  10. tests := []struct {
  11. in string
  12. want []candidate
  13. }{
  14. {"a.jpg 400w, b.jpg 1500w", []candidate{{url: "a.jpg", width: 400}, {url: "b.jpg", width: 1500}}},
  15. {"a.jpg, b.jpg 2x", []candidate{{url: "a.jpg"}, {url: "b.jpg", density: 2}}},
  16. {" a.jpg 1x ,\n b.jpg 2x ", []candidate{{url: "a.jpg", density: 1}, {url: "b.jpg", density: 2}}},
  17. // Commas inside URLs, as some image CDNs use.
  18. {"https://cdn.example/w_400,h_300/a.jpg 400w, https://cdn.example/w_1600,h_1200/a.jpg 1600w",
  19. []candidate{{url: "https://cdn.example/w_400,h_300/a.jpg", width: 400}, {url: "https://cdn.example/w_1600,h_1200/a.jpg", width: 1600}}},
  20. {"single.jpg", []candidate{{url: "single.jpg"}}},
  21. {"data:image/gif;base64,R0lG 1x, real.jpg 2x", []candidate{{url: "real.jpg", density: 2}}},
  22. {"", nil},
  23. }
  24. for _, tt := range tests {
  25. if got := parseSrcset(tt.in); !reflect.DeepEqual(got, tt.want) {
  26. t.Errorf("parseSrcset(%q) = %+v, want %+v", tt.in, got, tt.want)
  27. }
  28. }
  29. }
  30. func TestPickCandidate(t *testing.T) {
  31. tests := []struct {
  32. srcset, want string
  33. }{
  34. {"a 400w, b 1500w, c 3000w", "b"}, // smallest that is at least 1404 wide
  35. {"c 3000w, b 1500w, a 400w", "b"}, // order doesn't matter
  36. {"a 400w, b 800w", "b"}, // none wide enough: the widest
  37. {"a 1x, b 2x, c 1.5x", "b"}, // densities: the highest
  38. {"a, b", "a"}, // no descriptors: the first
  39. {"", ""},
  40. }
  41. for _, tt := range tests {
  42. if got := pickCandidate(parseSrcset(tt.srcset)); got != tt.want {
  43. t.Errorf("pickCandidate(%q) = %q, want %q", tt.srcset, got, tt.want)
  44. }
  45. }
  46. }
  47. func TestPrepareImages(t *testing.T) {
  48. tests := []struct{ name, in, want string }{
  49. {"relative src made absolute",
  50. `<img src="photo.jpg" alt="p">`,
  51. `<img src="https://example.com/posts/photo.jpg" alt="p"/>`},
  52. {"lazy data-src replaces placeholder",
  53. `<img src="data:image/gif;base64,R0lGODlhAQABAAAAACw=" data-src="/img/real.png" alt="p">`,
  54. `<img src="https://example.com/img/real.png" data-src="/img/real.png" alt="p"/>`},
  55. {"data-original",
  56. `<img data-original="/img/real.png">`,
  57. `<img data-original="/img/real.png" src="https://example.com/img/real.png"/>`},
  58. {"srcset picks the right width",
  59. `<img src="/s.jpg" srcset="/s.jpg 400w, /m.jpg 1500w, /l.jpg 3000w">`,
  60. `<img src="https://example.com/m.jpg" srcset="/s.jpg 400w, /m.jpg 1500w, /l.jpg 3000w"/>`},
  61. {"picture skips AVIF sources and uses WebP",
  62. `<picture><source srcset="/p.avif" type="image/avif"><source srcset="/p-800.webp 800w, /p-1600.webp 1600w" type="image/webp"><img src="/p.jpg"></picture>`,
  63. `<picture><source srcset="/p.avif" type="image/avif"/><source srcset="/p-800.webp 800w, /p-1600.webp 1600w" type="image/webp"/><img src="https://example.com/p-1600.webp"/></picture>`},
  64. {"picture with only AVIF falls back to the img",
  65. `<picture><source srcset="/p.avif" type="image/avif"><img src="/p.jpg"></picture>`,
  66. `<picture><source srcset="/p.avif" type="image/avif"/><img src="https://example.com/p.jpg"/></picture>`},
  67. {"tracking pixels are removed",
  68. `<p>a<img src="https://t.example/p.gif" width="1" height="1">b<img src="/x.gif" height="2px"></p>`,
  69. `<p>ab</p>`},
  70. {"captions laid out with divs are reduced to text",
  71. `<figure><img src="/a.jpg"><figcaption class="hidden"><div class="caption"><div class="icon"></div><div class="caption-content">The new dialog.<span class="credit">Credit: KDE</span></div></div></figcaption></figure>`,
  72. `<figure><img src="https://example.com/a.jpg"/><p><i>The new dialog. Credit: KDE</i></p></figure>`},
  73. {"zoom links around images are unwrapped",
  74. `<div><a href="/full.jpg" class="cursor-zoom-in"><img src="/small.jpg" alt="x"></a></div>`,
  75. `<div><img src="https://example.com/small.jpg" alt="x"/></div>`},
  76. {"links with text around images are kept",
  77. `<p><a href="/page"><img src="/icon.png" alt=""> Read more</a></p>`,
  78. `<p><a href="https://example.com/page"><img src="https://example.com/icon.png" alt=""/> Read more</a></p>`},
  79. {"figcaption becomes an italic paragraph",
  80. `<figure><img src="/a.jpg"><figcaption class="c">A <b>bold</b> caption</figcaption></figure>`,
  81. `<figure><img src="https://example.com/a.jpg"/><p><i>A <b>bold</b> caption</i></p></figure>`},
  82. }
  83. for _, tt := range tests {
  84. t.Run(tt.name, func(t *testing.T) {
  85. if got := prepareString(t, tt.in); got != tt.want {
  86. t.Errorf("got %q\nwant %q", got, tt.want)
  87. }
  88. })
  89. }
  90. }
  91. func TestBuildFigures(t *testing.T) {
  92. tests := []struct{ name, in, want string }{
  93. {"standalone image with caption",
  94. `<img src="https://example.com/a.jpg" alt="A"><p><i>The caption</i></p><p>Text.</p>`,
  95. `<figure><img src="https://example.com/a.jpg" alt="A"/><figcaption>The caption</figcaption></figure><p>Text.</p>`},
  96. {"em caption",
  97. `<img src="https://example.com/a.jpg"><p><em>Caption</em></p>`,
  98. `<figure><img src="https://example.com/a.jpg"/><figcaption>Caption</figcaption></figure>`},
  99. {"standalone image without caption",
  100. `<img src="https://example.com/a.jpg"><p>Just a paragraph with <i>some</i> italics.</p>`,
  101. `<figure><img src="https://example.com/a.jpg"/></figure><p>Just a paragraph with <i>some</i> italics.</p>`},
  102. {"image inside a paragraph stays inline",
  103. `<p>Look <img src="https://example.com/a.jpg"> here.</p>`,
  104. `<p>Look <img src="https://example.com/a.jpg"/> here.</p>`},
  105. }
  106. for _, tt := range tests {
  107. t.Run(tt.name, func(t *testing.T) {
  108. if got := cleanString(t, tt.in); got != tt.want {
  109. t.Errorf("got %q\nwant %q", got, tt.want)
  110. }
  111. })
  112. }
  113. }
  114. func TestStandInSources(t *testing.T) {
  115. for _, src := range []string{
  116. "https://thumb.example.org/a/Photo.jpg/500px-Photo.jpg?utm_source=en.wikipedia.org&utm_campaign=parser",
  117. "https://images.example.org/media/12345",
  118. "data:image/png;base64,iVBORw0KGgo=",
  119. "https://example.com/plain.png",
  120. } {
  121. standIn := protectSrc(src)
  122. if !strings.HasPrefix(standIn, "https://rekopub.invalid/img/") || !strings.HasSuffix(standIn, ".png") {
  123. t.Errorf("protectSrc(%q) = %q, want a rekopub.invalid URL ending in .png", src, standIn)
  124. }
  125. if got := restoreSrc(standIn); got != src {
  126. t.Errorf("restoreSrc(protectSrc(%q)) = %q", src, got)
  127. }
  128. }
  129. if got := restoreSrc("https://example.com/a.png"); got != "https://example.com/a.png" {
  130. t.Errorf("restoreSrc changed an ordinary URL: %q", got)
  131. }
  132. if got := restoreSrc("https://rekopub.invalid/img/!!!.png"); got != "" {
  133. t.Errorf("restoreSrc of a damaged stand-in = %q, want empty", got)
  134. }
  135. }
  136. func TestPrepareUsesStandIns(t *testing.T) {
  137. doc, err := html.Parse(strings.NewReader(`<body><img src="/a.jpg?x=1"></body>`))
  138. if err != nil {
  139. t.Fatal(err)
  140. }
  141. Prepare(doc, base)
  142. var src string
  143. walk(doc, func(n *html.Node) {
  144. if n.DataAtom == atom.Img {
  145. src = attr(n, "src")
  146. }
  147. })
  148. if !strings.HasPrefix(src, srcStandIn) || restoreSrc(src) != "https://example.com/a.jpg?x=1" {
  149. t.Errorf("src after Prepare = %q", src)
  150. }
  151. }