]> git.openstreetmap.org Git - rails.git/blob - test/lib/rich_text_test.rb
Fix copy-pasting breaking links 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_email
356     r = RichText.new("text", "foo example@example.com bar")
357     assert_html r do
358       assert_select "a", 0
359     end
360   end
361
362   def test_text_to_html_escape
363     r = RichText.new("text", "foo < bar & baz > qux")
364     assert_html r do
365       assert_select "p", "foo < bar & baz > qux"
366     end
367   end
368
369   def test_text_to_text
370     r = RichText.new("text", "foo http://example.com/ bar")
371     assert_equal "foo http://example.com/ bar", r.to_text
372   end
373
374   def test_text_no_opengraph_properties
375     r = RichText.new("text", "foo https://example.com/ bar")
376     assert_nil r.image
377     assert_nil r.image_alt
378     assert_nil r.description
379   end
380
381   def test_html_no_opengraph_properties
382     r = RichText.new("html", "foo <a href='https://example.com/'>bar</a> baz")
383     assert_nil r.image
384     assert_nil r.image_alt
385     assert_nil r.description
386   end
387
388   def test_markdown_no_image
389     r = RichText.new("markdown", "foo [bar](https://example.com/) baz")
390     assert_nil r.image
391     assert_nil r.image_alt
392   end
393
394   def test_markdown_image
395     r = RichText.new("markdown", "foo ![bar](https://example.com/image.jpg) baz")
396     assert_equal "https://example.com/image.jpg", r.image
397     assert_equal "bar", r.image_alt
398   end
399
400   def test_markdown_first_image
401     r = RichText.new("markdown", "foo ![bar1](https://example.com/image1.jpg) baz\nfoo ![bar2](https://example.com/image2.jpg) baz")
402     assert_equal "https://example.com/image1.jpg", r.image
403     assert_equal "bar1", r.image_alt
404   end
405
406   def test_markdown_image_with_empty_src
407     r = RichText.new("markdown", "![invalid]()")
408     assert_nil r.image
409     assert_nil r.image_alt
410   end
411
412   def test_markdown_skip_image_with_empty_src
413     r = RichText.new("markdown", "![invalid]() ![valid](https://example.com/valid.gif)")
414     assert_equal "https://example.com/valid.gif", r.image
415     assert_equal "valid", r.image_alt
416   end
417
418   def test_markdown_html_image
419     r = RichText.new("markdown", "<img src='https://example.com/img_element.png' alt='alt text here'>")
420     assert_equal "https://example.com/img_element.png", r.image
421     assert_equal "alt text here", r.image_alt
422   end
423
424   def test_markdown_html_image_without_alt
425     r = RichText.new("markdown", "<img src='https://example.com/img_element.png'>")
426     assert_equal "https://example.com/img_element.png", r.image
427     assert_nil r.image_alt
428   end
429
430   def test_markdown_html_image_with_empty_src
431     r = RichText.new("markdown", "<img src='' alt='forgot src'>")
432     assert_nil r.image
433     assert_nil r.image_alt
434   end
435
436   def test_markdown_skip_html_image_with_empty_src
437     r = RichText.new("markdown", "<img src='' alt='forgot src'> <img src='https://example.com/next_img_element.png' alt='have src'>")
438     assert_equal "https://example.com/next_img_element.png", r.image
439     assert_equal "have src", r.image_alt
440   end
441
442   def test_markdown_html_image_without_src
443     r = RichText.new("markdown", "<img alt='totally forgot src'>")
444     assert_nil r.image
445     assert_nil r.image_alt
446   end
447
448   def test_markdown_skip_html_image_without_src
449     r = RichText.new("markdown", "<img alt='totally forgot src'> <img src='https://example.com/next_img_element.png' alt='have src'>")
450     assert_equal "https://example.com/next_img_element.png", r.image
451     assert_equal "have src", r.image_alt
452   end
453
454   def test_markdown_no_description
455     r = RichText.new("markdown", "#Nope")
456     assert_nil r.description
457   end
458
459   def test_markdown_description
460     r = RichText.new("markdown", "This is an article about something.")
461     assert_equal "This is an article about something.", r.description
462   end
463
464   def test_markdown_description_after_heading
465     r = RichText.new("markdown", "#Heading\n\nHere starts the text.")
466     assert_equal "Here starts the text.", r.description
467   end
468
469   def test_markdown_description_after_image
470     r = RichText.new("markdown", "![bar](https://example.com/image.jpg)\n\nThis is below the image.")
471     assert_equal "This is below the image.", r.description
472   end
473
474   def test_markdown_description_only_first_paragraph
475     r = RichText.new("markdown", "This thing.\n\nMaybe also that thing.")
476     assert_equal "This thing.", r.description
477   end
478
479   def test_markdown_description_elements
480     r = RichText.new("markdown", "*Something* **important** [here](https://example.com/).")
481     assert_equal "Something important here.", r.description
482   end
483
484   def test_markdown_html_description
485     r = RichText.new("markdown", "<p>Can use HTML tags.</p>")
486     assert_equal "Can use HTML tags.", r.description
487   end
488
489   def test_markdown_description_max_length
490     m = RichText::DESCRIPTION_MAX_LENGTH
491     o = 3 # "...".length
492
493     r = RichText.new("markdown", "x" * m)
494     assert_equal "x" * m, r.description
495
496     r = RichText.new("markdown", "y" * (m + 1))
497     assert_equal "#{'y' * (m - o)}...", r.description
498
499     r = RichText.new("markdown", "*zzzzzzzzz*z" * ((m + 1) / 10.0).ceil)
500     assert_equal "#{'z' * (m - o)}...", r.description
501   end
502
503   def test_markdown_description_word_break_threshold_length
504     m = RichText::DESCRIPTION_MAX_LENGTH
505     t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
506     o = 3 # "...".length
507
508     r = RichText.new("markdown", "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 1)}")
509     assert_equal "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 1)}", r.description
510
511     r = RichText.new("markdown", "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1))}")
512     assert_equal "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 4)}...", r.description
513
514     r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o) - 1)}")
515     assert_equal "#{'x' * (t - o)} #{'y' * (m - (t - o) - 1)}", r.description
516
517     r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o))}")
518     assert_equal "#{'x' * (t - o)}...", r.description
519   end
520
521   def test_markdown_description_word_break_multiple_spaces
522     m = RichText::DESCRIPTION_MAX_LENGTH
523     t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
524     o = 3 # "...".length
525
526     r = RichText.new("markdown", "#{'x' * (t - o)}  #{'y' * (m - (t - o - 1))}")
527     assert_equal "#{'x' * (t - o)}...", r.description
528   end
529
530   private
531
532   def assert_html(richtext, &block)
533     html = richtext.to_html
534     assert_predicate html, :html_safe?
535     root = Nokogiri::HTML::DocumentFragment.parse(html)
536     assert_select root, "*" do
537       yield block
538     end
539   end
540 end