Skip to content

Commit 809b526

Browse files
committed
Look through all errors, don't just go with last error
1 parent 7b3abc5 commit 809b526

File tree

2 files changed

+67
-26
lines changed

2 files changed

+67
-26
lines changed

lib/rspec/rails/matchers/have_reported_error.rb

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,22 @@ def report(error, **attrs)
2626
# Matcher class for `have_reported_error`. Should not be instantiated directly.
2727
#
2828
# Provides a way to test that an error was reported to Rails.error.
29-
# This matcher follows the same patterns as RSpec's built-in `raise_error` matcher.
3029
#
3130
# @api private
3231
# @see RSpec::Rails::Matchers#have_reported_error
3332
class HaveReportedError < RSpec::Rails::Matchers::BaseMatcher
34-
# Initialize the matcher following raise_error patterns
33+
# Uses UndefinedValue as default to distinguish between no argument
34+
# passed vs explicitly passed nil.
3535
#
36-
# Uses UndefinedValue as default to distinguish between no argument
37-
# passed vs explicitly passed nil (same as raise_error matcher).
38-
#
39-
# @param expected_error_or_message [Class, String, Regexp, nil]
36+
# @param expected_error_or_message [Class, String, Regexp, nil]
4037
# Error class, message string, or message pattern
41-
# @param expected_message [String, Regexp, nil]
38+
# @param expected_message [String, Regexp, nil]
4239
# Expected message when first param is a class
4340
def initialize(expected_error_or_message = UndefinedValue, expected_message = nil)
4441
@actual_error = nil
4542
@attributes = {}
4643
@error_subscriber = nil
47-
44+
4845
case expected_error_or_message
4946
when nil, UndefinedValue
5047
@expected_error = nil
@@ -67,7 +64,6 @@ def and(_)
6764
raise ArgumentError, "Chaining is not supported"
6865
end
6966

70-
# Check if the block reports an error matching our expectations
7167
def matches?(block)
7268
if block.nil?
7369
raise ArgumentError, "this matcher doesn't work with value expectations"
@@ -89,6 +85,8 @@ def matches?(block)
8985
def supports_block_expectations?
9086
true
9187
end
88+
89+
9290

9391
def description
9492
base_desc = if @expected_error
@@ -122,6 +120,18 @@ def failure_message
122120
end
123121
elsif @error_subscriber.events.empty?
124122
return 'Expected the block to report an error, but none was reported.'
123+
elsif actual_error.nil?
124+
# Errors were reported but none matched our expectations
125+
reported_errors = @error_subscriber.events.map { |event| "#{event.error.class}: '#{event.error.message}'" }.join(', ')
126+
if @expected_error && @expected_message
127+
return "Expected error to be an instance of #{@expected_error} with message '#{@expected_message}', but got: #{reported_errors}"
128+
elsif @expected_error
129+
return "Expected error to be an instance of #{@expected_error}, but got: #{reported_errors}"
130+
elsif @expected_message.is_a?(Regexp)
131+
return "Expected error message to match #{@expected_message}, but got: #{reported_errors}"
132+
elsif @expected_message.is_a?(String)
133+
return "Expected error message to be '#{@expected_message}', but got: #{reported_errors}"
134+
end
125135
else
126136
if @expected_error && !actual_error.is_a?(@expected_error)
127137
return "Expected error to be an instance of #{@expected_error}, but got #{actual_error.class} with message: '#{actual_error.message}'"
@@ -148,28 +158,30 @@ def failure_message_when_negated
148158

149159
private
150160

151-
# Check if the reported error matches our class and message expectations
161+
# Check if any of the reported errors matches our class and message expectations
152162
def error_matches_expectation?
153163
return false if @error_subscriber.events.empty?
154164
return true if @expected_error.nil? && @expected_message.nil?
155165

156-
error_class_matches? && error_message_matches?
166+
@error_subscriber.events.any? do |event|
167+
error_class_matches?(event.error) && error_message_matches?(event.error)
168+
end
157169
end
158170

159-
# Check if the actual error class matches the expected error class
160-
def error_class_matches?
161-
@expected_error.nil? || actual_error.is_a?(@expected_error)
171+
# Check if the given error class matches the expected error class
172+
def error_class_matches?(error)
173+
@expected_error.nil? || error.is_a?(@expected_error)
162174
end
163175

164-
# Check if the actual error message matches the expected message pattern
165-
def error_message_matches?
176+
# Check if the given error message matches the expected message pattern
177+
def error_message_matches?(error)
166178
return true if @expected_message.nil?
167179

168180
case @expected_message
169181
when Regexp
170-
actual_error.message&.match(@expected_message)
182+
error.message&.match(@expected_message)
171183
when String
172-
actual_error.message == @expected_message
184+
error.message == @expected_message
173185
else
174186
false
175187
end
@@ -179,13 +191,42 @@ def attributes_match_if_specified?
179191
return true if @attributes.empty?
180192
return false if @error_subscriber.events.empty?
181193

182-
event_context = @error_subscriber.events.last.attributes[:context]
194+
matching_event = find_matching_event
195+
return false unless matching_event
196+
197+
event_context = matching_event.attributes[:context]
183198
attributes_match?(event_context)
184199
end
185200

186-
# Get the actual error that was reported (cached)
201+
# Get the first matching error from all reported events (cached)
187202
def actual_error
188-
@actual_error ||= (@error_subscriber.events.empty? ? nil : @error_subscriber.events.last.error)
203+
@actual_error ||= find_matching_error
204+
end
205+
206+
# Find the first error that matches our expectations
207+
def find_matching_error
208+
return nil if @error_subscriber.events.empty?
209+
210+
if @expected_error.nil? && @expected_message.nil?
211+
return @error_subscriber.events.first.error
212+
end
213+
214+
@error_subscriber.events.find do |event|
215+
error_class_matches?(event.error) && error_message_matches?(event.error)
216+
end&.error
217+
end
218+
219+
# Find the first event that matches our expectations
220+
def find_matching_event
221+
return nil if @error_subscriber.events.empty?
222+
223+
if @expected_error.nil? && @expected_message.nil?
224+
return @error_subscriber.events.first
225+
end
226+
227+
@error_subscriber.events.find do |event|
228+
error_class_matches?(event.error) && error_message_matches?(event.error)
229+
end
189230
end
190231

191232
def attributes_match?(actual)

spec/rspec/rails/matchers/have_reported_error_spec.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class AnotherTestError < StandardError; end
4848
expect {
4949
Rails.error.report(AnotherTestError.new("wrong error"))
5050
}.to have_reported_error(TestError)
51-
}.to fail_with(/Expected error to be an instance of TestError, but got AnotherTestError/)
51+
}.to fail_with(/Expected error to be an instance of TestError, but got: AnotherTestError/)
5252
end
5353
end
5454

@@ -70,7 +70,7 @@ class AnotherTestError < StandardError; end
7070
expect {
7171
Rails.error.report(TestError.new("actual message"))
7272
}.to have_reported_error(TestError, "expected message")
73-
}.to fail_with(/Expected error message to be 'expected message', but got: 'actual message'/)
73+
}.to fail_with(/Expected error to be an instance of TestError with message 'expected message', but got: TestError/)
7474
end
7575
end
7676

@@ -86,7 +86,7 @@ class AnotherTestError < StandardError; end
8686
expect {
8787
Rails.error.report(StandardError.new("error without match"))
8888
}.to have_reported_error(StandardError, /different pattern/)
89-
}.to fail_with(/Expected error message to match/)
89+
}.to fail_with(/Expected error to be an instance of StandardError with message/)
9090
end
9191
end
9292

@@ -180,15 +180,15 @@ class AnotherTestError < StandardError; end
180180
expect {
181181
Rails.error.report(StandardError.new("actual message"))
182182
}.to have_reported_error("expected message")
183-
}.to fail_with(/Expected error message to be 'expected message', but got: 'actual message'/)
183+
}.to fail_with(/Expected error message to be 'expected message', but got: StandardError/)
184184
end
185185

186186
it "fails when no error with matching pattern is reported" do
187187
expect {
188188
expect {
189189
Rails.error.report(StandardError.new("error without match"))
190190
}.to have_reported_error(/different pattern/)
191-
}.to fail_with(/Expected error message to match/)
191+
}.to fail_with(/Expected error message to match .+different pattern.+, but got: StandardError/)
192192
end
193193
end
194194

0 commit comments

Comments
 (0)