|
1 | 1 | package readability |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "net/url" |
4 | 5 | "strings" |
5 | 6 | "testing" |
6 | 7 |
|
@@ -46,3 +47,116 @@ func TestResolveDocumentURLsKeepsFragmentWithoutBaseElement(t *testing.T) { |
46 | 47 | t.Fatalf("fragment href = %q", got) |
47 | 48 | } |
48 | 49 | } |
| 50 | + |
| 51 | +// TestResolveSrcsetSingleEntry pins the simplest srcset: one URL with a |
| 52 | +// density descriptor. Verifies the URL is resolved relative to base and |
| 53 | +// the descriptor is preserved verbatim. |
| 54 | +func TestResolveSrcsetSingleEntry(t *testing.T) { |
| 55 | + base, _ := url.Parse("https://example.com/articles/") |
| 56 | + got := resolveSrcset("photo.jpg 2x", base) |
| 57 | + if got != "https://example.com/articles/photo.jpg 2x" { |
| 58 | + t.Fatalf("got %q", got) |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +// TestResolveSrcsetMultipleEntries verifies the comma-separated path: |
| 63 | +// each candidate URL must be resolved independently against base, and |
| 64 | +// commas plus inter-entry whitespace must round-trip unchanged. |
| 65 | +func TestResolveSrcsetMultipleEntries(t *testing.T) { |
| 66 | + base, _ := url.Parse("https://example.com/a/b/") |
| 67 | + got := resolveSrcset("small.jpg 1x, /big.jpg 2x, https://cdn.example.com/huge.jpg 3x", base) |
| 68 | + want := "https://example.com/a/b/small.jpg 1x, https://example.com/big.jpg 2x, https://cdn.example.com/huge.jpg 3x" |
| 69 | + if got != want { |
| 70 | + t.Fatalf("got %q\nwant %q", got, want) |
| 71 | + } |
| 72 | +} |
| 73 | + |
| 74 | +// TestResolveSrcsetWidthDescriptor exercises the "w" descriptor variant |
| 75 | +// (width hint) in addition to "x" (density). |
| 76 | +func TestResolveSrcsetWidthDescriptor(t *testing.T) { |
| 77 | + base, _ := url.Parse("https://example.com/") |
| 78 | + got := resolveSrcset("a.jpg 320w, b.jpg 640w", base) |
| 79 | + if got != "https://example.com/a.jpg 320w, https://example.com/b.jpg 640w" { |
| 80 | + t.Fatalf("got %q", got) |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +// TestResolveSrcsetEmpty confirms the empty-input fast path returns |
| 85 | +// empty without panicking — resolveDocumentURLs uses that signal to skip |
| 86 | +// SetAttr. |
| 87 | +func TestResolveSrcsetEmpty(t *testing.T) { |
| 88 | + base, _ := url.Parse("https://example.com/") |
| 89 | + if got := resolveSrcset("", base); got != "" { |
| 90 | + t.Fatalf("empty input should return empty, got %q", got) |
| 91 | + } |
| 92 | +} |
| 93 | + |
| 94 | +// TestResolveSrcsetUppercaseHostLowercased mirrors the host-folding |
| 95 | +// behavior already applied to <a href> / <img src>: srcset URLs land in |
| 96 | +// the same lowercase canonical form so deduplication and comparison are |
| 97 | +// consistent across the document. |
| 98 | +func TestResolveSrcsetUppercaseHostLowercased(t *testing.T) { |
| 99 | + base, _ := url.Parse("https://example.com/") |
| 100 | + got := resolveSrcset("https://CDN.Example.COM/p.jpg 2x", base) |
| 101 | + if got != "https://cdn.example.com/p.jpg 2x" { |
| 102 | + t.Fatalf("host should be lowercased, got %q", got) |
| 103 | + } |
| 104 | +} |
| 105 | + |
| 106 | +// TestResolveDocumentURLsResolvesAllResourceAttrs confirms every |
| 107 | +// resource selector listed in resolveDocumentURLs (a/img/source/video/ |
| 108 | +// audio/iframe) gets its URL resolved when no <base> element is present. |
| 109 | +// The previous tests focused on srcset / fragments / base href; this one |
| 110 | +// nails down the per-tag coverage. |
| 111 | +func TestResolveDocumentURLsResolvesAllResourceAttrs(t *testing.T) { |
| 112 | + doc, err := goquery.NewDocumentFromReader(strings.NewReader(` |
| 113 | + <html><body> |
| 114 | + <a href="story.html">story</a> |
| 115 | + <img src="image.png"> |
| 116 | + <picture><source src="alt.webp"></picture> |
| 117 | + <video src="movie.mp4"></video> |
| 118 | + <audio src="podcast.mp3"></audio> |
| 119 | + <iframe src="https://player.example/embed"></iframe> |
| 120 | + </body></html>`)) |
| 121 | + if err != nil { |
| 122 | + t.Fatal(err) |
| 123 | + } |
| 124 | + resolveDocumentURLs(doc, "https://example.com/articles/page.html") |
| 125 | + |
| 126 | + checks := map[string]string{ |
| 127 | + "a": "https://example.com/articles/story.html", |
| 128 | + "img": "https://example.com/articles/image.png", |
| 129 | + "source": "https://example.com/articles/alt.webp", |
| 130 | + "video": "https://example.com/articles/movie.mp4", |
| 131 | + "audio": "https://example.com/articles/podcast.mp3", |
| 132 | + // iframe absolute URLs are left as-is, host lowercased. |
| 133 | + "iframe": "https://player.example/embed", |
| 134 | + } |
| 135 | + for sel, want := range checks { |
| 136 | + var got string |
| 137 | + if sel == "a" { |
| 138 | + got = attr(doc.Find(sel).First(), "href") |
| 139 | + } else { |
| 140 | + got = attr(doc.Find(sel).First(), "src") |
| 141 | + } |
| 142 | + if got != want { |
| 143 | + t.Fatalf("%s: got %q, want %q", sel, got, want) |
| 144 | + } |
| 145 | + } |
| 146 | +} |
| 147 | + |
| 148 | +// TestResolveDocumentURLsPreservesRelativeIframeProtocol pins the |
| 149 | +// "//host/path" iframe special-case: protocol-relative iframe sources |
| 150 | +// must NOT be rewritten to absolute, matching upstream behavior that |
| 151 | +// preserves embed providers' protocol-agility. |
| 152 | +func TestResolveDocumentURLsPreservesRelativeIframeProtocol(t *testing.T) { |
| 153 | + doc, err := goquery.NewDocumentFromReader(strings.NewReader( |
| 154 | + `<iframe src="//player.example/embed/abc"></iframe>`)) |
| 155 | + if err != nil { |
| 156 | + t.Fatal(err) |
| 157 | + } |
| 158 | + resolveDocumentURLs(doc, "https://example.com/") |
| 159 | + if got := attr(doc.Find("iframe").First(), "src"); got != "//player.example/embed/abc" { |
| 160 | + t.Fatalf("iframe src should remain protocol-relative, got %q", got) |
| 161 | + } |
| 162 | +} |
0 commit comments