]> git.openstreetmap.org Git - rails.git/blob - test/lib/rich_text_test.rb
Merge remote-tracking branch 'upstream/pull/6798'
[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         "@aharvey" =>
432           ["@aharvey", "https://www.openstreetmap.org/user/aharvey"],
433
434         "node/12639964186" =>
435           ["node/12639964186", "https://www.openstreetmap.org/node/12639964186"],
436
437         "node 12639964186" =>
438           ["node/12639964186", "https://www.openstreetmap.org/node/12639964186"],
439
440         "n12639964186" =>
441           ["node/12639964186", "https://www.openstreetmap.org/node/12639964186"],
442
443         "way/1249366504" =>
444           ["way/1249366504", "https://www.openstreetmap.org/way/1249366504"],
445
446         "way 1249366504" =>
447           ["way/1249366504", "https://www.openstreetmap.org/way/1249366504"],
448
449         "w1249366504" =>
450           ["way/1249366504", "https://www.openstreetmap.org/way/1249366504"],
451
452         "relation/7876483" =>
453           ["relation/7876483", "https://www.openstreetmap.org/relation/7876483"],
454
455         "relation 7876483" =>
456           ["relation/7876483", "https://www.openstreetmap.org/relation/7876483"],
457
458         "r7876483" =>
459           ["relation/7876483", "https://www.openstreetmap.org/relation/7876483"],
460
461         "changeset/163353772" =>
462           ["changeset/163353772", "https://www.openstreetmap.org/changeset/163353772"],
463
464         "changeset 163353772" =>
465           ["changeset/163353772", "https://www.openstreetmap.org/changeset/163353772"],
466
467         "note/4655490" =>
468           ["note/4655490", "https://www.openstreetmap.org/note/4655490"],
469
470         "note 4655490" =>
471           ["note/4655490", "https://www.openstreetmap.org/note/4655490"]
472       }
473
474       cases.each do |input, (expected_text, expected_href)|
475         r = RichText.new("text", input)
476         assert_html r do
477           assert_dom "a[href='#{expected_href}']", :count => 1, :text => expected_text
478         end
479       end
480     end
481   end
482
483   def test_text_to_html_linkify_no_year_misinterpretation
484     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.")
485     assert_html r do
486       assert_select "a", 0
487     end
488   end
489
490   def test_deactivated_linkify_expansion_in_markdown
491     t0 = "foo `surface=metal` bar"
492
493     r1 = RichText.new("markdown", t0)
494     t1 = Nokogiri::HTML.fragment(r1.to_html).text
495
496     assert_equal t0.delete("`"), t1.strip
497   end
498
499   def test_text_to_html_linkify_trims_punctuation
500     r = RichText.new("text", "foo `surface=metal) bar inscription=🙂 baz")
501     assert_html r do
502       assert_dom "a", :count => 1
503       assert_dom "a[href$='Tag:surface=metal']", :text => "surface=metal"
504     end
505   end
506
507   def test_text_to_html_linkify_recognizes_non_standard_wiki_pages
508     r_paren = RichText.new("text", "foo source=Isle_of_Man_Government_1:25000_map_(2007) bar")
509     assert_html r_paren do
510       assert_dom "a[href*='Tag:source']", :text => "source=Isle_of_Man_Government_1:25000_map_(2007)"
511     end
512     r_latin_ext = RichText.new("text", "foo cuisine=açaí bar")
513     assert_html r_latin_ext do
514       assert_dom "a[href*='Tag:cuisine']", :text => "cuisine=açaí"
515     end
516     r_cyrillic = RichText.new("text", "foo name=Продукты bar")
517     assert_html r_cyrillic do
518       assert_dom "a[href*='Tag:name']", :text => "name=Продукты"
519     end
520     r_cjk = RichText.new("text", "foo shop=園芸店 bar")
521     assert_html r_cjk do
522       assert_dom "a[href*='Tag:shop']", :text => "shop=園芸店"
523     end
524   end
525
526   def test_text_to_html_email
527     r = RichText.new("text", "foo example@example.com bar")
528     assert_html r do
529       assert_select "a", 0
530     end
531   end
532
533   def test_text_to_html_escape
534     r = RichText.new("text", "foo < bar & baz > qux")
535     assert_html r do
536       assert_select "p", "foo < bar & baz > qux"
537     end
538   end
539
540   def test_text_to_text
541     r = RichText.new("text", "foo http://example.com/ bar")
542     assert_equal "foo http://example.com/ bar", r.to_text
543   end
544
545   def test_text_no_opengraph_properties
546     r = RichText.new("text", "foo https://example.com/ bar")
547     assert_nil r.image
548     assert_nil r.image_alt
549     assert_nil r.description
550   end
551
552   def test_html_no_opengraph_properties
553     r = RichText.new("html", "foo <a href='https://example.com/'>bar</a> baz")
554     assert_nil r.image
555     assert_nil r.image_alt
556     assert_nil r.description
557   end
558
559   def test_markdown_no_image
560     r = RichText.new("markdown", "foo [bar](https://example.com/) baz")
561     assert_nil r.image
562     assert_nil r.image_alt
563   end
564
565   def test_markdown_image
566     r = RichText.new("markdown", "foo ![bar](https://example.com/image.jpg) baz")
567     assert_equal "https://example.com/image.jpg", r.image
568     assert_equal "bar", r.image_alt
569   end
570
571   def test_markdown_first_image
572     r = RichText.new("markdown", "foo ![bar1](https://example.com/image1.jpg) baz\nfoo ![bar2](https://example.com/image2.jpg) baz")
573     assert_equal "https://example.com/image1.jpg", r.image
574     assert_equal "bar1", r.image_alt
575   end
576
577   def test_markdown_image_with_empty_src
578     r = RichText.new("markdown", "![invalid]()")
579     assert_nil r.image
580     assert_nil r.image_alt
581   end
582
583   def test_markdown_skip_image_with_empty_src
584     r = RichText.new("markdown", "![invalid]() ![valid](https://example.com/valid.gif)")
585     assert_equal "https://example.com/valid.gif", r.image
586     assert_equal "valid", r.image_alt
587   end
588
589   def test_markdown_html_image
590     r = RichText.new("markdown", "<img src='https://example.com/img_element.png' alt='alt text here'>")
591     assert_equal "https://example.com/img_element.png", r.image
592     assert_equal "alt text here", r.image_alt
593   end
594
595   def test_markdown_html_image_without_alt
596     r = RichText.new("markdown", "<img src='https://example.com/img_element.png'>")
597     assert_equal "https://example.com/img_element.png", r.image
598     assert_nil r.image_alt
599   end
600
601   def test_markdown_html_image_with_empty_src
602     r = RichText.new("markdown", "<img src='' alt='forgot src'>")
603     assert_nil r.image
604     assert_nil r.image_alt
605   end
606
607   def test_markdown_skip_html_image_with_empty_src
608     r = RichText.new("markdown", "<img src='' alt='forgot src'> <img src='https://example.com/next_img_element.png' alt='have src'>")
609     assert_equal "https://example.com/next_img_element.png", r.image
610     assert_equal "have src", r.image_alt
611   end
612
613   def test_markdown_html_image_without_src
614     r = RichText.new("markdown", "<img alt='totally forgot src'>")
615     assert_nil r.image
616     assert_nil r.image_alt
617   end
618
619   def test_markdown_skip_html_image_without_src
620     r = RichText.new("markdown", "<img alt='totally forgot src'> <img src='https://example.com/next_img_element.png' alt='have src'>")
621     assert_equal "https://example.com/next_img_element.png", r.image
622     assert_equal "have src", r.image_alt
623   end
624
625   def test_markdown_no_description
626     r = RichText.new("markdown", "#Nope")
627     assert_nil r.description
628   end
629
630   def test_markdown_description
631     r = RichText.new("markdown", "This is an article about something.")
632     assert_equal "This is an article about something.", r.description
633   end
634
635   def test_markdown_description_after_heading
636     r = RichText.new("markdown", "#Heading\n\nHere starts the text.")
637     assert_equal "Here starts the text.", r.description
638   end
639
640   def test_markdown_description_after_image
641     r = RichText.new("markdown", "![bar](https://example.com/image.jpg)\n\nThis is below the image.")
642     assert_equal "This is below the image.", r.description
643   end
644
645   def test_markdown_description_only_first_paragraph
646     r = RichText.new("markdown", "This thing.\n\nMaybe also that thing.")
647     assert_equal "This thing.", r.description
648   end
649
650   def test_markdown_description_elements
651     r = RichText.new("markdown", "*Something* **important** [here](https://example.com/).")
652     assert_equal "Something important here.", r.description
653   end
654
655   def test_markdown_html_description
656     r = RichText.new("markdown", "<p>Can use HTML tags.</p>")
657     assert_equal "Can use HTML tags.", r.description
658   end
659
660   def test_markdown_description_max_length
661     m = RichText::DESCRIPTION_MAX_LENGTH
662     o = 3 # "...".length
663
664     r = RichText.new("markdown", "x" * m)
665     assert_equal "x" * m, r.description
666
667     r = RichText.new("markdown", "y" * (m + 1))
668     assert_equal "#{'y' * (m - o)}...", r.description
669
670     r = RichText.new("markdown", "*zzzzzzzzz*z" * ((m + 1) / 10.0).ceil)
671     assert_equal "#{'z' * (m - o)}...", r.description
672   end
673
674   def test_markdown_description_word_break_threshold_length
675     m = RichText::DESCRIPTION_MAX_LENGTH
676     t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
677     o = 3 # "...".length
678
679     r = RichText.new("markdown", "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 1)}")
680     assert_equal "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 1)}", r.description
681
682     r = RichText.new("markdown", "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1))}")
683     assert_equal "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 4)}...", r.description
684
685     r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o) - 1)}")
686     assert_equal "#{'x' * (t - o)} #{'y' * (m - (t - o) - 1)}", r.description
687
688     r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o))}")
689     assert_equal "#{'x' * (t - o)}...", r.description
690   end
691
692   def test_markdown_description_word_break_multiple_spaces
693     m = RichText::DESCRIPTION_MAX_LENGTH
694     t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
695     o = 3 # "...".length
696
697     r = RichText.new("markdown", "#{'x' * (t - o)}  #{'y' * (m - (t - o - 1))}")
698     assert_equal "#{'x' * (t - o)}...", r.description
699   end
700
701   private
702
703   def assert_html(richtext, &block)
704     html = richtext.to_html
705     assert_predicate html, :html_safe?
706     root = Nokogiri::HTML::DocumentFragment.parse(html)
707     assert_select root, "*" do
708       yield block
709     end
710   end
711 end