File: test_helpers.rb

package info (click to toggle)
ruby-view-component 4.4.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 472 kB
  • sloc: ruby: 2,278; makefile: 4
file content (316 lines) | stat: -rw-r--r-- 10,906 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# frozen_string_literal: true

module ViewComponent
  module TestHelpers
    begin
      require "capybara/minitest"

      include Capybara::Minitest::Assertions

      def page
        @page ||= Capybara::Node::Simple.new(rendered_content)
      end

      def refute_component_rendered
        assert_no_selector("body")
      end

      def assert_component_rendered
        assert_selector("body")
      end
    rescue LoadError # We don't have a test case for running an application without capybara installed.
    end

    # Returns the result of a render_inline call.
    #
    # @return [ActionView::OutputBuffer]
    attr_reader :rendered_content

    # Render a component inline. Internally sets `page` to be a `Capybara::Node::Simple`,
    # allowing for Capybara assertions to be used:
    #
    # ```ruby
    # render_inline(MyComponent.new)
    # assert_text("Hello, World!")
    # ```
    #
    # @param component [ViewComponent::Base, ViewComponent::Collection] The instance of the component to be rendered.
    # @return [Nokogiri::HTML5]
    def render_inline(component, **args, &block)
      @page = nil
      @rendered_content = vc_test_view_context.render(component, args, &block)

      fragment = Nokogiri::HTML5.fragment(@rendered_content, context: "template")
      @vc_test_view_context = nil
      fragment
    end

    # Returns the view context used to render components in tests. Note that the view context
    # is reset after each call to `render_inline`.
    #
    # @return [ActionView::Base]
    def vc_test_view_context
      @vc_test_view_context ||= vc_test_controller.view_context
    end

    # `JSON.parse`-d component output.
    #
    # ```ruby
    # render_inline(MyJsonComponent.new)
    # assert_equal(rendered_json["hello"], "world")
    # ```
    def rendered_json
      JSON.parse(rendered_content)
    end

    # Render a preview inline. Internally sets `page` to be a `Capybara::Node::Simple`,
    # allowing for Capybara assertions to be used:
    #
    # ```ruby
    # render_preview(:default)
    # assert_text("Hello, World!")
    # ```
    #
    # Note: `#rendered_preview` expects a preview to be defined with the same class
    # name as the calling test, but with `Test` replaced with `Preview`:
    #
    # MyComponentTest -> MyComponentPreview etc.
    #
    # In RSpec, `Preview` is appended to `described_class`.
    #
    # @param name [String] The name of the preview to be rendered.
    # @param from [ViewComponent::Preview] The class of the preview to be rendered.
    # @param params [Hash] Parameters to be passed to the preview.
    # @return [Nokogiri::HTML5]
    def render_preview(name, from: __vc_test_helpers_preview_class, params: {})
      previews_controller = __vc_test_helpers_build_controller(Rails.application.config.view_component.previews.controller.constantize)

      # From what I can tell, it's not possible to overwrite all request parameters
      # at once, so we set them individually here.
      params.each do |k, v|
        previews_controller.request.params[k] = v
      end

      previews_controller.request.params[:path] = "#{from.preview_name}/#{name}"
      previews_controller.set_response!(ActionDispatch::Response.new)
      result = previews_controller.previews

      @rendered_content = result

      Nokogiri::HTML5.fragment(@rendered_content)
    end

    # Execute the given block in the view context (using `instance_exec`).
    # Internally sets `page` to be a `Capybara::Node::Simple`, allowing for
    # Capybara assertions to be used. All arguments are forwarded to the block.
    #
    # ```ruby
    # render_in_view_context(arg1, arg2: nil) do |arg1, arg2:|
    #   render(MyComponent.new(arg1, arg2))
    # end
    #
    # assert_text("Hello, World!")
    # ```
    def render_in_view_context(...)
      @page = nil
      @rendered_content = vc_test_view_context.instance_exec(...)
      Nokogiri::HTML5.fragment(@rendered_content)
    end

    # Set the Action Pack request variant for the given block:
    #
    # ```ruby
    # with_variant(:phone) do
    #   render_inline(MyComponent.new)
    # end
    # ```
    #
    # @param variants [Symbol[]] The variants to be set for the provided block.
    def with_variant(*variants)
      old_variants = vc_test_controller.view_context.lookup_context.variants

      vc_test_controller.view_context.lookup_context.variants += variants
      yield
    ensure
      vc_test_controller.view_context.lookup_context.variants = old_variants
    end

    # Set the controller to be used while executing the given block,
    # allowing access to controller-specific methods:
    #
    # ```ruby
    # with_controller_class(UsersController) do
    #   render_inline(MyComponent.new)
    # end
    # ```
    #
    # @param klass [Class<ActionController::Base>] The controller to be used.
    def with_controller_class(klass)
      old_controller = defined?(@vc_test_controller) && @vc_test_controller

      @vc_test_controller = __vc_test_helpers_build_controller(klass)
      yield
    ensure
      @vc_test_controller = old_controller
    end

    # Set format of the current request
    #
    # ```ruby
    # with_format(:json) do
    #   render_inline(MyComponent.new)
    # end
    # ```
    #
    # @param formats [Symbol[]] The format(s) to be set for the provided block.
    def with_format(*formats)
      old_formats = vc_test_controller.view_context.lookup_context.formats

      vc_test_controller.view_context.lookup_context.formats = formats
      yield
    ensure
      vc_test_controller.view_context.lookup_context.formats = old_formats
    end

    # Set the URL of the current request (such as when using request-dependent path helpers):
    #
    # ```ruby
    # with_request_url("/users/42") do
    #   render_inline(MyComponent.new)
    # end
    # ```
    #
    # To use a specific host, pass the host param:
    #
    # ```ruby
    # with_request_url("/users/42", host: "app.example.com") do
    #   render_inline(MyComponent.new)
    # end
    # ```
    #
    # To specify a request method, pass the method param:
    #
    # ```ruby
    # with_request_url("/users/42", method: "POST") do
    #   render_inline(MyComponent.new)
    # end
    # ```
    #
    # To specify a protocol, pass the protocol param:
    #
    # ```ruby
    # with_request_url("/users/42", protocol: :https) do
    #   render_inline(MyComponent.new)
    # end
    # ```
    #
    # @param full_path [String] The path to set for the current request.
    # @param host [String] The host to set for the current request.
    # @param method [String] The request method to set for the current request.
    # @param protocol [Symbol] The protocol to set for the current request (e.g., `:http` or `:https`).
    def with_request_url(full_path, host: nil, method: nil, protocol: nil)
      old_request_host = vc_test_request.host
      old_request_method = vc_test_request.request_method
      old_request_path_info = vc_test_request.path_info
      old_request_path_parameters = vc_test_request.path_parameters
      old_request_query_parameters = vc_test_request.query_parameters
      old_request_query_string = vc_test_request.query_string
      old_request_format = vc_test_request.format.symbol
      old_request_scheme = vc_test_request.scheme
      old_controller = defined?(@vc_test_controller) && @vc_test_controller

      path, query = full_path.split("?", 2)
      vc_test_request.instance_variable_set(:@fullpath, full_path)
      vc_test_request.instance_variable_set(:@original_fullpath, full_path)
      vc_test_request.host = host if host
      vc_test_request.request_method = method if method
      vc_test_request.set_header(Rack::RACK_URL_SCHEME, protocol.to_s) if protocol
      vc_test_request.path_info = path
      vc_test_request.path_parameters = Rails.application.routes.recognize_path_with_request(vc_test_request, path, {})
      vc_test_request.set_header("action_dispatch.request.query_parameters",
        Rack::Utils.parse_nested_query(query).with_indifferent_access)
      vc_test_request.set_header(Rack::QUERY_STRING, query)
      yield
    ensure
      vc_test_request.host = old_request_host
      vc_test_request.request_method = old_request_method
      vc_test_request.set_header(Rack::RACK_URL_SCHEME, old_request_scheme)
      vc_test_request.path_info = old_request_path_info
      vc_test_request.path_parameters = old_request_path_parameters
      vc_test_request.set_header("action_dispatch.request.query_parameters", old_request_query_parameters)
      vc_test_request.set_header(Rack::QUERY_STRING, old_request_query_string)
      vc_test_request.format = old_request_format
      @vc_test_controller = old_controller
    end

    # Access the controller used by `render_inline`:
    #
    # ```ruby
    # test "logged out user sees login link" do
    #   vc_test_controller.expects(:logged_in?).at_least_once.returns(false)
    #   render_inline(LoginComponent.new)
    #   assert_selector("[aria-label='You must be signed in']")
    # end
    # ```
    #
    # @return [ActionController::Base]
    def vc_test_controller
      @vc_test_controller ||= __vc_test_helpers_build_controller(vc_test_controller_class)
    end

    # Set the controller used by `render_inline`:
    #
    # ```ruby
    # def vc_test_controller_class
    #   MyTestController
    # end
    # ```
    def vc_test_controller_class
      return @__vc_test_controller_class if defined?(@__vc_test_controller_class)

      defined?(ApplicationController) ? ApplicationController : ActionController::Base
    end

    # Access the request used by `render_inline`:
    #
    # ```ruby
    # test "component does not render in Firefox" do
    #   request.env["HTTP_USER_AGENT"] = "Mozilla/5.0"
    #   render_inline(NoFirefoxComponent.new)
    #   refute_component_rendered
    # end
    # ```
    #
    # @return [ActionDispatch::TestRequest]
    def vc_test_request
      require "action_controller/test_case"

      @vc_test_request ||=
        begin
          out = ActionDispatch::TestRequest.create
          out.session = ActionController::TestSession.new
          out
        end
    end

    # Note: We prefix private methods here to prevent collisions in consumer's tests.
    private

    def __vc_test_helpers_build_controller(klass)
      klass.new.tap { |c| c.request = vc_test_request }.extend(Rails.application.routes.url_helpers)
    end

    def __vc_test_helpers_preview_class
      result = if respond_to?(:described_class)
        raise ArgumentError.new("`render_preview` expected a described_class, but it is nil.") if described_class.nil?

        "#{described_class}Preview"
      else
        self.class.name.gsub("Test", "Preview")
      end
      result = result.constantize
    rescue NameError
      raise NameError, "`render_preview` expected to find #{result}, but it does not exist."
    end
  end
end