actionview 7.1.5.2 → 8.1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -436
  3. data/README.rdoc +1 -1
  4. data/lib/action_view/base.rb +29 -11
  5. data/lib/action_view/buffers.rb +1 -1
  6. data/lib/action_view/cache_expiry.rb +9 -3
  7. data/lib/action_view/dependency_tracker/erb_tracker.rb +37 -28
  8. data/lib/action_view/dependency_tracker/ruby_tracker.rb +43 -0
  9. data/lib/action_view/dependency_tracker/wildcard_resolver.rb +32 -0
  10. data/lib/action_view/dependency_tracker.rb +8 -2
  11. data/lib/action_view/digestor.rb +6 -2
  12. data/lib/action_view/gem_version.rb +3 -3
  13. data/lib/action_view/helpers/asset_tag_helper.rb +39 -8
  14. data/lib/action_view/helpers/atom_feed_helper.rb +1 -3
  15. data/lib/action_view/helpers/cache_helper.rb +10 -2
  16. data/lib/action_view/helpers/capture_helper.rb +2 -2
  17. data/lib/action_view/helpers/controller_helper.rb +6 -2
  18. data/lib/action_view/helpers/csrf_helper.rb +1 -1
  19. data/lib/action_view/helpers/date_helper.rb +28 -4
  20. data/lib/action_view/helpers/form_helper.rb +277 -272
  21. data/lib/action_view/helpers/form_options_helper.rb +39 -35
  22. data/lib/action_view/helpers/form_tag_helper.rb +115 -72
  23. data/lib/action_view/helpers/javascript_helper.rb +5 -1
  24. data/lib/action_view/helpers/number_helper.rb +14 -0
  25. data/lib/action_view/helpers/output_safety_helper.rb +5 -6
  26. data/lib/action_view/helpers/rendering_helper.rb +160 -50
  27. data/lib/action_view/helpers/sanitize_helper.rb +6 -0
  28. data/lib/action_view/helpers/tag_helper.rb +225 -48
  29. data/lib/action_view/helpers/tags/base.rb +11 -9
  30. data/lib/action_view/helpers/tags/check_box.rb +9 -3
  31. data/lib/action_view/helpers/tags/collection_check_boxes.rb +4 -3
  32. data/lib/action_view/helpers/tags/collection_helpers.rb +2 -1
  33. data/lib/action_view/helpers/tags/datetime_field.rb +1 -1
  34. data/lib/action_view/helpers/tags/file_field.rb +7 -2
  35. data/lib/action_view/helpers/tags/hidden_field.rb +1 -1
  36. data/lib/action_view/helpers/tags/label.rb +3 -10
  37. data/lib/action_view/helpers/tags/radio_button.rb +1 -1
  38. data/lib/action_view/helpers/tags/select.rb +6 -1
  39. data/lib/action_view/helpers/tags/select_renderer.rb +6 -4
  40. data/lib/action_view/helpers/tags/text_area.rb +1 -1
  41. data/lib/action_view/helpers/tags/text_field.rb +1 -1
  42. data/lib/action_view/helpers/text_helper.rb +11 -4
  43. data/lib/action_view/helpers/translation_helper.rb +6 -1
  44. data/lib/action_view/helpers/url_helper.rb +42 -90
  45. data/lib/action_view/layouts.rb +9 -11
  46. data/lib/action_view/locale/en.yml +3 -0
  47. data/lib/action_view/log_subscriber.rb +9 -8
  48. data/lib/action_view/railtie.rb +12 -2
  49. data/lib/action_view/record_identifier.rb +22 -1
  50. data/lib/action_view/render_parser/prism_render_parser.rb +139 -0
  51. data/lib/action_view/{ripper_ast_parser.rb → render_parser/ripper_render_parser.rb} +162 -10
  52. data/lib/action_view/render_parser.rb +21 -169
  53. data/lib/action_view/renderer/abstract_renderer.rb +1 -1
  54. data/lib/action_view/renderer/partial_renderer.rb +18 -2
  55. data/lib/action_view/renderer/renderer.rb +32 -38
  56. data/lib/action_view/renderer/streaming_template_renderer.rb +8 -2
  57. data/lib/action_view/renderer/template_renderer.rb +3 -3
  58. data/lib/action_view/rendering.rb +6 -7
  59. data/lib/action_view/structured_event_subscriber.rb +97 -0
  60. data/lib/action_view/template/error.rb +18 -3
  61. data/lib/action_view/template/handlers/erb/erubi.rb +1 -1
  62. data/lib/action_view/template/handlers/erb.rb +77 -44
  63. data/lib/action_view/template/raw_file.rb +4 -0
  64. data/lib/action_view/template/renderable.rb +7 -1
  65. data/lib/action_view/template/resolver.rb +0 -3
  66. data/lib/action_view/template.rb +39 -12
  67. data/lib/action_view/test_case.rb +57 -62
  68. data/lib/action_view.rb +4 -0
  69. metadata +17 -14
  70. data/lib/action_view/dependency_tracker/ripper_tracker.rb +0 -59
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/structured_event_subscriber"
4
+
5
+ module ActionView
6
+ class StructuredEventSubscriber < ActiveSupport::StructuredEventSubscriber # :nodoc:
7
+ VIEWS_PATTERN = /^app\/views\//
8
+
9
+ def initialize
10
+ @root = nil
11
+ super
12
+ end
13
+
14
+ def render_template(event)
15
+ emit_debug_event("action_view.render_template",
16
+ identifier: from_rails_root(event.payload[:identifier]),
17
+ layout: from_rails_root(event.payload[:layout]),
18
+ duration_ms: event.duration.round(2),
19
+ gc_ms: event.gc_time.round(2),
20
+ )
21
+ end
22
+ debug_only :render_template
23
+
24
+ def render_partial(event)
25
+ emit_debug_event("action_view.render_partial",
26
+ identifier: from_rails_root(event.payload[:identifier]),
27
+ layout: from_rails_root(event.payload[:layout]),
28
+ duration_ms: event.duration.round(2),
29
+ gc_ms: event.gc_time.round(2),
30
+ cache_hit: event.payload[:cache_hit],
31
+ )
32
+ end
33
+ debug_only :render_partial
34
+
35
+ def render_layout(event)
36
+ emit_event("action_view.render_layout",
37
+ identifier: from_rails_root(event.payload[:identifier]),
38
+ duration_ms: event.duration.round(2),
39
+ gc_ms: event.gc_time.round(2),
40
+ )
41
+ end
42
+ debug_only :render_layout
43
+
44
+ def render_collection(event)
45
+ emit_debug_event("action_view.render_collection",
46
+ identifier: from_rails_root(event.payload[:identifier] || "templates"),
47
+ layout: from_rails_root(event.payload[:layout]),
48
+ duration_ms: event.duration.round(2),
49
+ gc_ms: event.gc_time.round(2),
50
+ cache_hits: event.payload[:cache_hits],
51
+ count: event.payload[:count],
52
+ )
53
+ end
54
+ debug_only :render_collection
55
+
56
+ module Utils # :nodoc:
57
+ private
58
+ def from_rails_root(string)
59
+ return unless string
60
+
61
+ string = string.sub("#{rails_root}/", "")
62
+ string.sub!(VIEWS_PATTERN, "")
63
+ string
64
+ end
65
+
66
+ def rails_root # :doc:
67
+ @root ||= Rails.try(:root)
68
+ end
69
+ end
70
+
71
+ include Utils
72
+
73
+ class Start # :nodoc:
74
+ include Utils
75
+
76
+ def start(name, id, payload)
77
+ ActiveSupport.event_reporter.debug("action_view.render_start",
78
+ is_layout: name == "render_layout.action_view",
79
+ identifier: from_rails_root(payload[:identifier]),
80
+ layout: from_rails_root(payload[:layout]),
81
+ )
82
+ end
83
+
84
+ def finish(name, id, payload)
85
+ end
86
+ end
87
+
88
+ def self.attach_to(*)
89
+ ActiveSupport::Notifications.subscribe("render_template.action_view", Start.new)
90
+ ActiveSupport::Notifications.subscribe("render_layout.action_view", Start.new)
91
+
92
+ super
93
+ end
94
+ end
95
+ end
96
+
97
+ ActionView::StructuredEventSubscriber.attach_to :action_view
@@ -27,6 +27,17 @@ module ActionView
27
27
  end
28
28
  end
29
29
 
30
+ class StrictLocalsError < ArgumentError # :nodoc:
31
+ def initialize(argument_error, template)
32
+ message = argument_error.message.
33
+ gsub("unknown keyword:", "unknown local:").
34
+ gsub("missing keyword:", "missing local:").
35
+ gsub("no keywords accepted", "no locals accepted").
36
+ concat(" for #{template.short_identifier}")
37
+ super(message)
38
+ end
39
+ end
40
+
30
41
  class MissingTemplate < ActionViewError # :nodoc:
31
42
  attr_reader :path, :paths, :prefixes, :partial
32
43
 
@@ -249,9 +260,13 @@ module ActionView
249
260
  end
250
261
 
251
262
  def message
252
- <<~MESSAGE
253
- Encountered a syntax error while rendering template: check #{@offending_code_string}
254
- MESSAGE
263
+ if template.is_a?(Template::Inline)
264
+ <<~MESSAGE
265
+ Encountered a syntax error while rendering template: check #{@offending_code_string}
266
+ MESSAGE
267
+ else
268
+ "Encountered a syntax error while rendering template located at: #{template.short_identifier}"
269
+ end
255
270
  end
256
271
 
257
272
  def annotated_source_code
@@ -18,7 +18,7 @@ module ActionView
18
18
  properties[:preamble] ||= ""
19
19
  properties[:postamble] ||= "#{properties[:bufvar]}"
20
20
 
21
- # Tell Eruby that whether template will be compiled with `frozen_string_literal: true`
21
+ # Tell Erubi whether the template will be compiled with `frozen_string_literal: true`
22
22
  properties[:freeze_template_literals] = !Template.frozen_string_literal
23
23
 
24
24
  properties[:escapefunc] = ""
@@ -40,18 +40,22 @@ module ActionView
40
40
 
41
41
  # Translate an error location returned by ErrorHighlight to the correct
42
42
  # source location inside the template.
43
- def translate_location(spot, backtrace_location, source)
44
- # Tokenize the source line
45
- tokens = ::ERB::Util.tokenize(source.lines[backtrace_location.lineno - 1])
46
- new_first_column = find_offset(spot[:snippet], tokens, spot[:first_column])
47
- lineno_delta = spot[:first_lineno] - backtrace_location.lineno
43
+ def translate_location(spot, _backtrace_location, source)
44
+ compiled = spot[:script_lines]
45
+ highlight = compiled[spot[:first_lineno] - 1]&.byteslice((spot[:first_column] - 1)...spot[:last_column])
46
+ return nil if highlight.blank?
47
+
48
+ source_lines = source.lines
49
+ lineno_delta = find_lineno_offset(compiled, source_lines, highlight, spot[:first_lineno])
50
+
51
+ tokens = ::ERB::Util.tokenize(source_lines[spot[:first_lineno] - lineno_delta - 1])
52
+ column_delta = find_offset(spot[:snippet], tokens, spot[:first_column])
53
+
48
54
  spot[:first_lineno] -= lineno_delta
49
55
  spot[:last_lineno] -= lineno_delta
50
-
51
- column_delta = spot[:first_column] - new_first_column
52
56
  spot[:first_column] -= column_delta
53
57
  spot[:last_column] -= column_delta
54
- spot[:script_lines] = source.lines
58
+ spot[:script_lines] = source_lines
55
59
 
56
60
  spot
57
61
  rescue NotImplementedError, LocationParsingError
@@ -82,7 +86,7 @@ module ActionView
82
86
  }
83
87
 
84
88
  if ActionView::Base.annotate_rendered_view_with_filenames && template.format == :html
85
- options[:preamble] = "@output_buffer.safe_append='<!-- BEGIN #{template.short_identifier} -->';"
89
+ options[:preamble] = "@output_buffer.safe_append='<!-- BEGIN #{template.short_identifier}\n-->';"
86
90
  options[:postamble] = "@output_buffer.safe_append='<!-- END #{template.short_identifier} -->';@output_buffer"
87
91
  end
88
92
 
@@ -105,51 +109,80 @@ module ActionView
105
109
  raise WrongEncodingError.new(string, string.encoding)
106
110
  end
107
111
 
112
+ # Return the offset between the error lineno and the source lineno.
113
+ # Searches in reverse from the backtrace lineno so we have a better
114
+ # chance of finding the correct line
115
+ #
116
+ # The compiled template is likely to be longer than the source.
117
+ # Use the difference between the compiled and source sizes to
118
+ # determine the earliest line that could contain the highlight.
119
+ def find_lineno_offset(compiled, source_lines, highlight, error_lineno)
120
+ first_index = error_lineno - 1 - compiled.size + source_lines.size
121
+ first_index = 0 if first_index < 0
122
+
123
+ last_index = error_lineno - 1
124
+ last_index = source_lines.size - 1 if last_index >= source_lines.size
125
+
126
+ last_index.downto(first_index) do |line_index|
127
+ next unless source_lines[line_index].include?(highlight)
128
+ return error_lineno - 1 - line_index
129
+ end
130
+
131
+ raise LocationParsingError, "Couldn't find code snippet"
132
+ end
133
+
134
+ # Find which token in the source template spans the byte range that
135
+ # contains the error_column, then return the offset compared to the
136
+ # original source template.
137
+ #
138
+ # Iterate consecutive pairs of CODE or TEXT tokens, requiring
139
+ # a match of the first token before matching either token.
140
+ #
141
+ # For example, if we want to find tokens A, B, C, we do the following:
142
+ # 1. Find a match for A: test error_column or advance scanner.
143
+ # 2. Find a match for B or A:
144
+ # a. If B: start over with next token set (B, C).
145
+ # b. If A: test error_column or advance scanner.
146
+ # c. Otherwise: Advance 1 byte
147
+ #
148
+ # Prioritize matching the next token over the current token once
149
+ # a match for the current token has been found. This is to prevent
150
+ # the current token from looping past the next token if they both
151
+ # match (i.e. if the current token is a single space character).
108
152
  def find_offset(compiled, source_tokens, error_column)
109
153
  compiled = StringScanner.new(compiled)
154
+ offset_source_tokens(source_tokens).each_cons(2) do |(name, str, offset), (_, next_str, _)|
155
+ matched_str = false
110
156
 
111
- passed_tokens = []
157
+ until compiled.eos?
158
+ if matched_str && next_str && compiled.match?(next_str)
159
+ break
160
+ elsif compiled.match?(str)
161
+ matched_str = true
112
162
 
113
- while tok = source_tokens.shift
114
- tok_name, str = *tok
115
- case tok_name
116
- when :TEXT
117
- loop do
118
- break if compiled.match?(str)
119
- compiled.getch
120
- end
121
- raise LocationParsingError unless compiled.scan(str)
122
- when :CODE
123
- if compiled.pos > error_column
124
- raise LocationParsingError, "We went too far"
125
- end
163
+ if name == :CODE && compiled.pos <= error_column && compiled.pos + str.bytesize >= error_column
164
+ return compiled.pos - offset
165
+ end
126
166
 
127
- if compiled.pos + str.bytesize >= error_column
128
- offset = error_column - compiled.pos
129
- return passed_tokens.map(&:last).join.bytesize + offset
167
+ compiled.pos += str.bytesize
130
168
  else
131
- unless compiled.scan(str)
132
- raise LocationParsingError, "Couldn't find code snippet"
133
- end
134
- end
135
- when :OPEN
136
- next_tok = source_tokens.first.last
137
- loop do
138
- break if compiled.match?(next_tok)
139
- compiled.getch
169
+ compiled.pos += 1
140
170
  end
141
- when :CLOSE
142
- next_tok = source_tokens.first.last
143
- loop do
144
- break if compiled.match?(next_tok)
145
- compiled.getch
146
- end
147
- else
148
- raise LocationParsingError, "Not implemented: #{tok.first}"
149
171
  end
172
+ end
173
+
174
+ raise LocationParsingError, "Couldn't find code snippet"
175
+ end
150
176
 
151
- passed_tokens << tok
177
+ def offset_source_tokens(source_tokens)
178
+ source_offset = 0
179
+ with_offset = source_tokens.filter_map do |name, str|
180
+ result = [:CODE, str, source_offset] if name == :CODE || name == :PLAIN
181
+ result = [:TEXT, str, source_offset] if name == :TEXT
182
+ source_offset += str.bytesize
183
+ result
152
184
  end
185
+ with_offset << [:EOS, nil, source_offset]
153
186
  end
154
187
  end
155
188
  end
@@ -20,6 +20,10 @@ module ActionView # :nodoc:
20
20
  def render(*args)
21
21
  ::File.read(@filename)
22
22
  end
23
+
24
+ def supports_streaming?
25
+ false
26
+ end
23
27
  end
24
28
  end
25
29
  end
@@ -14,10 +14,16 @@ module ActionView
14
14
 
15
15
  def render(context, *args)
16
16
  @renderable.render_in(context)
17
+ rescue NoMethodError
18
+ if !@renderable.respond_to?(:render_in)
19
+ raise ArgumentError, "'#{@renderable.inspect}' is not a renderable object. It must implement #render_in."
20
+ else
21
+ raise
22
+ end
17
23
  end
18
24
 
19
25
  def format
20
- @renderable.format
26
+ @renderable.try(:format)
21
27
  end
22
28
  end
23
29
  end
@@ -4,14 +4,11 @@ require "pathname"
4
4
  require "active_support/core_ext/class"
5
5
  require "active_support/core_ext/module/attribute_accessors"
6
6
  require "action_view/template"
7
- require "thread"
8
7
  require "concurrent/map"
9
8
 
10
9
  module ActionView
11
10
  # = Action View Resolver
12
11
  class Resolver
13
- include ActiveSupport::Deprecation::DeprecatedConstantAccessor
14
-
15
12
  class PathParser # :nodoc:
16
13
  ParsedPath = Struct.new(:path, :details)
17
14
 
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thread"
4
3
  require "delegate"
5
4
 
6
5
  module ActionView
@@ -8,7 +7,7 @@ module ActionView
8
7
  class Template
9
8
  extend ActiveSupport::Autoload
10
9
 
11
- STRICT_LOCALS_REGEX = /\#\s+locals:\s+\((.*)\)/
10
+ STRICT_LOCALS_REGEX = /\#\s+locals:\s+\((.*?)\)(?=\s*-?%>|\s*$)/m
12
11
 
13
12
  # === Encodings in ActionView::Template
14
13
  #
@@ -148,6 +147,20 @@ module ActionView
148
147
  # <p><%= alert %></p>
149
148
  # <% end %>
150
149
  #
150
+ # By default, templates will accept any <tt>locals</tt> as keyword arguments
151
+ # and make them available to <tt>local_assigns</tt>. To restrict what
152
+ # <tt>local_assigns</tt> a template will accept, add a <tt>locals:</tt> magic comment:
153
+ #
154
+ # <%# locals: (headline:, alerts: []) %>
155
+ #
156
+ # <h1><%= headline %></h1>
157
+ #
158
+ # <% alerts.each do |alert| %>
159
+ # <p><%= alert %></p>
160
+ # <% end %>
161
+ #
162
+ # Read more about strict locals in {Action View Overview}[https://guides.rubyonrails.org/action_view_overview.html#strict-locals]
163
+ # in the guides.
151
164
 
152
165
  eager_autoload do
153
166
  autoload :Error
@@ -216,11 +229,21 @@ module ActionView
216
229
  end
217
230
 
218
231
  def spot(location) # :nodoc:
219
- ast = RubyVM::AbstractSyntaxTree.parse(compiled_source, keep_script_lines: true)
220
232
  node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(location)
221
- node = find_node_by_id(ast, node_id)
233
+ found =
234
+ if RubyVM::InstructionSequence.compile("").to_a[4][:parser] == :prism
235
+ require "prism"
222
236
 
223
- ErrorHighlight.spot(node)
237
+ if Prism::VERSION >= "1.0.0"
238
+ result = Prism.parse(compiled_source).value
239
+ result.breadth_first_search { |node| node.node_id == node_id }
240
+ end
241
+ else
242
+ node = RubyVM::AbstractSyntaxTree.parse(compiled_source, keep_script_lines: true)
243
+ find_node_by_id(node, node_id)
244
+ end
245
+
246
+ ErrorHighlight.spot(found) if found
224
247
  end
225
248
 
226
249
  # Translate an error location returned by ErrorHighlight to the correct
@@ -333,7 +356,7 @@ module ActionView
333
356
 
334
357
  # This method is responsible for marking a template as having strict locals
335
358
  # which means the template can only accept the locals defined in a magic
336
- # comment. For example, if your template acceps the locals +title+ and
359
+ # comment. For example, if your template accepts the locals +title+ and
337
360
  # +comment_count+, add the following to your template file:
338
361
  #
339
362
  # <%# locals: (title: "Default title", comment_count: 0) %>
@@ -343,7 +366,7 @@ module ActionView
343
366
  def strict_locals!
344
367
  if @strict_locals == NONE
345
368
  self.source.sub!(STRICT_LOCALS_REGEX, "")
346
- @strict_locals = $1
369
+ @strict_locals = $1&.rstrip
347
370
 
348
371
  return if @strict_locals.nil? # Magic comment not found
349
372
 
@@ -425,9 +448,9 @@ module ActionView
425
448
  method_arguments =
426
449
  if set_strict_locals
427
450
  if set_strict_locals.include?("&")
428
- "output_buffer, #{set_strict_locals}"
451
+ "local_assigns, output_buffer, #{set_strict_locals}"
429
452
  else
430
- "output_buffer, #{set_strict_locals}, &_"
453
+ "local_assigns, output_buffer, #{set_strict_locals}, &_"
431
454
  end
432
455
  else
433
456
  "local_assigns, output_buffer, &_"
@@ -486,11 +509,12 @@ module ActionView
486
509
 
487
510
  return unless strict_locals?
488
511
 
489
- parameters = mod.instance_method(method_name).parameters - [[:req, :output_buffer]]
512
+ parameters = mod.instance_method(method_name).parameters
513
+ parameters -= [[:req, :local_assigns], [:req, :output_buffer]]
514
+
490
515
  # Check compiled method parameters to ensure that only kwargs
491
516
  # were provided as strict locals, preventing `locals: (foo, *foo)` etc
492
517
  # and allowing `locals: (foo:)`.
493
-
494
518
  non_kwarg_parameters = parameters.select do |parameter|
495
519
  ![:keyreq, :key, :keyrest, :nokey].include?(parameter[0])
496
520
  end
@@ -531,12 +555,15 @@ module ActionView
531
555
  end
532
556
  end
533
557
 
558
+ RUBY_RESERVED_KEYWORDS = ::ActiveSupport::Delegation::RUBY_RESERVED_KEYWORDS
559
+ private_constant :RUBY_RESERVED_KEYWORDS
560
+
534
561
  def locals_code
535
562
  return "" if strict_locals?
536
563
 
537
564
  # Only locals with valid variable names get set directly. Others will
538
565
  # still be available in local_assigns.
539
- locals = @locals - Module::RUBY_RESERVED_KEYWORDS
566
+ locals = @locals - RUBY_RESERVED_KEYWORDS
540
567
 
541
568
  locals = locals.grep(/\A(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)
542
569
 
@@ -60,7 +60,56 @@ module ActionView
60
60
  include ActiveSupport::Testing::ConstantLookup
61
61
 
62
62
  delegate :lookup_context, to: :controller
63
- attr_accessor :controller, :request, :output_buffer, :rendered
63
+ attr_accessor :controller, :request, :output_buffer
64
+
65
+ # Returns the content rendered by the last +render+ call.
66
+ #
67
+ # The returned object behaves like a string but also exposes a number of methods
68
+ # that allows you to parse the content string in formats registered using
69
+ # <tt>.register_parser</tt>.
70
+ #
71
+ # By default includes the following parsers:
72
+ #
73
+ # +.html+
74
+ #
75
+ # Parse the <tt>rendered</tt> content String into HTML. By default, this means
76
+ # a <tt>Nokogiri::XML::Node</tt>.
77
+ #
78
+ # test "renders HTML" do
79
+ # article = Article.create!(title: "Hello, world")
80
+ #
81
+ # render partial: "articles/article", locals: { article: article }
82
+ #
83
+ # assert_pattern { rendered.html.at("main h1") => { content: "Hello, world" } }
84
+ # end
85
+ #
86
+ # To parse the rendered content into a <tt>Capybara::Simple::Node</tt>,
87
+ # re-register an <tt>:html</tt> parser with a call to
88
+ # <tt>Capybara.string</tt>:
89
+ #
90
+ # register_parser :html, -> rendered { Capybara.string(rendered) }
91
+ #
92
+ # test "renders HTML" do
93
+ # article = Article.create!(title: "Hello, world")
94
+ #
95
+ # render partial: article
96
+ #
97
+ # rendered.html.assert_css "h1", text: "Hello, world"
98
+ # end
99
+ #
100
+ # +.json+
101
+ #
102
+ # Parse the <tt>rendered</tt> content String into JSON. By default, this means
103
+ # a <tt>ActiveSupport::HashWithIndifferentAccess</tt>.
104
+ #
105
+ # test "renders JSON" do
106
+ # article = Article.create!(title: "Hello, world")
107
+ #
108
+ # render formats: :json, partial: "articles/article", locals: { article: article }
109
+ #
110
+ # assert_pattern { rendered.json => { title: "Hello, world" } }
111
+ # end
112
+ attr_accessor :rendered
64
113
 
65
114
  module ClassMethods
66
115
  def inherited(descendant) # :nodoc:
@@ -171,10 +220,9 @@ module ActionView
171
220
  # Almost a duplicate from ActionController::Helpers
172
221
  methods.flatten.each do |method|
173
222
  _helpers_for_modification.module_eval <<~end_eval, __FILE__, __LINE__ + 1
174
- def #{method}(*args, &block) # def current_user(*args, &block)
175
- _test_case.send(:'#{method}', *args, &block) # _test_case.send(:'current_user', *args, &block)
176
- end # end
177
- ruby2_keywords(:'#{method}')
223
+ def #{method}(...) # def current_user(...)
224
+ _test_case.send(:'#{method}', ...) # _test_case.send(:'current_user', ...)
225
+ end # end
178
226
  end_eval
179
227
  end
180
228
  end
@@ -202,7 +250,7 @@ module ActionView
202
250
 
203
251
  setup :setup_with_controller
204
252
 
205
- register_parser :html, -> rendered { Rails::Dom::Testing.html_document.parse(rendered).root }
253
+ register_parser :html, -> rendered { Rails::Dom::Testing.html_document_fragment.parse(rendered) }
206
254
  register_parser :json, -> rendered { JSON.parse(rendered, object_class: ActiveSupport::HashWithIndifferentAccess) }
207
255
 
208
256
  ActiveSupport.run_load_hooks(:action_view_test_case, self)
@@ -244,57 +292,6 @@ module ActionView
244
292
  @_rendered_views ||= RenderedViewsCollection.new
245
293
  end
246
294
 
247
- ##
248
- # :method: rendered
249
- #
250
- # Returns the content rendered by the last +render+ call.
251
- #
252
- # The returned object behaves like a string but also exposes a number of methods
253
- # that allows you to parse the content string in formats registered using
254
- # <tt>.register_parser</tt>.
255
- #
256
- # By default includes the following parsers:
257
- #
258
- # +.html+
259
- #
260
- # Parse the <tt>rendered</tt> content String into HTML. By default, this means
261
- # a <tt>Nokogiri::XML::Node</tt>.
262
- #
263
- # test "renders HTML" do
264
- # article = Article.create!(title: "Hello, world")
265
- #
266
- # render partial: "articles/article", locals: { article: article }
267
- #
268
- # assert_pattern { rendered.html.at("main h1") => { content: "Hello, world" } }
269
- # end
270
- #
271
- # To parse the rendered content into a <tt>Capybara::Simple::Node</tt>,
272
- # re-register an <tt>:html</tt> parser with a call to
273
- # <tt>Capybara.string</tt>:
274
- #
275
- # register_parser :html, -> rendered { Capybara.string(rendered) }
276
- #
277
- # test "renders HTML" do
278
- # article = Article.create!(title: "Hello, world")
279
- #
280
- # render partial: article
281
- #
282
- # rendered.html.assert_css "h1", text: "Hello, world"
283
- # end
284
- #
285
- # +.json+
286
- #
287
- # Parse the <tt>rendered</tt> content String into JSON. By default, this means
288
- # a <tt>ActiveSupport::HashWithIndifferentAccess</tt>.
289
- #
290
- # test "renders JSON" do
291
- # article = Article.create!(title: "Hello, world")
292
- #
293
- # render formats: :json, partial: "articles/article", locals: { article: article }
294
- #
295
- # assert_pattern { rendered.json => { title: "Hello, world" } }
296
- # end
297
-
298
295
  def _routes
299
296
  @controller._routes if @controller.respond_to?(:_routes)
300
297
  end
@@ -302,7 +299,6 @@ module ActionView
302
299
  class RenderedViewContent < String # :nodoc:
303
300
  end
304
301
 
305
- # Need to experiment if this priority is the best one: rendered => output_buffer
306
302
  class RenderedViewsCollection
307
303
  def initialize
308
304
  @rendered_views ||= Hash.new { |hash, key| hash[key] = [] }
@@ -416,7 +412,7 @@ module ActionView
416
412
  end]
417
413
  end
418
414
 
419
- def method_missing(selector, *args)
415
+ def method_missing(selector, ...)
420
416
  begin
421
417
  routes = @controller.respond_to?(:_routes) && @controller._routes
422
418
  rescue
@@ -426,16 +422,15 @@ module ActionView
426
422
  if routes &&
427
423
  (routes.named_routes.route_defined?(selector) ||
428
424
  routes.mounted_helpers.method_defined?(selector))
429
- @controller.__send__(selector, *args)
425
+ @controller.__send__(selector, ...)
430
426
  else
431
427
  super
432
428
  end
433
429
  end
434
- ruby2_keywords(:method_missing)
435
430
 
436
431
  def respond_to_missing?(name, include_private = false)
437
432
  begin
438
- routes = defined?(@controller) && @controller.respond_to?(:_routes) && @controller._routes
433
+ routes = @controller.respond_to?(:_routes) && @controller._routes
439
434
  rescue
440
435
  # Don't call routes, if there is an error on _routes call
441
436
  end
data/lib/action_view.rb CHANGED
@@ -81,6 +81,7 @@ module ActionView
81
81
  autoload :MissingTemplate
82
82
  autoload :ActionViewError
83
83
  autoload :EncodingError
84
+ autoload :StrictLocalsError
84
85
  autoload :TemplateError
85
86
  autoload :SyntaxErrorInTemplate
86
87
  autoload :WrongEncodingError
@@ -90,6 +91,9 @@ module ActionView
90
91
  autoload :CacheExpiry
91
92
  autoload :TestCase
92
93
 
94
+ singleton_class.attr_accessor :render_tracker
95
+ self.render_tracker = :regex
96
+
93
97
  def self.eager_load!
94
98
  super
95
99
  ActionView::Helpers.eager_load!