better_html 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +30 -0
  4. data/lib/better_html.rb +53 -0
  5. data/lib/better_html/better_erb.rb +68 -0
  6. data/lib/better_html/better_erb/erubi_implementation.rb +50 -0
  7. data/lib/better_html/better_erb/erubis_implementation.rb +44 -0
  8. data/lib/better_html/better_erb/runtime_checks.rb +161 -0
  9. data/lib/better_html/better_erb/validated_output_buffer.rb +166 -0
  10. data/lib/better_html/errors.rb +22 -0
  11. data/lib/better_html/helpers.rb +5 -0
  12. data/lib/better_html/html_attributes.rb +26 -0
  13. data/lib/better_html/node_iterator.rb +144 -0
  14. data/lib/better_html/node_iterator/attribute.rb +34 -0
  15. data/lib/better_html/node_iterator/base.rb +27 -0
  16. data/lib/better_html/node_iterator/cdata.rb +8 -0
  17. data/lib/better_html/node_iterator/comment.rb +8 -0
  18. data/lib/better_html/node_iterator/content_node.rb +13 -0
  19. data/lib/better_html/node_iterator/element.rb +26 -0
  20. data/lib/better_html/node_iterator/html_erb.rb +78 -0
  21. data/lib/better_html/node_iterator/html_lodash.rb +101 -0
  22. data/lib/better_html/node_iterator/javascript_erb.rb +60 -0
  23. data/lib/better_html/node_iterator/location.rb +14 -0
  24. data/lib/better_html/node_iterator/text.rb +8 -0
  25. data/lib/better_html/node_iterator/token.rb +8 -0
  26. data/lib/better_html/railtie.rb +7 -0
  27. data/lib/better_html/test_helper/ruby_expr.rb +89 -0
  28. data/lib/better_html/test_helper/safe_erb_tester.rb +202 -0
  29. data/lib/better_html/test_helper/safe_lodash_tester.rb +121 -0
  30. data/lib/better_html/test_helper/safety_tester_base.rb +34 -0
  31. data/lib/better_html/tree.rb +113 -0
  32. data/lib/better_html/version.rb +3 -0
  33. data/lib/tasks/better_html_tasks.rake +4 -0
  34. data/test/better_html/better_erb/implementation_test.rb +402 -0
  35. data/test/better_html/helpers_test.rb +49 -0
  36. data/test/better_html/node_iterator/html_lodash_test.rb +132 -0
  37. data/test/better_html/node_iterator_test.rb +221 -0
  38. data/test/better_html/test_helper/ruby_expr_test.rb +206 -0
  39. data/test/better_html/test_helper/safe_erb_tester_test.rb +358 -0
  40. data/test/better_html/test_helper/safe_lodash_tester_test.rb +80 -0
  41. data/test/better_html/tree_test.rb +110 -0
  42. data/test/dummy/README.rdoc +28 -0
  43. data/test/dummy/Rakefile +6 -0
  44. data/test/dummy/app/assets/javascripts/application.js +13 -0
  45. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  46. data/test/dummy/app/controllers/application_controller.rb +5 -0
  47. data/test/dummy/app/helpers/application_helper.rb +2 -0
  48. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  49. data/test/dummy/bin/bundle +3 -0
  50. data/test/dummy/bin/rails +4 -0
  51. data/test/dummy/bin/rake +4 -0
  52. data/test/dummy/bin/setup +29 -0
  53. data/test/dummy/config.ru +4 -0
  54. data/test/dummy/config/application.rb +26 -0
  55. data/test/dummy/config/boot.rb +5 -0
  56. data/test/dummy/config/database.yml +25 -0
  57. data/test/dummy/config/environment.rb +5 -0
  58. data/test/dummy/config/environments/development.rb +41 -0
  59. data/test/dummy/config/environments/production.rb +79 -0
  60. data/test/dummy/config/environments/test.rb +42 -0
  61. data/test/dummy/config/initializers/assets.rb +11 -0
  62. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  63. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  64. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  65. data/test/dummy/config/initializers/inflections.rb +16 -0
  66. data/test/dummy/config/initializers/mime_types.rb +4 -0
  67. data/test/dummy/config/initializers/session_store.rb +3 -0
  68. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  69. data/test/dummy/config/locales/en.yml +23 -0
  70. data/test/dummy/config/routes.rb +56 -0
  71. data/test/dummy/config/secrets.yml +22 -0
  72. data/test/dummy/public/404.html +67 -0
  73. data/test/dummy/public/422.html +67 -0
  74. data/test/dummy/public/500.html +66 -0
  75. data/test/dummy/public/favicon.ico +0 -0
  76. data/test/test_helper.rb +19 -0
  77. metadata +205 -0
@@ -0,0 +1,358 @@
1
+ require 'test_helper'
2
+ require 'better_html/test_helper/safe_erb_tester'
3
+
4
+ module BetterHtml
5
+ module TestHelper
6
+ class SafeErbTesterTest < ActiveSupport::TestCase
7
+ setup do
8
+ BetterHtml.config
9
+ .stubs(:javascript_safe_methods)
10
+ .returns(['j', 'escape_javascript', 'to_json'])
11
+ end
12
+
13
+ test "string without interpolation is safe" do
14
+ errors = parse(<<-EOF).errors
15
+ <a onclick="alert('<%= "something" %>')">
16
+ EOF
17
+
18
+ assert_equal 1, errors.size
19
+ assert_equal '<%= "something" %>', errors.first.token.text
20
+ assert_equal "erb interpolation in javascript attribute must call '(...).to_json'", errors.first.message
21
+ end
22
+
23
+ test "string with interpolation" do
24
+ errors = parse(<<-EOF).errors
25
+ <a onclick="<%= "hello \#{name}" %>">
26
+ EOF
27
+
28
+ assert_equal 1, errors.size
29
+ assert_equal '<%= "hello #{name}" %>', errors.first.token.text
30
+ assert_equal "erb interpolation in javascript attribute must call '(...).to_json'", errors.first.message
31
+ end
32
+
33
+ test "string with interpolation and ternary" do
34
+ errors = parse(<<-EOF).errors
35
+ <a onclick="<%= "hello \#{foo ? bar : baz}" if bla? %>">
36
+ EOF
37
+
38
+ assert_equal 2, errors.size
39
+
40
+ assert_equal '<%= "hello #{foo ? bar : baz}" if bla? %>', errors.first.token.text
41
+ assert_equal "erb interpolation in javascript attribute must call '(...).to_json'", errors.first.message
42
+
43
+ assert_equal '<%= "hello #{foo ? bar : baz}" if bla? %>', errors.first.token.text
44
+ assert_equal "erb interpolation in javascript attribute must call '(...).to_json'", errors.first.message
45
+ end
46
+
47
+ test "plain erb tag in html attribute" do
48
+ errors = parse(<<-EOF).errors
49
+ <a onclick="method(<%= unsafe %>)">
50
+ EOF
51
+
52
+ assert_equal 1, errors.size
53
+ assert_equal '<%= unsafe %>', errors.first.token.text
54
+ assert_equal "erb interpolation in javascript attribute must call '(...).to_json'", errors.first.message
55
+ end
56
+
57
+ test "to_json is safe in html attribute" do
58
+ errors = parse(<<-EOF).errors
59
+ <a onclick="method(<%= unsafe.to_json %>)">
60
+ EOF
61
+ assert_predicate errors, :empty?
62
+ end
63
+
64
+ test "ternary with safe javascript escaping" do
65
+ errors = parse(<<-EOF).errors
66
+ <a onclick="method(<%= foo ? bar.to_json : j(baz) %>)">
67
+ EOF
68
+ assert_predicate errors, :empty?
69
+ end
70
+
71
+ test "ternary with unsafe javascript escaping" do
72
+ errors = parse(<<-EOF).errors
73
+ <a onclick="method(<%= foo ? bar : j(baz) %>)">
74
+ EOF
75
+
76
+ assert_equal 1, errors.size
77
+ assert_equal '<%= foo ? bar : j(baz) %>', errors.first.token.text
78
+ assert_equal "erb interpolation in javascript attribute must call '(...).to_json'", errors.first.message
79
+ end
80
+
81
+ test "j is safe in html attribute" do
82
+ errors = parse(<<-EOF).errors
83
+ <a onclick="method('<%= j unsafe %>')">
84
+ EOF
85
+ assert_predicate errors, :empty?
86
+ end
87
+
88
+ test "j() is safe in html attribute" do
89
+ errors = parse(<<-EOF).errors
90
+ <a onclick="method('<%= j(unsafe) %>')">
91
+ EOF
92
+ assert_predicate errors, :empty?
93
+ end
94
+
95
+ test "escape_javascript is safe in html attribute" do
96
+ errors = parse(<<-EOF).errors
97
+ <a onclick="method(<%= escape_javascript unsafe %>)">
98
+ EOF
99
+ assert_predicate errors, :empty?
100
+ end
101
+
102
+ test "escape_javascript() is safe in html attribute" do
103
+ errors = parse(<<-EOF).errors
104
+ <a onclick="method(<%= escape_javascript(unsafe) %>)">
105
+ EOF
106
+ assert_predicate errors, :empty?
107
+ end
108
+
109
+ test "html_safe is never safe in html attribute, even non javascript attributes like href" do
110
+ errors = parse(<<-EOF).errors
111
+ <a href="<%= unsafe.html_safe %>">
112
+ EOF
113
+
114
+ assert_equal 1, errors.size
115
+ assert_equal '<%= unsafe.html_safe %>', errors.first.token.text
116
+ assert_equal "erb interpolation with '<%= (...).html_safe %>' inside html attribute is never safe", errors.first.message
117
+ end
118
+
119
+ test "html_safe is never safe in html attribute, even with to_json" do
120
+ errors = parse(<<-EOF).errors
121
+ <a onclick="method(<%= unsafe.to_json.html_safe %>)">
122
+ EOF
123
+
124
+ assert_equal 1, errors.size
125
+ assert_equal '<%= unsafe.to_json.html_safe %>', errors.first.token.text
126
+ assert_equal "erb interpolation with '<%= (...).html_safe %>' inside html attribute is never safe", errors.first.message
127
+ end
128
+
129
+ test "<%== is never safe in html attribute, even non javascript attributes like href" do
130
+ errors = parse(<<-EOF).errors
131
+ <a href="<%== unsafe %>">
132
+ EOF
133
+
134
+ assert_equal 1, errors.size
135
+ assert_equal '<%== unsafe %>', errors.first.token.text
136
+ assert_includes "erb interpolation with '<%==' inside html attribute is never safe", errors.first.message
137
+ end
138
+
139
+ test "<%== is never safe in html attribute, even with to_json" do
140
+ errors = parse(<<-EOF).errors
141
+ <a onclick="method(<%== unsafe.to_json %>)">
142
+ EOF
143
+
144
+ assert_equal 1, errors.size
145
+ assert_equal '<%== unsafe.to_json %>', errors.first.token.text
146
+ assert_includes "erb interpolation with '<%==' inside html attribute is never safe", errors.first.message
147
+ end
148
+
149
+ test "raw is never safe in html attribute, even non javascript attributes like href" do
150
+ errors = parse(<<-EOF).errors
151
+ <a href="<%= raw unsafe %>">
152
+ EOF
153
+
154
+ assert_equal 1, errors.size
155
+ assert_equal '<%= raw unsafe %>', errors.first.token.text
156
+ assert_equal "erb interpolation with '<%= raw(...) %>' inside html attribute is never safe", errors.first.message
157
+ end
158
+
159
+ test "raw is never safe in html attribute, even with to_json" do
160
+ errors = parse(<<-EOF).errors
161
+ <a onclick="method(<%= raw unsafe.to_json %>)">
162
+ EOF
163
+
164
+ assert_equal 1, errors.size
165
+ assert_equal '<%= raw unsafe.to_json %>', errors.first.token.text
166
+ assert_equal "erb interpolation with '<%= raw(...) %>' inside html attribute is never safe", errors.first.message
167
+ end
168
+
169
+ test "unsafe erb in <script> tag without type" do
170
+ errors = parse(<<-EOF).errors
171
+ <script>
172
+ if (a < 1) { <%= unsafe %> }
173
+ </script>
174
+ EOF
175
+
176
+ assert_equal 1, errors.size
177
+ assert_equal '<%= unsafe %>', errors.first.token.text
178
+ assert_equal "erb interpolation in javascript tag must call '(...).to_json'", errors.first.message
179
+ end
180
+
181
+ test "unsafe erb in javascript template" do
182
+ errors = parse(<<-JS, template_language: :javascript).errors
183
+ if (a < 1) { <%= unsafe %> }
184
+ JS
185
+
186
+ assert_equal 1, errors.size
187
+ assert_equal '<%= unsafe %>', errors.first.token.text
188
+ assert_equal "erb interpolation in javascript tag must call '(...).to_json'", errors.first.message
189
+ end
190
+
191
+ test "<script> tag without calls is unsafe" do
192
+ errors = parse(<<-EOF).errors
193
+ <script type="text/javascript">
194
+ if (a < 1) { <%= "unsafe" %> }
195
+ </script>
196
+ EOF
197
+
198
+ assert_equal 1, errors.size
199
+ assert_equal '<%= "unsafe" %>', errors.first.token.text
200
+ assert_equal "erb interpolation in javascript tag must call '(...).to_json'", errors.first.message
201
+ end
202
+
203
+ test "javascript template without calls is unsafe" do
204
+ errors = parse(<<-JS, template_language: :javascript).errors
205
+ if (a < 1) { <%= "unsafe" %> }
206
+ JS
207
+
208
+ assert_equal 1, errors.size
209
+ assert_equal '<%= "unsafe" %>', errors.first.token.text
210
+ assert_equal "erb interpolation in javascript tag must call '(...).to_json'", errors.first.message
211
+ end
212
+
213
+ test "unsafe erb in javascript_tag" do
214
+ errors = parse(<<-EOF).errors
215
+ <%= javascript_tag do %>
216
+ if (a < 1) { <%= unsafe %> }
217
+ <% end %>
218
+ EOF
219
+
220
+ assert_equal 1, errors.size
221
+ assert_equal '<%= javascript_tag do %>', errors.first.token.text
222
+ assert_includes "'javascript_tag do' syntax is deprecated; use inline <script> instead", errors.first.message
223
+ end
224
+
225
+ test "unsafe erb in <script> tag with text/javascript content type" do
226
+ errors = parse(<<-EOF).errors
227
+ <script type="text/javascript">
228
+ if (a < 1) { <%= unsafe %> }
229
+ </script>
230
+ EOF
231
+
232
+ assert_equal 1, errors.size
233
+ assert_equal '<%= unsafe %>', errors.first.token.text
234
+ assert_equal "erb interpolation in javascript tag must call '(...).to_json'", errors.first.message
235
+ end
236
+
237
+ test "<script> tag with non executable content type is ignored" do
238
+ errors = parse(<<-EOF).errors
239
+ <script type="text/html">
240
+ <a onclick="<%= unsafe %>">
241
+ </script>
242
+ EOF
243
+
244
+ assert_predicate errors, :empty?
245
+ end
246
+
247
+ test "statements not allowed in script tags" do
248
+ errors = parse(<<-EOF).errors
249
+ <script type="text/javascript">
250
+ <% if foo? %>
251
+ bla
252
+ <% end %>
253
+ </script>
254
+ EOF
255
+
256
+ assert_equal 1, errors.size
257
+ assert_equal "<% if foo? \n%>", errors.first.token.text
258
+ assert_equal "erb statement not allowed here; did you mean '<%=' ?", errors.first.message
259
+ end
260
+
261
+ test "statements not allowed in javascript template" do
262
+ errors = parse(<<-JS, template_language: :javascript).errors
263
+ <% if foo %>
264
+ bla
265
+ <% end %>
266
+ JS
267
+
268
+ assert_equal 1, errors.size
269
+ assert_equal "<% if foo \n%>", errors.first.token.text
270
+ assert_equal "erb statement not allowed here; did you mean '<%=' ?", errors.first.message
271
+ end
272
+
273
+ test "script tag without content" do
274
+ errors = parse(<<-EOF).errors
275
+ <script type="text/javascript"></script>
276
+ EOF
277
+
278
+ assert_predicate errors, :empty?
279
+ end
280
+
281
+ test "statement after script regression" do
282
+ errors = parse(<<-EOF).errors
283
+ <script type="text/javascript">
284
+ foo()
285
+ </script>
286
+ <% if condition? %>
287
+ EOF
288
+
289
+ assert_predicate errors, :empty?
290
+ end
291
+
292
+ test "<script> with to_json is safe" do
293
+ errors = parse(<<-EOF).errors
294
+ <script type="text/javascript">
295
+ <%= unsafe.to_json %>
296
+ </script>
297
+ EOF
298
+
299
+ assert_predicate errors, :empty?
300
+ end
301
+
302
+ test "javascript template with to_json is safe" do
303
+ errors = parse(<<-JS, template_language: :javascript).errors
304
+ <%= unsafe.to_json %>
305
+ JS
306
+
307
+ assert_predicate errors, :empty?
308
+ end
309
+
310
+ test "<script> with raw and to_json is safe" do
311
+ errors = parse(<<-EOF).errors
312
+ <script type="text/javascript">
313
+ <%= raw unsafe.to_json %>
314
+ </script>
315
+ EOF
316
+
317
+ assert_predicate errors, :empty?
318
+ end
319
+
320
+ test "javascript template with raw and to_json is safe" do
321
+ errors = parse(<<-JS, template_language: :javascript).errors
322
+ <%= raw unsafe.to_json %>
323
+ JS
324
+
325
+ assert_predicate errors, :empty?
326
+ end
327
+
328
+ test "end statements are allowed in script tags" do
329
+ errors = parse(<<-EOF).errors
330
+ <script type="text/template">
331
+ <%= ui_form do %>
332
+ <div></div>
333
+ <% end %>
334
+ </script>
335
+ EOF
336
+
337
+ assert_predicate errors, :empty?
338
+ end
339
+
340
+ test "statements are allowed in text/html tags" do
341
+ errors = parse(<<-EOF).errors
342
+ <script type="text/html">
343
+ <% if condition? %>
344
+ <div></div>
345
+ <% end %>
346
+ </script>
347
+ EOF
348
+
349
+ assert_predicate errors, :empty?
350
+ end
351
+
352
+ private
353
+ def parse(data, template_language: :html)
354
+ SafeErbTester::Tester.new(data, template_language: template_language)
355
+ end
356
+ end
357
+ end
358
+ end
@@ -0,0 +1,80 @@
1
+ require 'test_helper'
2
+ require 'better_html/test_helper/safe_lodash_tester'
3
+
4
+ module BetterHtml
5
+ module TestHelper
6
+ class SafeLodashTesterTest < ActiveSupport::TestCase
7
+ test "interpolate in attribute not allowed" do
8
+ errors = parse(<<-EOF).errors
9
+ <div class="[%! foo %]">
10
+ EOF
11
+
12
+ assert_equal 1, errors.size
13
+ assert_equal '[%! foo %]', errors.first.token.text
14
+ assert_equal "lodash interpolation with '[%!' inside html attribute is never safe", errors.first.message
15
+ end
16
+
17
+ test "escape in attribute is allowed" do
18
+ errors = parse(<<-EOF).errors
19
+ <div class="[%= foo %]">
20
+ EOF
21
+
22
+ assert_predicate errors, :empty?
23
+ end
24
+
25
+ test "escape in javascript attribute not allowed" do
26
+ errors = parse(<<-EOF).errors
27
+ <div onclick="[%= foo %]">
28
+ EOF
29
+
30
+ assert_equal 1, errors.size
31
+ assert_equal '[%= foo %]', errors.first.token.text
32
+ assert_equal "lodash interpolation in javascript attribute `onclick` must call `JSON.stringify(foo)`", errors.first.message
33
+ end
34
+
35
+ test "escape in javascript attribute with JSON.stringify is allowed" do
36
+ errors = parse(<<-EOF).errors
37
+ <div onclick="[%= JSON.stringify(foo) %]">
38
+ EOF
39
+
40
+ assert_predicate errors, :empty?
41
+ end
42
+
43
+ test "script tag is not allowed" do
44
+ errors = parse(<<-EOF).errors
45
+ <script type="text/javascript"></script>
46
+ EOF
47
+
48
+ assert_equal 1, errors.size
49
+ assert_equal 'script', errors.first.token.text
50
+ assert_equal "No script tags allowed nested in lodash templates", errors.first.message
51
+ end
52
+
53
+ test "statement not allowed in attribute name" do
54
+ errors = parse(<<-EOF).errors
55
+ <div class[% if (foo) %]="foo">
56
+ EOF
57
+
58
+ assert_equal 1, errors.size
59
+ assert_equal '[% if (foo) %]', errors.first.token.text
60
+ assert_equal "javascript statement not allowed here; did you mean '[%=' ?", errors.first.message
61
+ end
62
+
63
+ test "statement not allowed in attribute value" do
64
+ errors = parse(<<-EOF).errors
65
+ <div class="foo[% if (foo) %]">
66
+ EOF
67
+
68
+ assert_equal 1, errors.size
69
+ assert_equal '[% if (foo) %]', errors.first.token.text
70
+ assert_equal "javascript statement not allowed here; did you mean '[%=' ?", errors.first.message
71
+ end
72
+
73
+ private
74
+
75
+ def parse(data)
76
+ SafeLodashTester::Tester.new(data)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,110 @@
1
+ require 'test_helper'
2
+
3
+ module BetterHtml
4
+ class TreeTest < ActiveSupport::TestCase
5
+ test "simple node" do
6
+ tree = Tree.new("<div></div>")
7
+ assert_predicate tree.errors, :empty?
8
+
9
+ assert_equal 1, tree.root.size
10
+ assert_equal 'div', tree.root[0].name
11
+ assert_equal true, tree.root[0].closed?
12
+ assert_equal false, tree.root[0].self_closing?
13
+ end
14
+
15
+ test "simple self-closing node" do
16
+ tree = Tree.new("<meta />")
17
+ assert_predicate tree.errors, :empty?
18
+
19
+ assert_equal 1, tree.root.size
20
+ assert_equal 'meta', tree.root[0].name
21
+ assert_equal true, tree.root[0].closed?
22
+ assert_equal true, tree.root[0].self_closing?
23
+ end
24
+
25
+ test "mismatched closing tag" do
26
+ tree = Tree.new("<div></p></div>")
27
+ assert_equal 1, tree.errors.size
28
+ assert_equal "mismatched </p> in <div> element", tree.errors[0].message
29
+
30
+ assert_equal 1, tree.root.size
31
+ assert_equal 'div', tree.root[0].name
32
+ assert_equal true, tree.root[0].closed?
33
+ assert_equal false, tree.root[0].self_closing?
34
+ end
35
+
36
+ test "mismatched closing tag at root" do
37
+ tree = Tree.new("</p>")
38
+ assert_equal 1, tree.errors.size
39
+ assert_equal "mismatched </p> at root of tree", tree.errors[0].message
40
+
41
+ assert_predicate tree.root, :empty?
42
+ end
43
+
44
+ test "node with text content" do
45
+ tree = Tree.new("<div>text</div>")
46
+ assert_predicate tree.errors, :empty?
47
+
48
+ assert_equal 1, tree.root.size
49
+ div = tree.root[0]
50
+ assert_equal 1, div.size
51
+ assert_equal 'text', div[0].content
52
+ end
53
+
54
+ test "extract erb from text node" do
55
+ tree = Tree.new("<div>before<%= foo %>after</div>")
56
+ assert_predicate tree.errors, :empty?
57
+
58
+ assert_equal 1, tree.root.size
59
+ div = tree.root[0]
60
+ assert_equal 1, div.size
61
+ text = div[0]
62
+ assert_equal 3, text.content_parts.size
63
+ assert_equal 'before', text.content_parts[0].text
64
+ assert_equal '<%= foo %>', text.content_parts[1].text
65
+ assert_equal ' foo ', text.content_parts[1].code
66
+ assert_equal 'after', text.content_parts[2].text
67
+ end
68
+
69
+ test "closing tag for void element" do
70
+ tree = Tree.new("<br></br>")
71
+ assert_equal 1, tree.errors.size
72
+ assert_equal "end of tag for void element: </br>", tree.errors[0].message
73
+
74
+ assert_equal 1, tree.root.size
75
+ assert_equal 'br', tree.root[0].name
76
+ assert_equal true, tree.root[0].closed?
77
+ assert_equal false, tree.root[0].self_closing?
78
+ assert_equal true, tree.root[0].void?
79
+ end
80
+
81
+ test "properly self-closed void element" do
82
+ tree = Tree.new("<br/>")
83
+ assert_predicate tree.errors, :empty?
84
+
85
+ assert_equal 1, tree.root.size
86
+ assert_equal 'br', tree.root[0].name
87
+ assert_equal true, tree.root[0].closed?
88
+ assert_equal true, tree.root[0].self_closing?
89
+ assert_equal true, tree.root[0].void?
90
+ end
91
+
92
+ test "void elements are nested properly" do
93
+ tree = Tree.new("<div><hr>test</hr></div>")
94
+ assert_equal 1, tree.errors.size
95
+ assert_equal "end of tag for void element: </hr>", tree.errors[0].message
96
+
97
+ assert_equal 1, tree.root.size
98
+ div = tree.root[0]
99
+ assert_equal 2, div.size
100
+ assert_equal true, div[0].element?
101
+ assert_equal true, div[1].text?
102
+ end
103
+
104
+ test "parser errors are bubbled up" do
105
+ tree = Tree.new("<>")
106
+ assert_equal 1, tree.errors.size
107
+ assert_equal "expected '/' or tag name", tree.errors[0].message
108
+ end
109
+ end
110
+ end