rspec-rails 3.8.2 → 7.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/Capybara.md +5 -54
  4. data/Changelog.md +440 -76
  5. data/README.md +281 -500
  6. data/lib/generators/rspec/channel/channel_generator.rb +12 -0
  7. data/lib/generators/rspec/{observer/templates/observer_spec.rb → channel/templates/channel_spec.rb.erb} +1 -1
  8. data/lib/generators/rspec/controller/controller_generator.rb +24 -7
  9. data/lib/generators/rspec/controller/templates/request_spec.rb +19 -0
  10. data/lib/generators/rspec/controller/templates/routing_spec.rb +13 -0
  11. data/lib/generators/rspec/feature/feature_generator.rb +3 -3
  12. data/lib/generators/rspec/generator/generator_generator.rb +24 -0
  13. data/lib/generators/rspec/generator/templates/generator_spec.rb +5 -0
  14. data/lib/generators/rspec/helper/helper_generator.rb +2 -2
  15. data/lib/generators/rspec/install/install_generator.rb +23 -6
  16. data/lib/generators/rspec/install/templates/spec/rails_helper.rb +32 -17
  17. data/lib/generators/rspec/job/job_generator.rb +2 -1
  18. data/lib/generators/rspec/job/templates/job_spec.rb.erb +1 -1
  19. data/lib/generators/rspec/mailbox/mailbox_generator.rb +14 -0
  20. data/lib/generators/rspec/mailbox/templates/mailbox_spec.rb.erb +7 -0
  21. data/lib/generators/rspec/mailer/mailer_generator.rb +7 -4
  22. data/lib/generators/rspec/mailer/templates/mailer_spec.rb +2 -2
  23. data/lib/generators/rspec/mailer/templates/preview.rb +4 -4
  24. data/lib/generators/rspec/model/model_generator.rb +8 -7
  25. data/lib/generators/rspec/model/templates/fixtures.yml +1 -1
  26. data/lib/generators/rspec/request/request_generator.rb +10 -3
  27. data/lib/generators/rspec/scaffold/scaffold_generator.rb +36 -22
  28. data/lib/generators/rspec/scaffold/templates/api_controller_spec.rb +13 -49
  29. data/lib/generators/rspec/scaffold/templates/api_request_spec.rb +131 -0
  30. data/lib/generators/rspec/scaffold/templates/controller_spec.rb +14 -62
  31. data/lib/generators/rspec/scaffold/templates/edit_spec.rb +9 -9
  32. data/lib/generators/rspec/scaffold/templates/index_spec.rb +3 -2
  33. data/lib/generators/rspec/scaffold/templates/new_spec.rb +2 -6
  34. data/lib/generators/rspec/scaffold/templates/request_spec.rb +138 -0
  35. data/lib/generators/rspec/scaffold/templates/routing_spec.rb +8 -10
  36. data/lib/generators/rspec/scaffold/templates/show_spec.rb +2 -2
  37. data/lib/generators/rspec/system/system_generator.rb +24 -0
  38. data/lib/generators/rspec/system/templates/system_spec.rb +9 -0
  39. data/lib/generators/rspec/view/view_generator.rb +4 -4
  40. data/lib/generators/rspec.rb +16 -5
  41. data/lib/rspec/rails/adapters.rb +22 -76
  42. data/lib/rspec/rails/configuration.rb +112 -38
  43. data/lib/rspec/rails/example/channel_example_group.rb +93 -0
  44. data/lib/rspec/rails/example/controller_example_group.rb +5 -4
  45. data/lib/rspec/rails/example/feature_example_group.rb +6 -26
  46. data/lib/rspec/rails/example/helper_example_group.rb +2 -9
  47. data/lib/rspec/rails/example/mailbox_example_group.rb +80 -0
  48. data/lib/rspec/rails/example/mailer_example_group.rb +2 -2
  49. data/lib/rspec/rails/example/rails_example_group.rb +7 -1
  50. data/lib/rspec/rails/example/request_example_group.rb +1 -4
  51. data/lib/rspec/rails/example/routing_example_group.rb +0 -2
  52. data/lib/rspec/rails/example/system_example_group.rb +88 -16
  53. data/lib/rspec/rails/example/view_example_group.rb +40 -28
  54. data/lib/rspec/rails/example.rb +2 -0
  55. data/lib/rspec/rails/extensions/active_record/proxy.rb +5 -10
  56. data/lib/rspec/rails/feature_check.rb +16 -29
  57. data/lib/rspec/rails/file_fixture_support.rb +11 -10
  58. data/lib/rspec/rails/fixture_file_upload_support.rb +20 -15
  59. data/lib/rspec/rails/fixture_support.rb +64 -34
  60. data/lib/rspec/rails/matchers/action_cable/have_broadcasted_to.rb +173 -0
  61. data/lib/rspec/rails/matchers/action_cable/have_streams.rb +58 -0
  62. data/lib/rspec/rails/matchers/action_cable.rb +65 -0
  63. data/lib/rspec/rails/matchers/action_mailbox.rb +73 -0
  64. data/lib/rspec/rails/matchers/active_job.rb +224 -24
  65. data/lib/rspec/rails/matchers/base_matcher.rb +179 -0
  66. data/lib/rspec/rails/matchers/be_a_new.rb +1 -1
  67. data/lib/rspec/rails/matchers/be_new_record.rb +1 -1
  68. data/lib/rspec/rails/matchers/be_valid.rb +1 -1
  69. data/lib/rspec/rails/matchers/have_enqueued_mail.rb +258 -0
  70. data/lib/rspec/rails/matchers/have_http_status.rb +23 -32
  71. data/lib/rspec/rails/matchers/have_rendered.rb +2 -1
  72. data/lib/rspec/rails/matchers/redirect_to.rb +1 -1
  73. data/lib/rspec/rails/matchers/relation_match_array.rb +1 -1
  74. data/lib/rspec/rails/matchers/routing_matchers.rb +13 -13
  75. data/lib/rspec/rails/matchers/send_email.rb +122 -0
  76. data/lib/rspec/rails/matchers.rb +12 -0
  77. data/lib/rspec/rails/tasks/rspec.rake +9 -17
  78. data/lib/rspec/rails/vendor/capybara.rb +10 -17
  79. data/lib/rspec/rails/version.rb +1 -1
  80. data/lib/rspec/rails/view_assigns.rb +0 -18
  81. data/lib/rspec/rails/view_path_builder.rb +1 -1
  82. data/lib/rspec/rails/view_rendering.rb +20 -7
  83. data/lib/rspec-rails.rb +36 -18
  84. data.tar.gz.sig +0 -0
  85. metadata +55 -37
  86. metadata.gz.sig +0 -0
  87. data/lib/generators/rspec/integration/integration_generator.rb +0 -22
  88. data/lib/generators/rspec/observer/observer_generator.rb +0 -13
  89. /data/lib/generators/rspec/{integration → request}/templates/request_spec.rb +0 -0
@@ -0,0 +1,258 @@
1
+ # We require the minimum amount of rspec-mocks possible to avoid
2
+ # conflicts with other mocking frameworks.
3
+ # See: https://github.com/rspec/rspec-rails/issues/2252
4
+ require "rspec/mocks/argument_matchers"
5
+ require "rspec/rails/matchers/active_job"
6
+
7
+ # rubocop: disable Metrics/ClassLength
8
+ module RSpec
9
+ module Rails
10
+ module Matchers
11
+ # Matcher class for `have_enqueued_mail`. Should not be instantiated directly.
12
+ #
13
+ # @private
14
+ # @see RSpec::Rails::Matchers#have_enqueued_mail
15
+ class HaveEnqueuedMail < ActiveJob::HaveEnqueuedJob
16
+ MAILER_JOB_METHOD = 'deliver_now'.freeze
17
+
18
+ include RSpec::Mocks::ArgumentMatchers
19
+
20
+ def initialize(mailer_class, method_name)
21
+ super(nil)
22
+ @mailer_class = mailer_class
23
+ @method_name = method_name
24
+ @mail_args = []
25
+ end
26
+
27
+ def description
28
+ "enqueues #{mailer_class_name}.#{@method_name}"
29
+ end
30
+
31
+ def with(*args, &block)
32
+ @mail_args = args
33
+ block.nil? ? super : super(&yield_mail_args(block))
34
+ end
35
+
36
+ def matches?(block)
37
+ raise ArgumentError, 'have_enqueued_mail and enqueue_mail only work with block arguments' unless block.respond_to?(:call)
38
+
39
+ check_active_job_adapter
40
+ super
41
+ end
42
+
43
+ def failure_message
44
+ return @failure_message if defined?(@failure_message)
45
+
46
+ "expected to enqueue #{base_message}".tap do |msg|
47
+ msg << "\n#{unmatching_mail_jobs_message}" if unmatching_mail_jobs.any?
48
+ end
49
+ end
50
+
51
+ def failure_message_when_negated
52
+ "expected not to enqueue #{base_message}"
53
+ end
54
+
55
+ private
56
+
57
+ def base_message
58
+ [mailer_class_name, @method_name].compact.join('.').tap do |msg|
59
+ msg << " #{expected_count_message}"
60
+ msg << " with #{@mail_args}," if @mail_args.any?
61
+ msg << " on queue #{@queue}," if @queue
62
+ msg << " at #{@at.inspect}," if @at
63
+ msg << " but enqueued #{@matching_jobs.size}"
64
+ end
65
+ end
66
+
67
+ def expected_count_message
68
+ "#{message_expectation_modifier} #{@expected_number} #{@expected_number == 1 ? 'time' : 'times'}"
69
+ end
70
+
71
+ def mailer_class_name
72
+ @mailer_class ? @mailer_class.name : 'ActionMailer::Base'
73
+ end
74
+
75
+ def job_match?(job)
76
+ legacy_mail?(job) || parameterized_mail?(job) || unified_mail?(job)
77
+ end
78
+
79
+ def arguments_match?(job)
80
+ @args =
81
+ if @mail_args.any?
82
+ base_mailer_args + @mail_args
83
+ elsif @mailer_class && @method_name
84
+ base_mailer_args + [any_args]
85
+ elsif @mailer_class
86
+ [mailer_class_name, any_args]
87
+ else
88
+ []
89
+ end
90
+
91
+ super(job)
92
+ end
93
+
94
+ def detect_args_signature_mismatch(jobs)
95
+ return if @method_name.nil?
96
+
97
+ mailer_class = mailer_class_name.constantize
98
+
99
+ jobs.each do |job|
100
+ mailer_args = extract_args_without_parameterized_params(job)
101
+
102
+ if (signature_mismatch = check_args_signature_mismatch(mailer_class, @method_name, mailer_args))
103
+ return signature_mismatch
104
+ end
105
+ end
106
+
107
+ nil
108
+ end
109
+
110
+ def base_mailer_args
111
+ [mailer_class_name, @method_name.to_s, MAILER_JOB_METHOD]
112
+ end
113
+
114
+ def yield_mail_args(block)
115
+ proc { |*job_args| block.call(*(job_args - base_mailer_args)) }
116
+ end
117
+
118
+ def check_active_job_adapter
119
+ return if ::ActiveJob::QueueAdapters::TestAdapter === ::ActiveJob::Base.queue_adapter
120
+
121
+ raise StandardError, "To use HaveEnqueuedMail matcher set `ActiveJob::Base.queue_adapter = :test`"
122
+ end
123
+
124
+ def unmatching_mail_jobs
125
+ @unmatching_jobs.select do |job|
126
+ job_match?(job)
127
+ end
128
+ end
129
+
130
+ def unmatching_mail_jobs_message
131
+ msg = "Queued deliveries:"
132
+
133
+ unmatching_mail_jobs.each do |job|
134
+ msg << "\n #{mail_job_message(job)}"
135
+ end
136
+
137
+ msg
138
+ end
139
+
140
+ def mail_job_message(job)
141
+ job_args = deserialize_arguments(job)
142
+
143
+ mailer_method = job_args[0..1].join('.')
144
+ mailer_args = job_args[3..-1]
145
+
146
+ msg_parts = []
147
+ msg_parts << "with #{mailer_args}" if mailer_args.any?
148
+ msg_parts << "on queue #{job[:queue]}" if job[:queue] && job[:queue] != 'mailers'
149
+ msg_parts << "at #{Time.at(job[:at])}" if job[:at]
150
+
151
+ "#{mailer_method} #{msg_parts.join(', ')}".strip
152
+ end
153
+
154
+ # Ruby 3.1 changed how params were serialized on Rails 6.1
155
+ # so we override the active job implementation and customize it here.
156
+ def deserialize_arguments(job)
157
+ args = super
158
+
159
+ return args unless Hash === args.last
160
+
161
+ hash = args.pop
162
+
163
+ if hash.key?("_aj_ruby2_keywords")
164
+ keywords = hash["_aj_ruby2_keywords"]
165
+
166
+ original_hash = keywords.each_with_object({}) { |keyword, new_hash| new_hash[keyword.to_sym] = hash[keyword] }
167
+
168
+ args + [original_hash]
169
+ elsif hash.key?(:args) && hash.key?(:params)
170
+ args + [hash]
171
+ elsif hash.key?(:args)
172
+ args + hash[:args]
173
+ else
174
+ args + [hash]
175
+ end
176
+ end
177
+
178
+ def extract_args_without_parameterized_params(job)
179
+ args = deserialize_arguments(job)
180
+ mailer_args = args - base_mailer_args
181
+
182
+ if parameterized_mail?(job)
183
+ mailer_args = mailer_args[1..-1] # ignore parameterized params
184
+ elsif mailer_args.last.is_a?(Hash) && mailer_args.last.key?(:args)
185
+ mailer_args = args.last[:args]
186
+ end
187
+
188
+ mailer_args
189
+ end
190
+
191
+ def legacy_mail?(job)
192
+ RSpec::Rails::FeatureCheck.has_action_mailer_legacy_delivery_job? && job[:job] <= ActionMailer::DeliveryJob
193
+ end
194
+
195
+ def parameterized_mail?(job)
196
+ RSpec::Rails::FeatureCheck.has_action_mailer_parameterized? && job[:job] <= ActionMailer::Parameterized::DeliveryJob
197
+ end
198
+
199
+ def unified_mail?(job)
200
+ RSpec::Rails::FeatureCheck.has_action_mailer_unified_delivery? && job[:job] <= ActionMailer::MailDeliveryJob
201
+ end
202
+ end
203
+
204
+ # @api public
205
+ # Passes if an email has been enqueued inside block.
206
+ # May chain with to specify expected arguments.
207
+ # May chain at_least, at_most or exactly to specify a number of times.
208
+ # May chain at to specify a send time.
209
+ # May chain on_queue to specify a queue.
210
+ #
211
+ # @example
212
+ # expect {
213
+ # MyMailer.welcome(user).deliver_later
214
+ # }.to have_enqueued_mail
215
+ #
216
+ # expect {
217
+ # MyMailer.welcome(user).deliver_later
218
+ # }.to have_enqueued_mail(MyMailer)
219
+ #
220
+ # expect {
221
+ # MyMailer.welcome(user).deliver_later
222
+ # }.to have_enqueued_mail(MyMailer, :welcome)
223
+ #
224
+ # # Using alias
225
+ # expect {
226
+ # MyMailer.welcome(user).deliver_later
227
+ # }.to enqueue_mail(MyMailer, :welcome)
228
+ #
229
+ # expect {
230
+ # MyMailer.welcome(user).deliver_later
231
+ # }.to have_enqueued_mail(MyMailer, :welcome).with(user)
232
+ #
233
+ # expect {
234
+ # MyMailer.welcome(user).deliver_later
235
+ # MyMailer.welcome(user).deliver_later
236
+ # }.to have_enqueued_mail(MyMailer, :welcome).at_least(:once)
237
+ #
238
+ # expect {
239
+ # MyMailer.welcome(user).deliver_later
240
+ # }.to have_enqueued_mail(MyMailer, :welcome).at_most(:twice)
241
+ #
242
+ # expect {
243
+ # MyMailer.welcome(user).deliver_later(wait_until: Date.tomorrow.noon)
244
+ # }.to have_enqueued_mail(MyMailer, :welcome).at(Date.tomorrow.noon)
245
+ #
246
+ # expect {
247
+ # MyMailer.welcome(user).deliver_later(queue: :urgent_mail)
248
+ # }.to have_enqueued_mail(MyMailer, :welcome).on_queue(:urgent_mail)
249
+ def have_enqueued_mail(mailer_class = nil, mail_method_name = nil)
250
+ HaveEnqueuedMail.new(mailer_class, mail_method_name)
251
+ end
252
+ alias_method :have_enqueued_email, :have_enqueued_mail
253
+ alias_method :enqueue_mail, :have_enqueued_mail
254
+ alias_method :enqueue_email, :have_enqueued_mail
255
+ end
256
+ end
257
+ end
258
+ # rubocop: enable Metrics/ClassLength
@@ -1,6 +1,6 @@
1
1
  # The following code inspired and modified from Rails' `assert_response`:
2
2
  #
3
- # https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/assertions/response.rb#L22-L38
3
+ # https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/testing/assertions/response.rb#L22-L38
4
4
  #
5
5
  # Thank you to all the Rails devs who did the heavy lifting on this!
6
6
 
@@ -33,7 +33,7 @@ module RSpec
33
33
  # @param obj [Object] object to convert to a response
34
34
  # @return [ActionDispatch::TestResponse]
35
35
  def as_test_response(obj)
36
- if ::ActionDispatch::Response === obj
36
+ if ::ActionDispatch::Response === obj || ::Rack::MockResponse === obj
37
37
  ::ActionDispatch::TestResponse.from_response(obj)
38
38
  elsif ::ActionDispatch::TestResponse === obj
39
39
  obj
@@ -59,6 +59,7 @@ module RSpec
59
59
  # `@invalid_response` is present, `nil` otherwise
60
60
  def invalid_response_type_message
61
61
  return unless @invalid_response
62
+
62
63
  "expected a response object, but an instance of " \
63
64
  "#{@invalid_response.class} was received"
64
65
  end
@@ -72,8 +73,8 @@ module RSpec
72
73
  # @example
73
74
  # expect(response).to have_http_status(404)
74
75
  #
75
- # @see RSpec::Rails::Matchers.have_http_status
76
- class NumericCode < RSpec::Matchers::BuiltIn::BaseMatcher
76
+ # @see RSpec::Rails::Matchers#have_http_status
77
+ class NumericCode < RSpec::Rails::Matchers::BaseMatcher
77
78
  include HaveHttpStatus
78
79
 
79
80
  def initialize(code)
@@ -122,9 +123,9 @@ module RSpec
122
123
  # @example
123
124
  # expect(response).to have_http_status(:created)
124
125
  #
125
- # @see RSpec::Rails::Matchers.have_http_status
126
+ # @see RSpec::Rails::Matchers#have_http_status
126
127
  # @see https://github.com/rack/rack/blob/master/lib/rack/utils.rb `Rack::Utils::SYMBOL_TO_STATUS_CODE`
127
- class SymbolicStatus < RSpec::Matchers::BuiltIn::BaseMatcher
128
+ class SymbolicStatus < RSpec::Rails::Matchers::BaseMatcher
128
129
  include HaveHttpStatus
129
130
 
130
131
  def initialize(status)
@@ -174,6 +175,7 @@ module RSpec
174
175
  # @return [Symbol] representing the actual http numeric code
175
176
  def actual_status
176
177
  return unless actual
178
+
177
179
  @actual_status ||= compute_status_from(actual)
178
180
  end
179
181
 
@@ -214,11 +216,7 @@ module RSpec
214
216
  # @see Rack::Utils::SYMBOL_TO_STATUS_CODE
215
217
  # @raise [ArgumentError] if an associated code could not be found
216
218
  def set_expected_code!
217
- @expected ||=
218
- Rack::Utils::SYMBOL_TO_STATUS_CODE.fetch(expected_status) do
219
- raise ArgumentError,
220
- "Invalid HTTP status: #{expected_status.inspect}"
221
- end
219
+ @expected ||= Rack::Utils.status_code(expected_status)
222
220
  end
223
221
  end
224
222
 
@@ -234,14 +232,14 @@ module RSpec
234
232
  # expect(response).to have_http_status(:missing)
235
233
  # expect(response).to have_http_status(:redirect)
236
234
  #
237
- # @see RSpec::Rails::Matchers.have_http_status
238
- # @see ActionDispatch::TestResponse
239
- class GenericStatus < RSpec::Matchers::BuiltIn::BaseMatcher
235
+ # @see RSpec::Rails::Matchers#have_http_status
236
+ # @see https://github.com/rails/rails/blob/7-2-stable/actionpack/lib/action_dispatch/testing/test_response.rb `ActionDispatch::TestResponse`
237
+ class GenericStatus < RSpec::Rails::Matchers::BaseMatcher
240
238
  include HaveHttpStatus
241
239
 
242
240
  # @return [Array<Symbol>] of status codes which represent a HTTP status
243
241
  # code "group"
244
- # @see https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/test_response.rb `ActionDispatch::TestResponse`
242
+ # @see https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/testing/test_response.rb `ActionDispatch::TestResponse`
245
243
  def self.valid_statuses
246
244
  [
247
245
  :error, :success, :missing,
@@ -254,6 +252,7 @@ module RSpec
254
252
  unless self.class.valid_statuses.include?(type)
255
253
  raise ArgumentError, "Invalid generic HTTP status: #{type.inspect}"
256
254
  end
255
+
257
256
  @expected = type
258
257
  @actual = nil
259
258
  @invalid_response = nil
@@ -289,20 +288,11 @@ module RSpec
289
288
 
290
289
  protected
291
290
 
292
- if 5 < ::Rails::VERSION::MAJOR ||
293
- (::Rails::VERSION::MAJOR == 5 && 2 <= ::Rails::VERSION::MINOR)
294
- RESPONSE_METHODS = {
295
- :success => 'successful',
296
- :error => 'server_error',
297
- :missing => 'not_found'
298
- }.freeze
299
- else
300
- RESPONSE_METHODS = {
301
- :successful => 'success',
302
- :server_error => 'error',
303
- :not_found => 'missing'
304
- }.freeze
305
- end
291
+ RESPONSE_METHODS = {
292
+ success: 'successful',
293
+ error: 'server_error',
294
+ missing: 'not_found'
295
+ }.freeze
306
296
 
307
297
  def check_expected_status(test_response, expected)
308
298
  test_response.send(
@@ -311,7 +301,7 @@ module RSpec
311
301
 
312
302
  private
313
303
 
314
- # @return [String] formating the expected status and associated code(s)
304
+ # @return [String] formatting the expected status and associated code(s)
315
305
  def type_message
316
306
  @type_message ||= (expected == :error ? "an error" : "a #{expected}") +
317
307
  " status code (#{type_codes})"
@@ -319,7 +309,7 @@ module RSpec
319
309
 
320
310
  # @return [String] formatting the associated code(s) for the various
321
311
  # status code "groups"
322
- # @see https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/test_response.rb `ActionDispatch::TestResponse`
312
+ # @see https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/testing/test_response.rb `ActionDispatch::TestResponse`
323
313
  # @see https://github.com/rack/rack/blob/master/lib/rack/response.rb `Rack::Response`
324
314
  def type_codes
325
315
  # At the time of this commit the most recent version of
@@ -379,10 +369,11 @@ module RSpec
379
369
  # expect(response).to have_http_status(404)
380
370
  # expect(page).to have_http_status(:created)
381
371
  #
382
- # @see https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/test_response.rb `ActionDispatch::TestResponse`
372
+ # @see https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/testing/test_response.rb `ActionDispatch::TestResponse`
383
373
  # @see https://github.com/rack/rack/blob/master/lib/rack/utils.rb `Rack::Utils::SYMBOL_TO_STATUS_CODE`
384
374
  def have_http_status(target)
385
375
  raise ArgumentError, "Invalid HTTP status: nil" unless target
376
+
386
377
  HaveHttpStatus.matcher_for_status(target)
387
378
  end
388
379
  end
@@ -4,7 +4,7 @@ module RSpec
4
4
  # Matcher for template rendering.
5
5
  module RenderTemplate
6
6
  # @private
7
- class RenderTemplateMatcher < RSpec::Matchers::BuiltIn::BaseMatcher
7
+ class RenderTemplateMatcher < RSpec::Rails::Matchers::BaseMatcher
8
8
  def initialize(scope, expected, message = nil)
9
9
  @expected = Symbol === expected ? expected.to_s : expected
10
10
  @message = message
@@ -29,6 +29,7 @@ module RSpec
29
29
  def check_redirect
30
30
  response = @scope.response
31
31
  return unless response.respond_to?(:redirect?) && response.redirect?
32
+
32
33
  @redirect_is = @scope.send(:normalize_argument_to_redirection, response.location)
33
34
  end
34
35
 
@@ -4,7 +4,7 @@ module RSpec
4
4
  # Matcher for redirects.
5
5
  module RedirectTo
6
6
  # @private
7
- class RedirectTo < RSpec::Matchers::BuiltIn::BaseMatcher
7
+ class RedirectTo < RSpec::Rails::Matchers::BaseMatcher
8
8
  def initialize(scope, expected)
9
9
  @expected = expected
10
10
  @scope = scope
@@ -1,3 +1,3 @@
1
- if defined?(ActiveRecord::Relation)
1
+ if defined?(ActiveRecord::Relation) && defined?(RSpec::Matchers::BuiltIn::OperatorMatcher) # RSpec 4 removed OperatorMatcher
2
2
  RSpec::Matchers::BuiltIn::OperatorMatcher.register(ActiveRecord::Relation, '=~', RSpec::Matchers::BuiltIn::ContainExactly)
3
3
  end
@@ -6,7 +6,7 @@ module RSpec
6
6
  extend RSpec::Matchers::DSL
7
7
 
8
8
  # @private
9
- class RouteToMatcher < RSpec::Matchers::BuiltIn::BaseMatcher
9
+ class RouteToMatcher < RSpec::Rails::Matchers::BaseMatcher
10
10
  def initialize(scope, *expected)
11
11
  @scope = scope
12
12
  @expected = expected[1] || {}
@@ -14,7 +14,7 @@ module RSpec
14
14
  @expected.merge!(expected[0])
15
15
  else
16
16
  controller, action = expected[0].split('#')
17
- @expected.merge!(:controller => controller, :action => action)
17
+ @expected.merge!(controller: controller, action: action)
18
18
  end
19
19
  end
20
20
 
@@ -26,7 +26,7 @@ module RSpec
26
26
  path, query = *verb_to_path_map.values.first.split('?')
27
27
  @scope.assert_recognizes(
28
28
  @expected,
29
- { :method => verb_to_path_map.keys.first, :path => path },
29
+ { method: verb_to_path_map.keys.first, path: path },
30
30
  Rack::Utils.parse_nested_query(query)
31
31
  )
32
32
  end
@@ -50,20 +50,20 @@ module RSpec
50
50
  #
51
51
  # @example
52
52
  #
53
- # expect(:get => "/things/special").to route_to(
54
- # :controller => "things",
55
- # :action => "special"
53
+ # expect(get: "/things/special").to route_to(
54
+ # controller: "things",
55
+ # action: "special"
56
56
  # )
57
57
  #
58
- # expect(:get => "/things/special").to route_to("things#special")
58
+ # expect(get: "/things/special").to route_to("things#special")
59
59
  #
60
- # @see http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html#method-i-assert_recognizes
60
+ # @see https://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html#method-i-assert_recognizes
61
61
  def route_to(*expected)
62
62
  RouteToMatcher.new(self, *expected)
63
63
  end
64
64
 
65
65
  # @private
66
- class BeRoutableMatcher < RSpec::Matchers::BuiltIn::BaseMatcher
66
+ class BeRoutableMatcher < RSpec::Rails::Matchers::BaseMatcher
67
67
  def initialize(scope)
68
68
  @scope = scope
69
69
  end
@@ -72,7 +72,7 @@ module RSpec
72
72
  @actual = path
73
73
  match_unless_raises ActionController::RoutingError do
74
74
  @routing_options = @scope.routes.recognize_path(
75
- path.values.first, :method => path.keys.first
75
+ path.values.first, method: path.keys.first
76
76
  )
77
77
  end
78
78
  end
@@ -95,9 +95,9 @@ module RSpec
95
95
  # `RouteSet#recognize_path`.
96
96
  #
97
97
  # @example You can use route helpers provided by rspec-rails.
98
- # expect(:get => "/a/path").to be_routable
99
- # expect(:post => "/another/path").to be_routable
100
- # expect(:put => "/yet/another/path").to be_routable
98
+ # expect(get: "/a/path").to be_routable
99
+ # expect(post: "/another/path").to be_routable
100
+ # expect(put: "/yet/another/path").to be_routable
101
101
  def be_routable
102
102
  BeRoutableMatcher.new(self)
103
103
  end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Rails
5
+ module Matchers
6
+ # @api private
7
+ #
8
+ # Matcher class for `send_email`. Should not be instantiated directly.
9
+ #
10
+ # @see RSpec::Rails::Matchers#send_email
11
+ class SendEmail < RSpec::Rails::Matchers::BaseMatcher
12
+ # @api private
13
+ # Define the email attributes that should be included in the inspection output.
14
+ INSPECT_EMAIL_ATTRIBUTES = %i[subject from to cc bcc].freeze
15
+
16
+ def initialize(criteria)
17
+ @criteria = criteria
18
+ end
19
+
20
+ # @api private
21
+ def supports_value_expectations?
22
+ false
23
+ end
24
+
25
+ # @api private
26
+ def supports_block_expectations?
27
+ true
28
+ end
29
+
30
+ def matches?(block)
31
+ define_matched_emails(block)
32
+
33
+ @matched_emails.one?
34
+ end
35
+
36
+ # @api private
37
+ # @return [String]
38
+ def failure_message
39
+ result =
40
+ if multiple_match?
41
+ "More than 1 matching emails were sent."
42
+ else
43
+ "No matching emails were sent."
44
+ end
45
+ "#{result}#{sent_emails_message}"
46
+ end
47
+
48
+ # @api private
49
+ # @return [String]
50
+ def failure_message_when_negated
51
+ "Expected not to send an email but it was sent."
52
+ end
53
+
54
+ private
55
+
56
+ def diffable?
57
+ true
58
+ end
59
+
60
+ def deliveries
61
+ ActionMailer::Base.deliveries
62
+ end
63
+
64
+ def define_matched_emails(block)
65
+ before = deliveries.dup
66
+
67
+ block.call
68
+
69
+ after = deliveries
70
+
71
+ @diff = after - before
72
+ @matched_emails = @diff.select(&method(:matched_email?))
73
+ end
74
+
75
+ def matched_email?(email)
76
+ @criteria.all? do |attr, value|
77
+ expected =
78
+ case attr
79
+ when :to, :from, :cc, :bcc then Array(value)
80
+ else
81
+ value
82
+ end
83
+
84
+ values_match?(expected, email.public_send(attr))
85
+ end
86
+ end
87
+
88
+ def multiple_match?
89
+ @matched_emails.many?
90
+ end
91
+
92
+ def sent_emails_message
93
+ if @diff.empty?
94
+ "\n\nThere were no any emails sent inside the expectation block."
95
+ else
96
+ sent_emails =
97
+ @diff.map do |email|
98
+ inspected = INSPECT_EMAIL_ATTRIBUTES.map { |attr| "#{attr}: #{email.public_send(attr)}" }.join(", ")
99
+ "- #{inspected}"
100
+ end.join("\n")
101
+ "\n\nThe following emails were sent:\n#{sent_emails}"
102
+ end
103
+ end
104
+ end
105
+
106
+ # @api public
107
+ # Check email sending with specific parameters.
108
+ #
109
+ # @example Positive expectation
110
+ # expect { action }.to send_email
111
+ #
112
+ # @example Negative expectations
113
+ # expect { action }.not_to send_email
114
+ #
115
+ # @example More precise expectation with attributes to match
116
+ # expect { action }.to send_email(to: 'test@example.com', subject: 'Confirm email')
117
+ def send_email(criteria = {})
118
+ SendEmail.new(criteria)
119
+ end
120
+ end
121
+ end
122
+ end
@@ -11,6 +11,7 @@ module RSpec
11
11
  end
12
12
  end
13
13
 
14
+ require 'rspec/rails/matchers/base_matcher'
14
15
  require 'rspec/rails/matchers/have_rendered'
15
16
  require 'rspec/rails/matchers/redirect_to'
16
17
  require 'rspec/rails/matchers/routing_matchers'
@@ -19,6 +20,17 @@ require 'rspec/rails/matchers/be_a_new'
19
20
  require 'rspec/rails/matchers/relation_match_array'
20
21
  require 'rspec/rails/matchers/be_valid'
21
22
  require 'rspec/rails/matchers/have_http_status'
23
+ require 'rspec/rails/matchers/send_email'
24
+
22
25
  if RSpec::Rails::FeatureCheck.has_active_job?
23
26
  require 'rspec/rails/matchers/active_job'
27
+ require 'rspec/rails/matchers/have_enqueued_mail'
28
+ end
29
+
30
+ if RSpec::Rails::FeatureCheck.has_action_cable_testing?
31
+ require 'rspec/rails/matchers/action_cable'
32
+ end
33
+
34
+ if RSpec::Rails::FeatureCheck.has_action_mailbox?
35
+ require 'rspec/rails/matchers/action_mailbox'
24
36
  end