]> git.openstreetmap.org Git - rails.git/blob - test/lib/rich_text_test.rb
Pretty-print URLs in linkify
[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_hosts => ["replace-me.example.com"], :linkify_hosts_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_hosts => ["replace-me.example.com"], :linkify_hosts_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_hosts => ["replace-me.example.com"], :linkify_hosts_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_hosts => ["replace-me.example.com"], :linkify_hosts_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_hosts => ["replace-me.example.com"], :linkify_hosts_replacement => nil) 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_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => "wiki.example.com",
278                   :linkify_wiki_optional_path_prefix => "^/wiki(?=/[A-Z])") do
279       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
280       assert_html r do
281         assert_dom "a", :count => 1, :text => "wiki.example.com/Tag:surface%3Dmetal" do
282           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal"
283           assert_dom "> @rel", "nofollow noopener noreferrer"
284         end
285       end
286     end
287   end
288
289   def test_text_to_html_linkify_wiki_replace_prefix_undefined
290     with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => "wiki.example.com",
291                   :linkify_wiki_optional_path_prefix => nil) do
292       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
293       assert_html r do
294         assert_dom "a", :count => 1, :text => "wiki.example.com/wiki/Tag:surface%3Dmetal" do
295           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal"
296           assert_dom "> @rel", "nofollow noopener noreferrer"
297         end
298       end
299     end
300   end
301
302   def test_text_to_html_linkify_wiki_replace_undefined_prefix
303     with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => nil,
304                   :linkify_wiki_optional_path_prefix => "^/wiki(?=/[A-Z])") do
305       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
306       assert_html r do
307         assert_dom "a", :count => 1, :text => "https://replace-me-wiki.example.com/Tag:surface%3Dmetal" do
308           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal"
309           assert_dom "> @rel", "nofollow noopener noreferrer"
310         end
311       end
312     end
313   end
314
315   def test_text_to_html_linkify_wiki_replace_prefix_no_match
316     with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => "wiki.example.com",
317                   :linkify_wiki_optional_path_prefix => "^/wiki(?=/[A-Z])") do
318       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/w bar")
319       assert_html r do
320         assert_dom "a", :count => 1, :text => "wiki.example.com/wiki/w" do
321           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/w"
322           assert_dom "> @rel", "nofollow noopener noreferrer"
323         end
324       end
325     end
326   end
327
328   def test_text_to_html_linkify_recognize_wiki
329     with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => "wiki.example.com",
330                   :linkify_wiki_optional_path_prefix => "^/wiki(?=/[A-Z])") do
331       r = RichText.new("text", "foo wiki.example.com/Tag:surface%3Dmetal bar")
332       assert_html r do
333         assert_dom "a", :count => 1, :text => "wiki.example.com/Tag:surface%3Dmetal" do
334           assert_dom "> @href", "http://replace-me-wiki.example.com/Tag:surface%3Dmetal"
335           assert_dom "> @rel", "nofollow noopener noreferrer"
336         end
337       end
338     end
339   end
340
341   def test_text_to_html_linkify_idempotent
342     with_settings(:linkify_hosts => ["test.host"], :linkify_hosts_replacement => "test.host") do
343       t0 = "foo https://test.host/way/123456789 bar"
344
345       r1 = RichText.new("text", t0)
346       t1 = Nokogiri::HTML.fragment(r1.to_html).text
347
348       r2 = RichText.new("text", t1)
349       t2 = Nokogiri::HTML.fragment(r2.to_html).text
350
351       assert_equal t1, t2
352     end
353   end
354
355   def test_text_to_html_linkify_recognize_path
356     with_settings(:linkify => { :detection_rules => [{ :patterns => ["@(?<username>\\w+)"], :path_template => "user/\\k<username>" }] }) do
357       r = RichText.new("text", "foo @example bar")
358       assert_html r do
359         assert_dom "a", :count => 1, :text => "http://test.host/user/example" do
360           assert_dom "> @href", "http://test.host/user/example"
361           assert_dom "> @rel", "nofollow noopener noreferrer"
362         end
363       end
364     end
365   end
366
367   def test_text_to_html_linkify_recognize_path_no_partial_match
368     with_settings(:linkify => { :detection_rules => [{ :patterns => ["@(?<username>\\w+)"], :path_template => "user/\\k<username>" }] }) do
369       r = RichText.new("text", "foo example@example.com bar")
370       assert_html r do
371         assert_select "a", 0
372       end
373     end
374   end
375
376   def test_text_to_html_linkify_recognize_wiki_path
377     with_settings(:linkify => { :detection_rules => [{ :patterns => ["(?<key>[^\"?#<>/\\s]+)=(?<value>[^\"?#<>\\s]+)"], :path_template => "Tag:\\k<key>=\\k<value>", :host => "http://example.wiki" }] }) do
378       r = RichText.new("text", "foo surface=metal bar")
379       assert_html r do
380         assert_dom "a", :count => 1, :text => "http://example.wiki/Tag:surface=metal" do
381           assert_dom "> @href", "http://example.wiki/Tag:surface=metal"
382           assert_dom "> @rel", "nofollow noopener noreferrer"
383         end
384       end
385     end
386     with_settings(:linkify => { :detection_rules => [{ :patterns => ["(?<key>[^\"?#<>/\\s]+)=\\*?"], :path_template => "Key:\\k<key>", :host => "http://example.wiki" }] }) do
387       r = RichText.new("text", "foo surface=* bar")
388       assert_html r do
389         assert_dom "a", :count => 1, :text => "http://example.wiki/Key:surface" do
390           assert_dom "> @href", "http://example.wiki/Key:surface"
391           assert_dom "> @rel", "nofollow noopener noreferrer"
392         end
393       end
394     end
395   end
396
397   def test_text_to_html_linkify_openstreetmap_links
398     with_settings(:server_url => "www.openstreetmap.org", :server_protocol => "https") do
399       cases = {
400         "https://www.openstreetmap.org/note/4655490" =>
401           ["note/4655490", "https://www.openstreetmap.org/note/4655490"],
402
403         "https://www.openstreetmap.org/changeset/163353772" =>
404           ["changeset/163353772", "https://www.openstreetmap.org/changeset/163353772"],
405
406         "https://www.openstreetmap.org/way/1249366504" =>
407           ["way/1249366504", "https://www.openstreetmap.org/way/1249366504"],
408
409         "https://www.openstreetmap.org/way/1249366504/history" =>
410           ["way/1249366504/history", "https://www.openstreetmap.org/way/1249366504/history"],
411
412         "https://www.openstreetmap.org/way/1249366504/history/2" =>
413           ["way/1249366504/history/2", "https://www.openstreetmap.org/way/1249366504/history/2"],
414
415         "https://www.openstreetmap.org/node/12639964186" =>
416           ["node/12639964186", "https://www.openstreetmap.org/node/12639964186"],
417
418         "https://www.openstreetmap.org/relation/7876483" =>
419           ["relation/7876483", "https://www.openstreetmap.org/relation/7876483"],
420
421         "https://www.openstreetmap.org/user/aharvey" =>
422           ["@aharvey", "https://www.openstreetmap.org/user/aharvey"],
423
424         "https://wiki.openstreetmap.org/wiki/Key:boundary" =>
425           ["boundary=*", "https://wiki.openstreetmap.org/wiki/Key:boundary"],
426
427         "https://wiki.openstreetmap.org/wiki/Tag:boundary=place" =>
428           ["boundary=place", "https://wiki.openstreetmap.org/wiki/Tag:boundary=place"],
429
430         "boundary=*" =>
431           ["boundary=*", "https://wiki.openstreetmap.org/wiki/Key:boundary"],
432
433         "boundary=place" =>
434           ["boundary=place", "https://wiki.openstreetmap.org/wiki/Tag:boundary=place"],
435
436         "node/12639964186" =>
437           ["node/12639964186", "https://www.openstreetmap.org/node/12639964186"],
438
439         "node 12639964186" =>
440           ["node/12639964186", "https://www.openstreetmap.org/node/12639964186"],
441
442         "n12639964186" =>
443           ["node/12639964186", "https://www.openstreetmap.org/node/12639964186"],
444
445         "way/1249366504" =>
446           ["way/1249366504", "https://www.openstreetmap.org/way/1249366504"],
447
448         "way 1249366504" =>
449           ["way/1249366504", "https://www.openstreetmap.org/way/1249366504"],
450
451         "w1249366504" =>
452           ["way/1249366504", "https://www.openstreetmap.org/way/1249366504"],
453
454         "relation/7876483" =>
455           ["relation/7876483", "https://www.openstreetmap.org/relation/7876483"],
456
457         "relation 7876483" =>
458           ["relation/7876483", "https://www.openstreetmap.org/relation/7876483"],
459
460         "r7876483" =>
461           ["relation/7876483", "https://www.openstreetmap.org/relation/7876483"],
462
463         "changeset/163353772" =>
464           ["changeset/163353772", "https://www.openstreetmap.org/changeset/163353772"],
465
466         "changeset 163353772" =>
467           ["changeset/163353772", "https://www.openstreetmap.org/changeset/163353772"],
468
469         "note/4655490" =>
470           ["note/4655490", "https://www.openstreetmap.org/note/4655490"],
471
472         "note 4655490" =>
473           ["note/4655490", "https://www.openstreetmap.org/note/4655490"]
474       }
475
476       cases.each do |input, (expected_text, expected_href)|
477         r = RichText.new("text", input)
478         assert_html r do
479           assert_dom "a", :count => 1, :text => expected_text do
480             assert_dom "> @href", expected_href
481           end
482         end
483       end
484     end
485   end
486
487   def test_text_to_html_linkify_no_year_misinterpretation
488     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.")
489     assert_html r do
490       assert_select "a", 0
491     end
492   end
493
494   def test_text_to_html_email
495     r = RichText.new("text", "foo example@example.com bar")
496     assert_html r do
497       assert_select "a", 0
498     end
499   end
500
501   def test_text_to_html_escape
502     r = RichText.new("text", "foo < bar & baz > qux")
503     assert_html r do
504       assert_select "p", "foo < bar & baz > qux"
505     end
506   end
507
508   def test_text_to_text
509     r = RichText.new("text", "foo http://example.com/ bar")
510     assert_equal "foo http://example.com/ bar", r.to_text
511   end
512
513   def test_text_no_opengraph_properties
514     r = RichText.new("text", "foo https://example.com/ bar")
515     assert_nil r.image
516     assert_nil r.image_alt
517     assert_nil r.description
518   end
519
520   def test_html_no_opengraph_properties
521     r = RichText.new("html", "foo <a href='https://example.com/'>bar</a> baz")
522     assert_nil r.image
523     assert_nil r.image_alt
524     assert_nil r.description
525   end
526
527   def test_markdown_no_image
528     r = RichText.new("markdown", "foo [bar](https://example.com/) baz")
529     assert_nil r.image
530     assert_nil r.image_alt
531   end
532
533   def test_markdown_image
534     r = RichText.new("markdown", "foo ![bar](https://example.com/image.jpg) baz")
535     assert_equal "https://example.com/image.jpg", r.image
536     assert_equal "bar", r.image_alt
537   end
538
539   def test_markdown_first_image
540     r = RichText.new("markdown", "foo ![bar1](https://example.com/image1.jpg) baz\nfoo ![bar2](https://example.com/image2.jpg) baz")
541     assert_equal "https://example.com/image1.jpg", r.image
542     assert_equal "bar1", r.image_alt
543   end
544
545   def test_markdown_image_with_empty_src
546     r = RichText.new("markdown", "![invalid]()")
547     assert_nil r.image
548     assert_nil r.image_alt
549   end
550
551   def test_markdown_skip_image_with_empty_src
552     r = RichText.new("markdown", "![invalid]() ![valid](https://example.com/valid.gif)")
553     assert_equal "https://example.com/valid.gif", r.image
554     assert_equal "valid", r.image_alt
555   end
556
557   def test_markdown_html_image
558     r = RichText.new("markdown", "<img src='https://example.com/img_element.png' alt='alt text here'>")
559     assert_equal "https://example.com/img_element.png", r.image
560     assert_equal "alt text here", r.image_alt
561   end
562
563   def test_markdown_html_image_without_alt
564     r = RichText.new("markdown", "<img src='https://example.com/img_element.png'>")
565     assert_equal "https://example.com/img_element.png", r.image
566     assert_nil r.image_alt
567   end
568
569   def test_markdown_html_image_with_empty_src
570     r = RichText.new("markdown", "<img src='' alt='forgot src'>")
571     assert_nil r.image
572     assert_nil r.image_alt
573   end
574
575   def test_markdown_skip_html_image_with_empty_src
576     r = RichText.new("markdown", "<img src='' alt='forgot src'> <img src='https://example.com/next_img_element.png' alt='have src'>")
577     assert_equal "https://example.com/next_img_element.png", r.image
578     assert_equal "have src", r.image_alt
579   end
580
581   def test_markdown_html_image_without_src
582     r = RichText.new("markdown", "<img alt='totally forgot src'>")
583     assert_nil r.image
584     assert_nil r.image_alt
585   end
586
587   def test_markdown_skip_html_image_without_src
588     r = RichText.new("markdown", "<img alt='totally forgot src'> <img src='https://example.com/next_img_element.png' alt='have src'>")
589     assert_equal "https://example.com/next_img_element.png", r.image
590     assert_equal "have src", r.image_alt
591   end
592
593   def test_markdown_no_description
594     r = RichText.new("markdown", "#Nope")
595     assert_nil r.description
596   end
597
598   def test_markdown_description
599     r = RichText.new("markdown", "This is an article about something.")
600     assert_equal "This is an article about something.", r.description
601   end
602
603   def test_markdown_description_after_heading
604     r = RichText.new("markdown", "#Heading\n\nHere starts the text.")
605     assert_equal "Here starts the text.", r.description
606   end
607
608   def test_markdown_description_after_image
609     r = RichText.new("markdown", "![bar](https://example.com/image.jpg)\n\nThis is below the image.")
610     assert_equal "This is below the image.", r.description
611   end
612
613   def test_markdown_description_only_first_paragraph
614     r = RichText.new("markdown", "This thing.\n\nMaybe also that thing.")
615     assert_equal "This thing.", r.description
616   end
617
618   def test_markdown_description_elements
619     r = RichText.new("markdown", "*Something* **important** [here](https://example.com/).")
620     assert_equal "Something important here.", r.description
621   end
622
623   def test_markdown_html_description
624     r = RichText.new("markdown", "<p>Can use HTML tags.</p>")
625     assert_equal "Can use HTML tags.", r.description
626   end
627
628   def test_markdown_description_max_length
629     m = RichText::DESCRIPTION_MAX_LENGTH
630     o = 3 # "...".length
631
632     r = RichText.new("markdown", "x" * m)
633     assert_equal "x" * m, r.description
634
635     r = RichText.new("markdown", "y" * (m + 1))
636     assert_equal "#{'y' * (m - o)}...", r.description
637
638     r = RichText.new("markdown", "*zzzzzzzzz*z" * ((m + 1) / 10.0).ceil)
639     assert_equal "#{'z' * (m - o)}...", r.description
640   end
641
642   def test_markdown_description_word_break_threshold_length
643     m = RichText::DESCRIPTION_MAX_LENGTH
644     t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
645     o = 3 # "...".length
646
647     r = RichText.new("markdown", "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 1)}")
648     assert_equal "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 1)}", r.description
649
650     r = RichText.new("markdown", "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1))}")
651     assert_equal "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 4)}...", r.description
652
653     r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o) - 1)}")
654     assert_equal "#{'x' * (t - o)} #{'y' * (m - (t - o) - 1)}", r.description
655
656     r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o))}")
657     assert_equal "#{'x' * (t - o)}...", r.description
658   end
659
660   def test_markdown_description_word_break_multiple_spaces
661     m = RichText::DESCRIPTION_MAX_LENGTH
662     t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
663     o = 3 # "...".length
664
665     r = RichText.new("markdown", "#{'x' * (t - o)}  #{'y' * (m - (t - o - 1))}")
666     assert_equal "#{'x' * (t - o)}...", r.description
667   end
668
669   private
670
671   def assert_html(richtext, &block)
672     html = richtext.to_html
673     assert_predicate html, :html_safe?
674     root = Nokogiri::HTML::DocumentFragment.parse(html)
675     assert_select root, "*" do
676       yield block
677     end
678   end
679 end