view_component 3.0.0.rc1 → 3.0.0.rc3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

@@ -1,80 +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
- def inherited(child)
19
- # Clone slot configuration into child class
20
- # see #test_slots_pollution
21
- child.slots = slots.clone
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)
81
+ end
82
+ ruby2_keywords(:"with_#{slot_name}") if respond_to?(:ruby2_keywords, true)
83
+
84
+ define_method slot_name do |*args, &block|
85
+ get_slot(slot_name)
86
+ end
87
+ ruby2_keywords(slot_name.to_sym) if respond_to?(:ruby2_keywords, true)
88
+
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)
151
+ end
152
+ end
22
153
 
154
+ define_method slot_name do |collection_args = nil, &block|
155
+ get_slot(slot_name)
156
+ end
157
+
158
+ define_method "#{slot_name}?" do
159
+ get_slot(slot_name).present?
160
+ end
161
+
162
+ register_slot(slot_name, collection: true, callable: callable)
163
+ end
164
+ end
165
+
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
174
+ end
175
+ end
176
+
177
+ # Clone slot configuration into child class
178
+ # see #test_slots_pollution
179
+ def inherited(child)
180
+ child.registered_slots = registered_slots.clone
23
181
  super
24
182
  end
25
- end
26
183
 
27
- # Build a Slot instance on a component,
28
- # exposing it for use inside the
29
- # component template.
30
- #
31
- # slot: Name of Slot, in symbol form
32
- # **args: Arguments to be passed to Slot initializer
33
- #
34
- # For example:
35
- # <%= render(SlotsComponent.new) do |component| %>
36
- # <% component.slot(:footer, class_names: "footer-class") do %>
37
- # <p>This is my footer!</p>
38
- # <% end %>
39
- # <% end %>
40
- #
41
- def slot(slot_name, **args, &block)
42
- # Raise ArgumentError if `slot` doesn't exist
43
- unless slots.key?(slot_name)
44
- 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
+ }
45
219
  end
46
220
 
47
- slot = slots[slot_name]
221
+ private
222
+
223
+ def register_slot(slot_name, **kwargs)
224
+ registered_slots[slot_name] = define_slot(slot_name, **kwargs)
225
+ end
48
226
 
49
- # The class name of the Slot, such as Header
50
- slot_class = self.class.const_get(slot[:class_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
51
233
 
52
- unless slot_class <= ViewComponent::Slot
53
- raise ArgumentError.new "#{slot[:class_name]} must inherit from ViewComponent::Slot"
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
255
+
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
263
+
264
+ raise_if_slot_ends_with_question_mark(slot_name)
265
+ raise_if_slot_registered(slot_name)
266
+ end
267
+
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
276
+
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
54
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
55
312
 
56
- # Instantiate Slot class, accommodating Slots that don't accept arguments
57
- slot_instance = args.present? ? slot_class.new(**args) : slot_class.new
313
+ slot = self.class.registered_slots[slot_name]
314
+ @__vc_set_slots ||= {}
58
315
 
59
- # Capture block and assign to slot_instance#content
60
- slot_instance.content = view_context.capture(&block).to_s.strip.html_safe if block
316
+ if @__vc_set_slots[slot_name]
317
+ return @__vc_set_slots[slot_name]
318
+ end
61
319
 
62
320
  if slot[:collection]
63
- # Initialize instance variable as an empty array
64
- # if slot is a collection and has yet to be initialized
65
- unless instance_variable_defined?(slot[:instance_variable_name])
66
- 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
67
367
  end
368
+ end
369
+
370
+ @__vc_set_slots ||= {}
68
371
 
69
- # Append Slot instance to collection accessor Array
70
- 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)
71
375
  else
72
- # Assign the Slot instance to the slot accessor
73
- instance_variable_set(slot[:instance_variable_name], slot_instance)
376
+ @__vc_set_slots[slot_name] = slot
377
+ end
378
+
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"
74
388
  end
75
389
 
76
- # Return nil, as this method shouldn't output anything to the view itself.
77
- nil
390
+ poly_def = slot_definition[:renderable_hash][poly_type]
391
+
392
+ set_slot(slot_name, poly_def, *args, &block)
78
393
  end
394
+ ruby2_keywords(:set_polymorphic_slot) if respond_to?(:ruby2_keywords, true)
79
395
  end
80
396
  end
@@ -10,12 +10,12 @@ module ViewComponent
10
10
  # @param layout [String] The (optional) layout to use.
11
11
  # @return [Proc] A block that can be used to visit the path of the inline rendered component.
12
12
  def with_rendered_component_path(fragment, layout: false, &block)
13
- # Add './tmp/view_components/' directory if it doesn't exist to store the rendered component HTML
14
- FileUtils.mkdir_p("./tmp/view_components/") unless Dir.exist?("./tmp/view_components/")
15
-
16
- file = Tempfile.new(["rendered_#{fragment.class.name}", ".html"], "tmp/view_components/")
13
+ file = Tempfile.new(
14
+ ["rendered_#{fragment.class.name}", ".html"],
15
+ ViewComponentsSystemTestController::TEMP_DIR
16
+ )
17
17
  begin
18
- file.write(controller.render_to_string(html: fragment.to_html.html_safe, layout: layout))
18
+ file.write(vc_test_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,7 +28,9 @@ module ViewComponent
28
28
  # :nocov:
29
29
  end
30
30
 
31
- # @private
31
+ # Returns the result of a render_inline call.
32
+ #
33
+ # @return [ActionView::OutputBuffer]
32
34
  attr_reader :rendered_content
33
35
 
34
36
  # Render a component inline. Internally sets `page` to be a `Capybara::Node::Simple`,
@@ -45,9 +47,9 @@ module ViewComponent
45
47
  @page = nil
46
48
  @rendered_content =
47
49
  if Rails.version.to_f >= 6.1
48
- controller.view_context.render(component, args, &block)
50
+ vc_test_controller.view_context.render(component, args, &block)
49
51
  else
50
- controller.view_context.render_component(component, &block)
52
+ vc_test_controller.view_context.render_component(component, &block)
51
53
  end
52
54
 
53
55
  Nokogiri::HTML.fragment(@rendered_content)
@@ -72,8 +74,8 @@ module ViewComponent
72
74
  # @param from [ViewComponent::Preview] The class of the preview to be rendered.
73
75
  # @param params [Hash] Parameters to be passed to the preview.
74
76
  # @return [Nokogiri::HTML]
75
- def render_preview(name, from: preview_class, params: {})
76
- 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)
77
79
 
78
80
  # From what I can tell, it's not possible to overwrite all request parameters
79
81
  # at once, so we set them individually here.
@@ -103,26 +105,11 @@ module ViewComponent
103
105
  # ```
104
106
  def render_in_view_context(*args, &block)
105
107
  @page = nil
106
- @rendered_content = controller.view_context.instance_exec(*args, &block)
108
+ @rendered_content = vc_test_controller.view_context.instance_exec(*args, &block)
107
109
  Nokogiri::HTML.fragment(@rendered_content)
108
110
  end
109
111
  ruby2_keywords(:render_in_view_context) if respond_to?(:ruby2_keywords, true)
110
112
 
111
- # @private
112
- def controller
113
- @controller ||= build_controller(Base.test_controller.constantize)
114
- end
115
-
116
- # @private
117
- def request
118
- @request ||=
119
- begin
120
- request = ActionDispatch::TestRequest.create
121
- request.session = ActionController::TestSession.new
122
- request
123
- end
124
- end
125
-
126
113
  # Set the Action Pack request variant for the given block:
127
114
  #
128
115
  # ```ruby
@@ -133,12 +120,12 @@ module ViewComponent
133
120
  #
134
121
  # @param variant [Symbol] The variant to be set for the provided block.
135
122
  def with_variant(variant)
136
- old_variants = controller.view_context.lookup_context.variants
123
+ old_variants = vc_test_controller.view_context.lookup_context.variants
137
124
 
138
- controller.view_context.lookup_context.variants = variant
125
+ vc_test_controller.view_context.lookup_context.variants = variant
139
126
  yield
140
127
  ensure
141
- controller.view_context.lookup_context.variants = old_variants
128
+ vc_test_controller.view_context.lookup_context.variants = old_variants
142
129
  end
143
130
 
144
131
  # Set the controller to be used while executing the given block,
@@ -152,12 +139,12 @@ module ViewComponent
152
139
  #
153
140
  # @param klass [ActionController::Base] The controller to be used.
154
141
  def with_controller_class(klass)
155
- old_controller = defined?(@controller) && @controller
142
+ old_controller = defined?(@vc_test_controller) && @vc_test_controller
156
143
 
157
- @controller = build_controller(klass)
144
+ @vc_test_controller = __vc_test_helpers_build_controller(klass)
158
145
  yield
159
146
  ensure
160
- @controller = old_controller
147
+ @vc_test_controller = old_controller
161
148
  end
162
149
 
163
150
  # Set the URL of the current request (such as when using request-dependent path helpers):
@@ -170,34 +157,58 @@ module ViewComponent
170
157
  #
171
158
  # @param path [String] The path to set for the current request.
172
159
  def with_request_url(path)
173
- old_request_path_info = request.path_info
174
- old_request_path_parameters = request.path_parameters
175
- old_request_query_parameters = request.query_parameters
176
- old_request_query_string = request.query_string
177
- 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_controller) && @vc_test_controller
178
165
 
179
166
  path, query = path.split("?", 2)
180
- request.path_info = path
181
- request.path_parameters = Rails.application.routes.recognize_path_with_request(request, path, {})
182
- request.set_header("action_dispatch.request.query_parameters", Rack::Utils.parse_nested_query(query))
183
- 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)
184
171
  yield
185
172
  ensure
186
- request.path_info = old_request_path_info
187
- request.path_parameters = old_request_path_parameters
188
- request.set_header("action_dispatch.request.query_parameters", old_request_query_parameters)
189
- request.set_header(Rack::QUERY_STRING, old_request_query_string)
190
- @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_controller = old_controller
191
178
  end
192
179
 
193
- # @private
194
- def build_controller(klass)
195
- klass.new.tap { |c| c.request = request }.extend(Rails.application.routes.url_helpers)
180
+ # Access the controller used by `render_inline`:
181
+ #
182
+ # ```ruby
183
+ # test "logged out user sees login link" do
184
+ # vc_test_controller.expects(:logged_in?).at_least_once.returns(false)
185
+ # render_inline(LoginComponent.new)
186
+ # assert_selector("[aria-label='You must be signed in']")
187
+ # end
188
+ # ```
189
+ #
190
+ # @return [ActionController::Base]
191
+ def vc_test_controller
192
+ @vc_test_controller ||= __vc_test_helpers_build_controller(Base.test_controller.constantize)
196
193
  end
197
194
 
195
+ # Note: We prefix private methods here to prevent collisions in consumer's tests.
198
196
  private
199
197
 
200
- def preview_class
198
+ def __vc_test_helpers_request
199
+ @__vc_test_helpers_request ||=
200
+ begin
201
+ out = ActionDispatch::TestRequest.create
202
+ out.session = ActionController::TestSession.new
203
+ out
204
+ end
205
+ end
206
+
207
+ def __vc_test_helpers_build_controller(klass)
208
+ klass.new.tap { |c| c.request = __vc_test_helpers_request }.extend(Rails.application.routes.url_helpers)
209
+ end
210
+
211
+ def __vc_test_helpers_preview_class
201
212
  result = if respond_to?(:described_class)
202
213
  raise "`render_preview` expected a described_class, but it is nil." if described_class.nil?
203
214