view_component 2.82.0 → 3.0.0.rc2

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.

Potentially problematic release.


This version of view_component might be problematic. Click here for more details.

@@ -1,145 +1,396 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/concern"
4
-
5
4
  require "view_component/slot"
6
5
 
7
6
  module ViewComponent
8
7
  module Slotable
9
8
  extend ActiveSupport::Concern
10
9
 
10
+ RESERVED_NAMES = {
11
+ singular: %i[content render].freeze,
12
+ plural: %i[contents renders].freeze
13
+ }.freeze
14
+
15
+ # Setup component slot state
11
16
  included do
12
17
  # Hash of registered Slots
13
- class_attribute :slots
14
- self.slots = {}
18
+ class_attribute :registered_slots
19
+ self.registered_slots = {}
15
20
  end
16
21
 
17
22
  class_methods do
18
- # support initializing slots as:
19
- #
20
- # with_slot(
21
- # :header,
22
- # collection: true|false,
23
- # class_name: "Header" # class name string, used to instantiate Slot
24
- # )
25
- def with_slot(*slot_names, collection: false, class_name: nil)
26
- ViewComponent::Deprecation.deprecation_warning(
27
- "`with_slot`", "use the new slots API (https://viewcomponent.org/guide/slots.html) instead"
28
- )
29
-
30
- slot_names.each do |slot_name|
31
- # Ensure slot_name isn't already declared
32
- if slots.key?(slot_name)
33
- raise ArgumentError.new("#{slot_name} slot declared multiple times")
23
+ ##
24
+ # Registers a sub-component
25
+ #
26
+ # = Example
27
+ #
28
+ # renders_one :header -> (classes:) do
29
+ # HeaderComponent.new(classes: classes)
30
+ # end
31
+ #
32
+ # # OR
33
+ #
34
+ # renders_one :header, HeaderComponent
35
+ #
36
+ # where `HeaderComponent` is defined as:
37
+ #
38
+ # class HeaderComponent < ViewComponent::Base
39
+ # def initialize(classes:)
40
+ # @classes = classes
41
+ # end
42
+ # end
43
+ #
44
+ # and has the following template:
45
+ #
46
+ # <header class="<%= @classes %>">
47
+ # <%= content %>
48
+ # </header>
49
+ #
50
+ # = Rendering sub-component content
51
+ #
52
+ # The component's sidecar template can access the sub-component by calling a
53
+ # helper method with the same name as the sub-component.
54
+ #
55
+ # <h1>
56
+ # <%= header do %>
57
+ # My header title
58
+ # <% end %>
59
+ # </h1>
60
+ #
61
+ # = Setting sub-component content
62
+ #
63
+ # Consumers of the component can render a sub-component by calling a
64
+ # helper method with the same name as the slot prefixed with `with_`.
65
+ #
66
+ # <%= render_inline(MyComponent.new) do |component| %>
67
+ # <% component.with_header(classes: "Foo") do %>
68
+ # <p>Bar</p>
69
+ # <% end %>
70
+ # <% end %>
71
+ def renders_one(slot_name, callable = nil)
72
+ validate_singular_slot_name(slot_name)
73
+
74
+ if callable.is_a?(Hash) && callable.key?(:types)
75
+ register_polymorphic_slot(slot_name, callable[:types], collection: false)
76
+ else
77
+ validate_plural_slot_name(ActiveSupport::Inflector.pluralize(slot_name).to_sym)
78
+
79
+ define_method :"with_#{slot_name}" do |*args, &block|
80
+ set_slot(slot_name, nil, *args, &block)
34
81
  end
82
+ ruby2_keywords(:"with_#{slot_name}") if respond_to?(:ruby2_keywords, true)
35
83
 
36
- # Ensure slot name isn't :content
37
- if slot_name == :content
38
- raise ArgumentError.new ":content is a reserved slot name. Please use another name, such as ':body'"
84
+ define_method slot_name do |*args, &block|
85
+ get_slot(slot_name)
39
86
  end
87
+ ruby2_keywords(slot_name.to_sym) if respond_to?(:ruby2_keywords, true)
40
88
 
41
- # Set the name of the method used to access the Slot(s)
42
- accessor_name =
43
- if collection
44
- # If Slot is a collection, set the accessor
45
- # name to the pluralized form of the slot name
46
- # For example: :tab => :tabs
47
- ActiveSupport::Inflector.pluralize(slot_name)
48
- else
49
- slot_name
89
+ define_method "#{slot_name}?" do
90
+ get_slot(slot_name).present?
91
+ end
92
+
93
+ register_slot(slot_name, collection: false, callable: callable)
94
+ end
95
+ end
96
+
97
+ ##
98
+ # Registers a collection sub-component
99
+ #
100
+ # = Example
101
+ #
102
+ # renders_many :items, -> (name:) { ItemComponent.new(name: name }
103
+ #
104
+ # # OR
105
+ #
106
+ # renders_many :items, ItemComponent
107
+ #
108
+ # = Rendering sub-components
109
+ #
110
+ # The component's sidecar template can access the slot by calling a
111
+ # helper method with the same name as the slot.
112
+ #
113
+ # <h1>
114
+ # <% items.each do |item| %>
115
+ # <%= item %>
116
+ # <% end %>
117
+ # </h1>
118
+ #
119
+ # = Setting sub-component content
120
+ #
121
+ # Consumers of the component can set the content of a slot by calling a
122
+ # helper method with the same name as the slot prefixed with `with_`. The
123
+ # method can be called multiple times to append to the slot.
124
+ #
125
+ # <%= render_inline(MyComponent.new) do |component| %>
126
+ # <% component.with_item(name: "Foo") do %>
127
+ # <p>One</p>
128
+ # <% end %>
129
+ #
130
+ # <% component.with_item(name: "Bar") do %>
131
+ # <p>two</p>
132
+ # <% end %>
133
+ # <% end %>
134
+ def renders_many(slot_name, callable = nil)
135
+ validate_plural_slot_name(slot_name)
136
+
137
+ if callable.is_a?(Hash) && callable.key?(:types)
138
+ register_polymorphic_slot(slot_name, callable[:types], collection: true)
139
+ else
140
+ singular_name = ActiveSupport::Inflector.singularize(slot_name)
141
+ validate_singular_slot_name(ActiveSupport::Inflector.singularize(slot_name).to_sym)
142
+
143
+ define_method :"with_#{singular_name}" do |*args, &block|
144
+ set_slot(slot_name, nil, *args, &block)
145
+ end
146
+ ruby2_keywords(:"with_#{singular_name}") if respond_to?(:ruby2_keywords, true)
147
+
148
+ define_method :"with_#{slot_name}" do |collection_args = nil, &block|
149
+ collection_args.map do |args|
150
+ set_slot(slot_name, nil, **args, &block)
50
151
  end
152
+ end
51
153
 
52
- instance_variable_name = "@#{accessor_name}"
154
+ define_method slot_name do |collection_args = nil, &block|
155
+ get_slot(slot_name)
156
+ end
53
157
 
54
- # If the slot is a collection, define an accesor that defaults to an empty array
55
- if collection
56
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
57
- def #{accessor_name}
58
- content unless content_evaluated? # ensure content is loaded so slots will be defined
59
- #{instance_variable_name} ||= []
60
- end
61
- RUBY
62
- else
63
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
64
- def #{accessor_name}
65
- content unless content_evaluated? # ensure content is loaded so slots will be defined
66
- #{instance_variable_name} if defined?(#{instance_variable_name})
67
- end
68
- RUBY
158
+ define_method "#{slot_name}?" do
159
+ get_slot(slot_name).present?
69
160
  end
70
161
 
71
- # Default class_name to ViewComponent::Slot
72
- class_name = "ViewComponent::Slot" unless class_name.present?
162
+ register_slot(slot_name, collection: true, callable: callable)
163
+ end
164
+ end
73
165
 
74
- # Register the slot on the component
75
- slots[slot_name] = {
76
- class_name: class_name,
77
- instance_variable_name: instance_variable_name,
78
- collection: collection
79
- }
166
+ def slot_type(slot_name)
167
+ registered_slot = registered_slots[slot_name]
168
+ if registered_slot
169
+ registered_slot[:collection] ? :collection : :single
170
+ else
171
+ plural_slot_name = ActiveSupport::Inflector.pluralize(slot_name).to_sym
172
+ plural_registered_slot = registered_slots[plural_slot_name]
173
+ plural_registered_slot&.fetch(:collection) ? :collection_item : nil
80
174
  end
81
175
  end
82
176
 
177
+ # Clone slot configuration into child class
178
+ # see #test_slots_pollution
83
179
  def inherited(child)
84
- # Clone slot configuration into child class
85
- # see #test_slots_pollution
86
- child.slots = slots.clone
87
-
180
+ child.registered_slots = registered_slots.clone
88
181
  super
89
182
  end
90
- end
91
183
 
92
- # Build a Slot instance on a component,
93
- # exposing it for use inside the
94
- # component template.
95
- #
96
- # slot: Name of Slot, in symbol form
97
- # **args: Arguments to be passed to Slot initializer
98
- #
99
- # For example:
100
- # <%= render(SlotsComponent.new) do |component| %>
101
- # <% component.slot(:footer, class_names: "footer-class") do %>
102
- # <p>This is my footer!</p>
103
- # <% end %>
104
- # <% end %>
105
- #
106
- def slot(slot_name, **args, &block)
107
- # Raise ArgumentError if `slot` doesn't exist
108
- unless slots.key?(slot_name)
109
- raise ArgumentError.new "Unknown slot '#{slot_name}' - expected one of '#{slots.keys}'"
184
+ def register_polymorphic_slot(slot_name, types, collection:)
185
+ unless types.empty?
186
+ getter_name = slot_name
187
+
188
+ define_method(getter_name) do
189
+ get_slot(slot_name)
190
+ end
191
+
192
+ define_method("#{getter_name}?") do
193
+ get_slot(slot_name).present?
194
+ end
195
+ end
196
+
197
+ renderable_hash = types.each_with_object({}) do |(poly_type, poly_callable), memo|
198
+ memo[poly_type] = define_slot(
199
+ "#{slot_name}_#{poly_type}", collection: collection, callable: poly_callable
200
+ )
201
+
202
+ setter_name =
203
+ if collection
204
+ "#{ActiveSupport::Inflector.singularize(slot_name)}_#{poly_type}"
205
+ else
206
+ "#{slot_name}_#{poly_type}"
207
+ end
208
+
209
+ define_method("with_#{setter_name}") do |*args, &block|
210
+ set_polymorphic_slot(slot_name, poly_type, *args, &block)
211
+ end
212
+ ruby2_keywords(:"with_#{setter_name}") if respond_to?(:ruby2_keywords, true)
213
+ end
214
+
215
+ registered_slots[slot_name] = {
216
+ collection: collection,
217
+ renderable_hash: renderable_hash
218
+ }
219
+ end
220
+
221
+ private
222
+
223
+ def register_slot(slot_name, **kwargs)
224
+ registered_slots[slot_name] = define_slot(slot_name, **kwargs)
110
225
  end
111
226
 
112
- slot = slots[slot_name]
227
+ def define_slot(slot_name, collection:, callable:)
228
+ # Setup basic slot data
229
+ slot = {
230
+ collection: collection
231
+ }
232
+ return slot unless callable
233
+
234
+ # If callable responds to `render_in`, we set it on the slot as a renderable
235
+ if callable.respond_to?(:method_defined?) && callable.method_defined?(:render_in)
236
+ slot[:renderable] = callable
237
+ elsif callable.is_a?(String)
238
+ # If callable is a string, we assume it's referencing an internal class
239
+ slot[:renderable_class_name] = callable
240
+ elsif callable.respond_to?(:call)
241
+ # If slot doesn't respond to `render_in`, we assume it's a proc,
242
+ # define a method, and save a reference to it to call when setting
243
+ method_name = :"_call_#{slot_name}"
244
+ define_method method_name, &callable
245
+ slot[:renderable_function] = instance_method(method_name)
246
+ else
247
+ raise(
248
+ ArgumentError,
249
+ "invalid slot definition. Please pass a class, string, or callable (i.e. proc, lambda, etc)"
250
+ )
251
+ end
252
+
253
+ slot
254
+ end
113
255
 
114
- # The class name of the Slot, such as Header
115
- slot_class = self.class.const_get(slot[:class_name])
256
+ def validate_plural_slot_name(slot_name)
257
+ if RESERVED_NAMES[:plural].include?(slot_name.to_sym)
258
+ raise ArgumentError.new(
259
+ "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
260
+ "To fix this issue, choose a different name."
261
+ )
262
+ end
116
263
 
117
- unless slot_class <= ViewComponent::Slot
118
- raise ArgumentError.new "#{slot[:class_name]} must inherit from ViewComponent::Slot"
264
+ raise_if_slot_ends_with_question_mark(slot_name)
265
+ raise_if_slot_registered(slot_name)
119
266
  end
120
267
 
121
- # Instantiate Slot class, accommodating Slots that don't accept arguments
122
- slot_instance = args.present? ? slot_class.new(**args) : slot_class.new
268
+ def validate_singular_slot_name(slot_name)
269
+ if slot_name.to_sym == :content
270
+ raise ArgumentError.new(
271
+ "#{self} declares a slot named content, which is a reserved word in ViewComponent.\n\n" \
272
+ "Content passed to a ViewComponent as a block is captured and assigned to the `content` accessor without having to create an explicit slot.\n\n" \
273
+ "To fix this issue, either use the `content` accessor directly or choose a different slot name."
274
+ )
275
+ end
123
276
 
124
- # Capture block and assign to slot_instance#content
125
- slot_instance.content = view_context.capture(&block).to_s.strip.html_safe if block
277
+ if RESERVED_NAMES[:singular].include?(slot_name.to_sym)
278
+ raise ArgumentError.new(
279
+ "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
280
+ "To fix this issue, choose a different name."
281
+ )
282
+ end
283
+
284
+ raise_if_slot_ends_with_question_mark(slot_name)
285
+ raise_if_slot_registered(slot_name)
286
+ end
287
+
288
+ def raise_if_slot_registered(slot_name)
289
+ if registered_slots.key?(slot_name)
290
+ # TODO remove? This breaks overriding slots when slots are inherited
291
+ raise ArgumentError.new(
292
+ "#{self} declares the #{slot_name} slot multiple times.\n\n" \
293
+ "To fix this issue, choose a different slot name."
294
+ )
295
+ end
296
+ end
297
+
298
+ def raise_if_slot_ends_with_question_mark(slot_name)
299
+ if slot_name.to_s.ends_with?("?")
300
+ raise ArgumentError.new(
301
+ "#{self} declares a slot named #{slot_name}, which ends with a question mark.\n\n" \
302
+ "This is not allowed because the ViewComponent framework already provides predicate " \
303
+ "methods ending in `?`.\n\n" \
304
+ "To fix this issue, choose a different name."
305
+ )
306
+ end
307
+ end
308
+ end
309
+
310
+ def get_slot(slot_name)
311
+ content unless content_evaluated? # ensure content is loaded so slots will be defined
312
+
313
+ slot = self.class.registered_slots[slot_name]
314
+ @__vc_set_slots ||= {}
315
+
316
+ if @__vc_set_slots[slot_name]
317
+ return @__vc_set_slots[slot_name]
318
+ end
126
319
 
127
320
  if slot[:collection]
128
- # Initialize instance variable as an empty array
129
- # if slot is a collection and has yet to be initialized
130
- unless instance_variable_defined?(slot[:instance_variable_name])
131
- instance_variable_set(slot[:instance_variable_name], [])
321
+ []
322
+ end
323
+ end
324
+
325
+ def set_slot(slot_name, slot_definition = nil, *args, &block)
326
+ slot_definition ||= self.class.registered_slots[slot_name]
327
+ slot = Slot.new(self)
328
+
329
+ # Passing the block to the sub-component wrapper like this has two
330
+ # benefits:
331
+ #
332
+ # 1. If this is a `content_area` style sub-component, we will render the
333
+ # block via the `slot`
334
+ #
335
+ # 2. Since we've to pass block content to components when calling
336
+ # `render`, evaluating the block here would require us to call
337
+ # `view_context.capture` twice, which is slower
338
+ slot.__vc_content_block = block if block
339
+
340
+ # If class
341
+ if slot_definition[:renderable]
342
+ slot.__vc_component_instance = slot_definition[:renderable].new(*args)
343
+ # If class name as a string
344
+ elsif slot_definition[:renderable_class_name]
345
+ slot.__vc_component_instance =
346
+ self.class.const_get(slot_definition[:renderable_class_name]).new(*args)
347
+ # If passed a lambda
348
+ elsif slot_definition[:renderable_function]
349
+ # Use `bind(self)` to ensure lambda is executed in the context of the
350
+ # current component. This is necessary to allow the lambda to access helper
351
+ # methods like `content_tag` as well as parent component state.
352
+ renderable_function = slot_definition[:renderable_function].bind(self)
353
+ renderable_value =
354
+ if block
355
+ renderable_function.call(*args) do |*rargs|
356
+ view_context.capture(*rargs, &block)
357
+ end
358
+ else
359
+ renderable_function.call(*args)
360
+ end
361
+
362
+ # Function calls can return components, so if it's a component handle it specially
363
+ if renderable_value.respond_to?(:render_in)
364
+ slot.__vc_component_instance = renderable_value
365
+ else
366
+ slot.__vc_content = renderable_value
132
367
  end
368
+ end
369
+
370
+ @__vc_set_slots ||= {}
133
371
 
134
- # Append Slot instance to collection accessor Array
135
- instance_variable_get(slot[:instance_variable_name]) << slot_instance
372
+ if slot_definition[:collection]
373
+ @__vc_set_slots[slot_name] ||= []
374
+ @__vc_set_slots[slot_name].push(slot)
136
375
  else
137
- # Assign the Slot instance to the slot accessor
138
- instance_variable_set(slot[:instance_variable_name], slot_instance)
376
+ @__vc_set_slots[slot_name] = slot
139
377
  end
140
378
 
141
- # Return nil, as this method shouldn't output anything to the view itself.
142
- nil
379
+ slot
380
+ end
381
+ ruby2_keywords(:set_slot) if respond_to?(:ruby2_keywords, true)
382
+
383
+ def set_polymorphic_slot(slot_name, poly_type = nil, *args, &block)
384
+ slot_definition = self.class.registered_slots[slot_name]
385
+
386
+ if !slot_definition[:collection] && (defined?(@__vc_set_slots) && @__vc_set_slots[slot_name])
387
+ raise ArgumentError, "content for slot '#{slot_name}' has already been provided"
388
+ end
389
+
390
+ poly_def = slot_definition[:renderable_hash][poly_type]
391
+
392
+ set_slot(slot_name, poly_def, *args, &block)
143
393
  end
394
+ ruby2_keywords(:set_polymorphic_slot) if respond_to?(:ruby2_keywords, true)
144
395
  end
145
396
  end
@@ -15,7 +15,7 @@ module ViewComponent
15
15
 
16
16
  file = Tempfile.new(["rendered_#{fragment.class.name}", ".html"], "tmp/view_components/")
17
17
  begin
18
- file.write(controller.render_to_string(html: fragment.to_html.html_safe, layout: layout))
18
+ file.write(__vc_test_helpers_controller.render_to_string(html: fragment.to_html.html_safe, layout: layout))
19
19
  file.rewind
20
20
 
21
21
  block.call("/_system_test_entrypoint?file=#{file.path.split("/").last}")
@@ -28,17 +28,10 @@ module ViewComponent
28
28
  # :nocov:
29
29
  end
30
30
 
31
- # @private
32
- attr_reader :rendered_content
33
-
34
31
  # Returns the result of a render_inline call.
35
32
  #
36
- # @return [String]
37
- def rendered_component
38
- ViewComponent::Deprecation.deprecation_warning("`rendered_component`", :"`page`")
39
-
40
- rendered_content
41
- end
33
+ # @return [ActionView::OutputBuffer]
34
+ attr_reader :rendered_content
42
35
 
43
36
  # Render a component inline. Internally sets `page` to be a `Capybara::Node::Simple`,
44
37
  # allowing for Capybara assertions to be used:
@@ -54,9 +47,9 @@ module ViewComponent
54
47
  @page = nil
55
48
  @rendered_content =
56
49
  if Rails.version.to_f >= 6.1
57
- controller.view_context.render(component, args, &block)
50
+ __vc_test_helpers_controller.view_context.render(component, args, &block)
58
51
  else
59
- controller.view_context.render_component(component, &block)
52
+ __vc_test_helpers_controller.view_context.render_component(component, &block)
60
53
  end
61
54
 
62
55
  Nokogiri::HTML.fragment(@rendered_content)
@@ -81,8 +74,8 @@ module ViewComponent
81
74
  # @param from [ViewComponent::Preview] The class of the preview to be rendered.
82
75
  # @param params [Hash] Parameters to be passed to the preview.
83
76
  # @return [Nokogiri::HTML]
84
- def render_preview(name, from: preview_class, params: {})
85
- previews_controller = build_controller(Rails.application.config.view_component.preview_controller.constantize)
77
+ def render_preview(name, from: __vc_test_helpers_preview_class, params: {})
78
+ previews_controller = __vc_test_helpers_build_controller(Rails.application.config.view_component.preview_controller.constantize)
86
79
 
87
80
  # From what I can tell, it's not possible to overwrite all request parameters
88
81
  # at once, so we set them individually here.
@@ -112,26 +105,11 @@ module ViewComponent
112
105
  # ```
113
106
  def render_in_view_context(*args, &block)
114
107
  @page = nil
115
- @rendered_content = controller.view_context.instance_exec(*args, &block)
108
+ @rendered_content = __vc_test_helpers_controller.view_context.instance_exec(*args, &block)
116
109
  Nokogiri::HTML.fragment(@rendered_content)
117
110
  end
118
111
  ruby2_keywords(:render_in_view_context) if respond_to?(:ruby2_keywords, true)
119
112
 
120
- # @private
121
- def controller
122
- @controller ||= build_controller(Base.test_controller.constantize)
123
- end
124
-
125
- # @private
126
- def request
127
- @request ||=
128
- begin
129
- request = ActionDispatch::TestRequest.create
130
- request.session = ActionController::TestSession.new
131
- request
132
- end
133
- end
134
-
135
113
  # Set the Action Pack request variant for the given block:
136
114
  #
137
115
  # ```ruby
@@ -142,12 +120,12 @@ module ViewComponent
142
120
  #
143
121
  # @param variant [Symbol] The variant to be set for the provided block.
144
122
  def with_variant(variant)
145
- old_variants = controller.view_context.lookup_context.variants
123
+ old_variants = __vc_test_helpers_controller.view_context.lookup_context.variants
146
124
 
147
- controller.view_context.lookup_context.variants = variant
125
+ __vc_test_helpers_controller.view_context.lookup_context.variants = variant
148
126
  yield
149
127
  ensure
150
- controller.view_context.lookup_context.variants = old_variants
128
+ __vc_test_helpers_controller.view_context.lookup_context.variants = old_variants
151
129
  end
152
130
 
153
131
  # Set the controller to be used while executing the given block,
@@ -161,12 +139,12 @@ module ViewComponent
161
139
  #
162
140
  # @param klass [ActionController::Base] The controller to be used.
163
141
  def with_controller_class(klass)
164
- old_controller = defined?(@controller) && @controller
142
+ old_controller = defined?(@__vc_test_helpers_controller) && @__vc_test_helpers_controller
165
143
 
166
- @controller = build_controller(klass)
144
+ @__vc_test_helpers_controller = __vc_test_helpers_build_controller(klass)
167
145
  yield
168
146
  ensure
169
- @controller = old_controller
147
+ @__vc_test_helpers_controller = old_controller
170
148
  end
171
149
 
172
150
  # Set the URL of the current request (such as when using request-dependent path helpers):
@@ -179,34 +157,47 @@ module ViewComponent
179
157
  #
180
158
  # @param path [String] The path to set for the current request.
181
159
  def with_request_url(path)
182
- old_request_path_info = request.path_info
183
- old_request_path_parameters = request.path_parameters
184
- old_request_query_parameters = request.query_parameters
185
- old_request_query_string = request.query_string
186
- old_controller = defined?(@controller) && @controller
160
+ old_request_path_info = __vc_test_helpers_request.path_info
161
+ old_request_path_parameters = __vc_test_helpers_request.path_parameters
162
+ old_request_query_parameters = __vc_test_helpers_request.query_parameters
163
+ old_request_query_string = __vc_test_helpers_request.query_string
164
+ old_controller = defined?(@__vc_test_helpers_controller) && @__vc_test_helpers_controller
187
165
 
188
166
  path, query = path.split("?", 2)
189
- request.path_info = path
190
- request.path_parameters = Rails.application.routes.recognize_path_with_request(request, path, {})
191
- request.set_header("action_dispatch.request.query_parameters", Rack::Utils.parse_nested_query(query))
192
- request.set_header(Rack::QUERY_STRING, query)
167
+ __vc_test_helpers_request.path_info = path
168
+ __vc_test_helpers_request.path_parameters = Rails.application.routes.recognize_path_with_request(__vc_test_helpers_request, path, {})
169
+ __vc_test_helpers_request.set_header("action_dispatch.request.query_parameters", Rack::Utils.parse_nested_query(query))
170
+ __vc_test_helpers_request.set_header(Rack::QUERY_STRING, query)
193
171
  yield
194
172
  ensure
195
- request.path_info = old_request_path_info
196
- request.path_parameters = old_request_path_parameters
197
- request.set_header("action_dispatch.request.query_parameters", old_request_query_parameters)
198
- request.set_header(Rack::QUERY_STRING, old_request_query_string)
199
- @controller = old_controller
173
+ __vc_test_helpers_request.path_info = old_request_path_info
174
+ __vc_test_helpers_request.path_parameters = old_request_path_parameters
175
+ __vc_test_helpers_request.set_header("action_dispatch.request.query_parameters", old_request_query_parameters)
176
+ __vc_test_helpers_request.set_header(Rack::QUERY_STRING, old_request_query_string)
177
+ @__vc_test_helpers_controller = old_controller
200
178
  end
201
179
 
202
- # @private
203
- def build_controller(klass)
204
- klass.new.tap { |c| c.request = request }.extend(Rails.application.routes.url_helpers)
180
+ # Note: We prefix private methods here to prevent collisions in consumer's tests.
181
+ private
182
+
183
+ def __vc_test_helpers_controller
184
+ @__vc_test_helpers_controller ||= __vc_test_helpers_build_controller(Base.test_controller.constantize)
205
185
  end
206
186
 
207
- private
187
+ def __vc_test_helpers_request
188
+ @__vc_test_helpers_request ||=
189
+ begin
190
+ out = ActionDispatch::TestRequest.create
191
+ out.session = ActionController::TestSession.new
192
+ out
193
+ end
194
+ end
195
+
196
+ def __vc_test_helpers_build_controller(klass)
197
+ klass.new.tap { |c| c.request = __vc_test_helpers_request }.extend(Rails.application.routes.url_helpers)
198
+ end
208
199
 
209
- def preview_class
200
+ def __vc_test_helpers_preview_class
210
201
  result = if respond_to?(:described_class)
211
202
  raise "`render_preview` expected a described_class, but it is nil." if described_class.nil?
212
203