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