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
|