]> git.openstreetmap.org Git - rails.git/blob - test/lib/rich_text_test.rb
Merge remote-tracking branch 'upstream/pull/6735'
[rails.git] / test / lib / rich_text_test.rb
1 # frozen_string_literal: true
2
3 require "test_helper"
4
5 class RichTextTest < ActiveSupport::TestCase
6   include Rails::Dom::Testing::Assertions::SelectorAssertions
7
8   def test_html_to_html
9     r = RichText.new("html", "foo http://example.com/ bar")
10     assert_html r do
11       assert_select "a", 1
12       assert_select "a[href='http://example.com/']", 1
13       assert_select "a[rel='nofollow noopener noreferrer']", 1
14     end
15
16     r = RichText.new("html", "foo <a href='http://example.com/'>bar</a> baz")
17     assert_html r do
18       assert_select "a", 1
19       assert_select "a[href='http://example.com/']", 1
20       assert_select "a[rel='nofollow noopener noreferrer']", 1
21     end
22
23     r = RichText.new("html", "foo <a rel='junk me trash' href='http://example.com/'>bar</a> baz")
24     assert_html r do
25       assert_select "a", 1
26       assert_select "a[href='http://example.com/']", 1
27       assert_select "a[rel='me nofollow noopener noreferrer']", 1
28     end
29
30     r = RichText.new("html", "foo example@example.com bar")
31     assert_html r do
32       assert_select "a", 0
33     end
34
35     r = RichText.new("html", "foo <a href='mailto:example@example.com'>bar</a> baz")
36     assert_html r do
37       assert_select "a", 1
38       assert_select "a[href='mailto:example@example.com']", 1
39       assert_select "a[rel='nofollow noopener noreferrer']", 1
40     end
41
42     r = RichText.new("html", "foo <div>bar</div> baz")
43     assert_html r do
44       assert_select "div", false
45       assert_select "p", /^foo *bar *baz$/
46     end
47
48     r = RichText.new("html", "foo <script>bar = 1;</script> baz")
49     assert_html r do
50       assert_select "script", false
51       assert_select "p", /^foo *baz$/
52     end
53
54     r = RichText.new("html", "foo <style>div { display: none; }</style> baz")
55     assert_html r do
56       assert_select "style", false
57       assert_select "p", /^foo *baz$/
58     end
59
60     r = RichText.new("html", "<table><tr><td>column</td></tr></table>")
61     assert_html r do
62       assert_select "table[class='table table-sm w-auto']"
63     end
64
65     r = RichText.new("html", "<p class='btn btn-warning'>Click Me</p>")
66     assert_html r do
67       assert_select "p[class='btn btn-warning']", false
68       assert_select "p", /^Click Me$/
69     end
70
71     r = RichText.new("html", "<p style='color:red'>Danger</p>")
72     assert_html r do
73       assert_select "p[style='color:red']", false
74       assert_select "p", /^Danger$/
75     end
76   end
77
78   def test_html_to_text
79     r = RichText.new("html", "foo <a href='http://example.com/'>bar</a> baz")
80     assert_equal "foo <a href='http://example.com/'>bar</a> baz", r.to_text
81   end
82
83   def test_markdown_to_html
84     r = RichText.new("markdown", "foo http://example.com/ bar")
85     assert_html r do
86       assert_select "a", 1
87       assert_select "a[href='http://example.com/']", 1
88       assert_select "a[rel='nofollow noopener noreferrer']", 1
89     end
90
91     r = RichText.new("markdown", "foo [bar](http://example.com/) baz")
92     assert_html r do
93       assert_select "a", 1
94       assert_select "a[href='http://example.com/']", 1
95       assert_select "a[rel='nofollow noopener noreferrer']", 1
96     end
97
98     r = RichText.new("markdown", "foo <a rel='junk me trash' href='http://example.com/'>bar</a>) baz")
99     assert_html r do
100       assert_select "a", 1
101       assert_select "a[href='http://example.com/']", 1
102       assert_select "a[rel='me nofollow noopener noreferrer']", 1
103     end
104
105     r = RichText.new("markdown", "foo example@example.com bar")
106     assert_html r do
107       assert_select "a", 1
108       assert_select "a[href='mailto:example@example.com']", 1
109       assert_select "a[rel='nofollow noopener noreferrer']", 1
110     end
111
112     r = RichText.new("markdown", "foo [bar](mailto:example@example.com) bar")
113     assert_html r do
114       assert_select "a", 1
115       assert_select "a[href='mailto:example@example.com']", 1
116       assert_select "a[rel='nofollow noopener noreferrer']", 1
117     end
118
119     r = RichText.new("markdown", "foo ![bar](http://example.com/example.png) bar")
120     assert_html r do
121       assert_select "img", 1
122       assert_select "img[alt='bar']", 1
123       assert_select "img[src='http://example.com/example.png']", 1
124     end
125
126     r = RichText.new("markdown", "# foo bar baz")
127     assert_html r do
128       assert_select "h1", "foo bar baz"
129     end
130
131     r = RichText.new("markdown", "## foo bar baz")
132     assert_html r do
133       assert_select "h2", "foo bar baz"
134     end
135
136     r = RichText.new("markdown", "### foo bar baz")
137     assert_html r do
138       assert_select "h3", "foo bar baz"
139     end
140
141     r = RichText.new("markdown", "* foo bar baz")
142     assert_html r do
143       assert_select "ul" do
144         assert_select "li", "foo bar baz"
145       end
146     end
147
148     r = RichText.new("markdown", "1. foo bar baz")
149     assert_html r do
150       assert_select "ol" do
151         assert_select "li", "foo bar baz"
152       end
153     end
154
155     r = RichText.new("markdown", "foo *bar* _baz_ qux")
156     assert_html r do
157       assert_select "em", "bar"
158       assert_select "em", "baz"
159     end
160
161     r = RichText.new("markdown", "foo **bar** __baz__ qux")
162     assert_html r do
163       assert_select "strong", "bar"
164       assert_select "strong", "baz"
165     end
166
167     r = RichText.new("markdown", "foo `bar` baz")
168     assert_html r do
169       assert_select "code", "bar"
170     end
171
172     r = RichText.new("markdown", "    foo bar baz")
173     assert_html r do
174       assert_select "pre", /^\s*foo bar baz\s*$/
175     end
176
177     r = RichText.new("markdown", "|column|column")
178     assert_html r do
179       assert_select "table[class='table table-sm w-auto']"
180     end
181
182     r = RichText.new("markdown", "Click Me\n{:.btn.btn-warning}")
183     assert_html r do
184       assert_select "p[class='btn btn-warning']", false
185       assert_select "p", /^Click Me$/
186     end
187
188     r = RichText.new("markdown", "<p style='color:red'>Danger</p>")
189     assert_html r do
190       assert_select "p[style='color:red']", false
191       assert_select "p", /^Danger$/
192     end
193   end
194
195   def test_markdown_table_alignment
196     # Ensure that kramdown table alignment styles are converted to bootstrap classes
197     markdown_table = <<~MARKDOWN
198       | foo  | bar |
199       |:----:|----:|
200       |center|right|
201     MARKDOWN
202     r = RichText.new("markdown", markdown_table)
203     assert_html r do
204       assert_select "td[style='text-align:center']", false
205       assert_select "td[class='text-center']", true
206       assert_select "td[style='text-align:right']", false
207       assert_select "td[class='text-end']", true
208     end
209   end
210
211   def test_markdown_to_text
212     r = RichText.new("markdown", "foo [bar](http://example.com/) baz")
213     assert_equal "foo [bar](http://example.com/) baz", r.to_text
214   end
215
216   def test_text_to_html_linkify
217     with_settings(:linkify => { :normalisation_rules => [{ :hosts => ["replace-me.example.com"], :host_replacement => "repl.example.com" }] }) do
218       r = RichText.new("text", "foo http://example.com/ bar")
219       assert_html r do
220         assert_dom "a", :count => 1, :text => "http://example.com/" do
221           assert_dom "> @href", "http://example.com/"
222           assert_dom "> @rel", "nofollow noopener noreferrer"
223         end
224       end
225     end
226   end
227
228   def test_text_to_html_linkify_replace
229     with_settings(:linkify => { :normalisation_rules => [{ :hosts => ["replace-me.example.com"], :host_replacement => "repl.example.com" }] }) do
230       r = RichText.new("text", "foo https://replace-me.example.com/some/path?query=te<st&limit=20>10#result12 bar")
231       assert_html r do
232         assert_dom "a", :count => 1, :text => "repl.example.com/some/path?query=te<st&limit=20>10#result12" do
233           assert_dom "> @href", "https://replace-me.example.com/some/path?query=te<st&limit=20>10#result12"
234           assert_dom "> @rel", "nofollow noopener noreferrer"
235         end
236       end
237     end
238   end
239
240   def test_text_to_html_linkify_recognize
241     with_settings(:linkify => { :normalisation_rules => [{ :hosts => ["replace-me.example.com"], :host_replacement => "repl.example.com" }] }) do
242       r = RichText.new("text", "foo repl.example.com/some/path?query=te<st&limit=20>10#result12 bar")
243       assert_html r do
244         assert_dom "a", :count => 1, :text => "repl.example.com/some/path?query=te<st&limit=20>10#result12" do
245           assert_dom "> @href", "http://replace-me.example.com/some/path?query=te<st&limit=20>10#result12"
246           assert_dom "> @rel", "nofollow noopener noreferrer"
247         end
248       end
249     end
250   end
251
252   def test_text_to_html_linkify_replace_other_scheme
253     with_settings(:linkify => { :normalisation_rules => [{ :hosts => ["replace-me.example.com"], :host_replacement => "repl.example.com" }] }) do
254       r = RichText.new("text", "foo ftp://replace-me.example.com/some/path?query=te<st&limit=20>10#result12 bar")
255       assert_html r do
256         assert_dom "a", :count => 1, :text => "ftp://replace-me.example.com/some/path?query=te<st&limit=20>10#result12" do
257           assert_dom "> @href", "ftp://replace-me.example.com/some/path?query=te<st&limit=20>10#result12"
258           assert_dom "> @rel", "nofollow noopener noreferrer"
259         end
260       end
261     end
262   end
263
264   def test_text_to_html_linkify_replace_undefined
265     with_settings(:linkify => { :normalisation_rules => [{ :hosts => ["replace-me.example.com"] }] }) do
266       r = RichText.new("text", "foo https://replace-me.example.com/some/path?query=te<st&limit=20>10#result12 bar")
267       assert_html r do
268         assert_dom "a", :count => 1, :text => "https://replace-me.example.com/some/path?query=te<st&limit=20>10#result12" do
269           assert_dom "> @href", "https://replace-me.example.com/some/path?query=te<st&limit=20>10#result12"
270           assert_dom "> @rel", "nofollow noopener noreferrer"
271         end
272       end
273     end
274   end
275
276   def test_text_to_html_linkify_wiki_replace_prefix
277     with_settings(:linkify => { :normalisation_rules => [{ :hosts => ["replace-me-wiki.example.com"], :host_replacement => "wiki.example.com", :optional_path_prefix => "^/wiki(?=/[A-Z])" }] }) do
278       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
279       assert_html r do
280         assert_dom "a", :count => 1, :text => "wiki.example.com/Tag:surface%3Dmetal" do
281           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal"
282           assert_dom "> @rel", "nofollow noopener noreferrer"
283         end
284       end
285     end
286   end
287
288   def test_text_to_html_linkify_wiki_replace_prefix_undefined
289     with_settings(:linkify => { :normalisation_rules => [{ :hosts => ["replace-me-wiki.example.com"], :host_replacement => "wiki.example.com" }] }) do
290       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
291       assert_html r do
292         assert_dom "a", :count => 1, :text => "wiki.example.com/wiki/Tag:surface%3Dmetal" do
293           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal"
294           assert_dom "> @rel", "nofollow noopener noreferrer"
295         end
296       end
297     end
298   end
299
300   def test_text_to_html_linkify_wiki_replace_undefined_prefix
301     with_settings(:linkify => { :normalisation_rules => [{ :hosts => ["replace-me-wiki.example.com"], :optional_path_prefix => "^/wiki(?=/[A-Z])" }] }) do
302       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
303       assert_html r do
304         assert_dom "a", :count => 1, :text => "https://replace-me-wiki.example.com/Tag:surface%3Dmetal" do
305           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal"
306           assert_dom "> @rel", "nofollow noopener noreferrer"
307         end
308       end
309     end
310   end
311
312   def test_text_to_html_linkify_wiki_replace_prefix_no_match
313     with_settings(:linkify => { :normalisation_rules => [{ :hosts => ["replace-me-wiki.example.com"], :host_replacement => "wiki.example.com", :optional_path_prefix => "^/wiki(?=/[A-Z])" }] }) do
314       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/w bar")
315       assert_html r do
316         assert_dom "a", :count => 1, :text => "wiki.example.com/wiki/w" do
317           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/w"
318           assert_dom "> @rel", "nofollow noopener noreferrer"
319         end
320       end
321     end
322   end
323
324   def test_text_to_html_linkify_recognize_wiki
325     with_settings(:linkify => { :normalisation_rules => [{ :hosts => ["replace-me-wiki.example.com"], :host_replacement => "wiki.example.com", :optional_path_prefix => "^/wiki(?=/[A-Z])" }] }) do
326       r = RichText.new("text", "foo wiki.example.com/Tag:surface%3Dmetal bar")
327       assert_html r do
328         assert_dom "a", :count => 1, :text => "wiki.example.com/Tag:surface%3Dmetal" do
329           assert_dom "> @href", "http://replace-me-wiki.example.com/Tag:surface%3Dmetal"
330           assert_dom "> @rel", "nofollow noopener noreferrer"
331         end
332       end
333     end
334   end
335
336   def test_text_to_html_linkify_idempotent
337     with_settings(:linkify => { :normalisation_rules => [{ :hosts => ["test.host"], :host_replacement => "test.host" }] }) do
338       t0 = "foo https://test.host/way/123456789 bar"
339
340       r1 = RichText.new("text", t0)
341       t1 = Nokogiri::HTML.fragment(r1.to_html).text
342
343       r2 = RichText.new("text", t1)
344       t2 = Nokogiri::HTML.fragment(r2.to_html).text
345
346       assert_equal t1, t2
347     end
348   end
349
350   def test_text_to_html_linkify_recognize_path
351     with_settings(:linkify => { :detection_rules => [{ :patterns => ["@(?<username>\\w+)"], :path_template => "user/\\k<username>" }] }) do
352       r = RichText.new("text", "foo @example bar")
353       assert_html r do
354         assert_dom "a", :count => 1, :text => "http://test.host/user/example" do
355           assert_dom "> @href", "http://test.host/user/example"
356           assert_dom "> @rel", "nofollow noopener noreferrer"
357         end
358       end
359     end
360   end
361
362   def test_text_to_html_linkify_recognize_path_no_partial_match
363     with_settings(:linkify => { :detection_rules => [{ :patterns => ["@(?<username>\\w+)"], :path_template => "user/\\k<username>" }] }) do
364       r = RichText.new("text", "foo example@example.com bar")
365       assert_html r do
366         assert_select "a", 0
367       end
368     end
369   end
370
371   def test_text_to_html_linkify_recognize_wiki_path
372     with_settings(:linkify => { :detection_rules => [{ :patterns => ["(?<key>[^\"?#<>/\\s]+)=(?<value>[^\"?#<>\\s]+)"], :path_template => "Tag:\\k<key>=\\k<value>", :host => "http://example.wiki" }] }) do
373       r = RichText.new("text", "foo surface=metal bar")
374       assert_html r do
375         assert_dom "a", :count => 1, :text => "http://example.wiki/Tag:surface=metal" do
376           assert_dom "> @href", "http://example.wiki/Tag:surface=metal"
377           assert_dom "> @rel", "nofollow noopener noreferrer"
378         end
379       end
380     end
381     with_settings(:linkify => { :detection_rules => [{ :patterns => ["(?<key>[^\"?#<>/\\s]+)=\\*?"], :path_template => "Key:\\k<key>", :host => "http://example.wiki" }] }) do
382       r = RichText.new("text", "foo surface=* bar")
383       assert_html r do
384         assert_dom "a", :count => 1, :text => "http://example.wiki/Key:surface" do
385           assert_dom "> @href", "http://example.wiki/Key:surface"
386           assert_dom "> @rel", "nofollow noopener noreferrer"
387         end
388       end
389     end
390   end
391
392   def test_text_to_html_linkify_openstreetmap_links
393     with_settings(:server_url => "www.openstreetmap.org", :server_protocol => "https") do
394       cases = {
395         "https://www.openstreetmap.org/note/4655490" =>
396           ["note/4655490", "https://www.openstreetmap.org/note/4655490"],
397
398         "https://www.openstreetmap.org/changeset/163353772" =>
399           ["changeset/163353772", "https://www.openstreetmap.org/changeset/163353772"],
400
401         "https://www.openstreetmap.org/way/1249366504" =>
402           ["way/1249366504", "https://www.openstreetmap.org/way/1249366504"],
403
404         "https://www.openstreetmap.org/way/1249366504/history" =>
405           ["way/1249366504/history", "https://www.openstreetmap.org/way/1249366504/history"],
406
407         "https://www.openstreetmap.org/way/1249366504/history/2" =>
408           ["way/1249366504/history/2", "https://www.openstreetmap.org/way/1249366504/history/2"],
409
410         "https://www.openstreetmap.org/node/12639964186" =>
411           ["node/12639964186", "https://www.openstreetmap.org/node/12639964186"],
412
413         "https://www.openstreetmap.org/relation/7876483" =>
414           ["relation/7876483", "https://www.openstreetmap.org/relation/7876483"],
415
416         "https://www.openstreetmap.org/user/aharvey" =>
417           ["@aharvey", "https://www.openstreetmap.org/user/aharvey"],
418
419         "https://wiki.openstreetmap.org/wiki/Key:boundary" =>
420           ["boundary=*", "https://wiki.openstreetmap.org/wiki/Key:boundary"],
421
422         "https://wiki.openstreetmap.org/wiki/Tag:boundary=place" =>
423           ["boundary=place", "https://wiki.openstreetmap.org/wiki/Tag:boundary=place"],
424
425         "boundary=*" =>
426           ["boundary=*", "https://wiki.openstreetmap.org/wiki/Key:boundary"],
427
428         "boundary=place" =>
429           ["boundary=place", "https://wiki.openstreetmap.org/wiki/Tag:boundary=place"],
430
431         "node/12639964186" =>
432           ["node/12639964186", "https://www.openstreetmap.org/node/12639964186"],
433
434         "node 12639964186" =>
435           ["node/12639964186", "https://www.openstreetmap.org/node/12639964186"],
436
437         "n12639964186" =>
438           ["node/12639964186", "https://www.openstreetmap.org/node/12639964186"],
439
440         "way/1249366504" =>
441           ["way/1249366504", "https://www.openstreetmap.org/way/1249366504"],
442
443         "way 1249366504" =>
444           ["way/1249366504", "https://www.openstreetmap.org/way/1249366504"],
445
446         "w1249366504" =>
447           ["way/1249366504", "https://www.openstreetmap.org/way/1249366504"],
448
449         "relation/7876483" =>
450           ["relation/7876483", "https://www.openstreetmap.org/relation/7876483"],
451
452         "relation 7876483" =>
453           ["relation/7876483", "https://www.openstreetmap.org/relation/7876483"],
454
455         "r7876483" =>
456           ["relation/7876483", "https://www.openstreetmap.org/relation/7876483"],
457
458         "changeset/163353772" =>
459           ["changeset/163353772", "https://www.openstreetmap.org/changeset/163353772"],
460
461         "changeset 163353772" =>
462           ["changeset/163353772", "https://www.openstreetmap.org/changeset/163353772"],
463
464         "note/4655490" =>
465           ["note/4655490", "https://www.openstreetmap.org/note/4655490"],
466
467         "note 4655490" =>
468           ["note/4655490", "https://www.openstreetmap.org/note/4655490"]
469       }
470
471       cases.each do |input, (expected_text, expected_href)|
472         r = RichText.new("text", input)
473         assert_html r do
474           assert_dom "a", :count => 1, :text => expected_text do
475             assert_dom "> @href", expected_href
476           end
477         end
478       end
479     end
480   end
481
482   def test_text_to_html_linkify_no_year_misinterpretation
483     r = RichText.new("text", "We thought there was no way 2020 could be worse than 2019. We were wrong. Please note 2025 is the first square year since OSM started. In that year, some osmlab repos switched from node 22 to bun 1.3.")
484     assert_html r do
485       assert_select "a", 0
486     end
487   end
488
489   def test_deactivated_linkify_expansion_in_markdown
490     t0 = "foo `surface=metal` bar"
491
492     r1 = RichText.new("markdown", t0)
493     t1 = Nokogiri::HTML.fragment(r1.to_html).text
494
495     assert_equal t0.delete("`"), t1.strip
496   end
497
498   def test_text_to_html_email
499     r = RichText.new("text", "foo example@example.com bar")
500     assert_html r do
501       assert_select "a", 0
502     end
503   end
504
505   def test_text_to_html_escape
506     r = RichText.new("text", "foo < bar & baz > qux")
507     assert_html r do
508       assert_select "p", "foo < bar & baz > qux"
509     end
510   end
511
512   def test_text_to_text
513     r = RichText.new("text", "foo http://example.com/ bar")
514     assert_equal "foo http://example.com/ bar", r.to_text
515   end
516
517   def test_text_no_opengraph_properties
518     r = RichText.new("text", "foo https://example.com/ bar")
519     assert_nil r.image
520     assert_nil r.image_alt
521     assert_nil r.description
522   end
523
524   def test_html_no_opengraph_properties
525     r = RichText.new("html", "foo <a href='https://example.com/'>bar</a> baz")
526     assert_nil r.image
527     assert_nil r.image_alt
528     assert_nil r.description
529   end
530
531   def test_markdown_no_image
532     r = RichText.new("markdown", "foo [bar](https://example.com/) baz")
533     assert_nil r.image
534     assert_nil r.image_alt
535   end
536
537   def test_markdown_image
538     r = RichText.new("markdown", "foo ![bar](https://example.com/image.jpg) baz")
539     assert_equal "https://example.com/image.jpg", r.image
540     assert_equal "bar", r.image_alt
541   end
542
543   def test_markdown_first_image
544     r = RichText.new("markdown", "foo ![bar1](https://example.com/image1.jpg) baz\nfoo ![bar2](https://example.com/image2.jpg) baz")
545     assert_equal "https://example.com/image1.jpg", r.image
546     assert_equal "bar1", r.image_alt
547   end
548
549   def test_markdown_image_with_empty_src
550     r = RichText.new("markdown", "![invalid]()")
551     assert_nil r.image
552     assert_nil r.image_alt
553   end
554
555   def test_markdown_skip_image_with_empty_src
556     r = RichText.new("markdown", "![invalid]() ![valid](https://example.com/valid.gif)")
557     assert_equal "https://example.com/valid.gif", r.image
558     assert_equal "valid", r.image_alt
559   end
560
561   def test_markdown_html_image
562     r = RichText.new("markdown", "<img src='https://example.com/img_element.png' alt='alt text here'>")
563     assert_equal "https://example.com/img_element.png", r.image
564     assert_equal "alt text here", r.image_alt
565   end
566
567   def test_markdown_html_image_without_alt
568     r = RichText.new("markdown", "<img src='https://example.com/img_element.png'>")
569     assert_equal "https://example.com/img_element.png", r.image
570     assert_nil r.image_alt
571   end
572
573   def test_markdown_html_image_with_empty_src
574     r = RichText.new("markdown", "<img src='' alt='forgot src'>")
575     assert_nil r.image
576     assert_nil r.image_alt
577   end
578
579   def test_markdown_skip_html_image_with_empty_src
580     r = RichText.new("markdown", "<img src='' alt='forgot src'> <img src='https://example.com/next_img_element.png' alt='have src'>")
581     assert_equal "https://example.com/next_img_element.png", r.image
582     assert_equal "have src", r.image_alt
583   end
584
585   def test_markdown_html_image_without_src
586     r = RichText.new("markdown", "<img alt='totally forgot src'>")
587     assert_nil r.image
588     assert_nil r.image_alt
589   end
590
591   def test_markdown_skip_html_image_without_src
592     r = RichText.new("markdown", "<img alt='totally forgot src'> <img src='https://example.com/next_img_element.png' alt='have src'>")
593     assert_equal "https://example.com/next_img_element.png", r.image
594     assert_equal "have src", r.image_alt
595   end
596
597   def test_markdown_no_description
598     r = RichText.new("markdown", "#Nope")
599     assert_nil r.description
600   end
601
602   def test_markdown_description
603     r = RichText.new("markdown", "This is an article about something.")
604     assert_equal "This is an article about something.", r.description
605   end
606
607   def test_markdown_description_after_heading
608     r = RichText.new("markdown", "#Heading\n\nHere starts the text.")
609     assert_equal "Here starts the text.", r.description
610   end
611
612   def test_markdown_description_after_image
613     r = RichText.new("markdown", "![bar](https://example.com/image.jpg)\n\nThis is below the image.")
614     assert_equal "This is below the image.", r.description
615   end
616
617   def test_markdown_description_only_first_paragraph
618     r = RichText.new("markdown", "This thing.\n\nMaybe also that thing.")
619     assert_equal "This thing.", r.description
620   end
621
622   def test_markdown_description_elements
623     r = RichText.new("markdown", "*Something* **important** [here](https://example.com/).")
624     assert_equal "Something important here.", r.description
625   end
626
627   def test_markdown_html_description
628     r = RichText.new("markdown", "<p>Can use HTML tags.</p>")
629     assert_equal "Can use HTML tags.", r.description
630   end
631
632   def test_markdown_description_max_length
633     m = RichText::DESCRIPTION_MAX_LENGTH
634     o = 3 # "...".length
635
636     r = RichText.new("markdown", "x" * m)
637     assert_equal "x" * m, r.description
638
639     r = RichText.new("markdown", "y" * (m + 1))
640     assert_equal "#{'y' * (m - o)}...", r.description
641
642     r = RichText.new("markdown", "*zzzzzzzzz*z" * ((m + 1) / 10.0).ceil)
643     assert_equal "#{'z' * (m - o)}...", r.description
644   end
645
646   def test_markdown_description_word_break_threshold_length
647     m = RichText::DESCRIPTION_MAX_LENGTH
648     t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
649     o = 3 # "...".length
650
651     r = RichText.new("markdown", "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 1)}")
652     assert_equal "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 1)}", r.description
653
654     r = RichText.new("markdown", "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1))}")
655     assert_equal "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 4)}...", r.description
656
657     r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o) - 1)}")
658     assert_equal "#{'x' * (t - o)} #{'y' * (m - (t - o) - 1)}", r.description
659
660     r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o))}")
661     assert_equal "#{'x' * (t - o)}...", r.description
662   end
663
664   def test_markdown_description_word_break_multiple_spaces
665     m = RichText::DESCRIPTION_MAX_LENGTH
666     t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
667     o = 3 # "...".length
668
669     r = RichText.new("markdown", "#{'x' * (t - o)}  #{'y' * (m - (t - o - 1))}")
670     assert_equal "#{'x' * (t - o)}...", r.description
671   end
672
673   private
674
675   def assert_html(richtext, &block)
676     html = richtext.to_html
677     assert_predicate html, :html_safe?
678     root = Nokogiri::HTML::DocumentFragment.parse(html)
679     assert_select root, "*" do
680       yield block
681     end
682   end
683 end