diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index 1238aa72f16..d9c7665997e 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -900,8 +900,8 @@ 'GetUspsProofingResultsJob: Unexpected response received', hash_including( reason: 'Unexpected number of days before enrollment expired', + job_name: 'GetUspsProofingResultsJob', ), - job_name: 'GetUspsProofingResultsJob', ) end end diff --git a/spec/support/fake_analytics.rb b/spec/support/fake_analytics.rb index dc1d8eb3aa7..a0a1acf0c2b 100644 --- a/spec/support/fake_analytics.rb +++ b/spec/support/fake_analytics.rb @@ -180,63 +180,3 @@ def browser_attributes FakeAnalytics::UndocumentedParamsChecker.allowed_extra_analytics = [] end end - -RSpec::Matchers.define :have_logged_event do |event, attributes_matcher| - match do |actual| - if event.nil? && attributes_matcher.nil? - expect(actual.events.count).to be > 0 - elsif attributes_matcher.nil? - expect(actual.events).to have_key(event) - else - expect(actual.events[event]).to include(match(attributes_matcher)) - end - end - - failure_message do |actual| - matching_events = actual.events[event] - if matching_events&.length == 1 && attributes_matcher.instance_of?(Hash) - # We found one matching event. Let's show the user a diff of the actual and expected - # attributes - expected = attributes_matcher - actual = matching_events.first - message = "Expected that FakeAnalytics would have received matching event #{event}\n" - message += "expected: #{expected}\n" - message += " got: #{actual}\n\n" - message += "Diff:#{differ.diff(actual, expected)}" - message - elsif matching_events&.length == 1 && - attributes_matcher.instance_of?(RSpec::Matchers::BuiltIn::Include) - # We found one matching event and an `include` matcher. Let's show the user a diff of the - # actual and expected attributes - expected = attributes_matcher.expecteds.first - actual_attrs = matching_events.first - actual_compared = actual_attrs.slice(*expected.keys) - actual_ignored = actual_attrs.except(*expected.keys) - message = "Expected that FakeAnalytics would have received matching event #{event}" - message += "expected: include #{expected}\n" - message += " got: #{actual_attrs}\n\n" - message += "Diff:#{differ.diff(actual_compared, expected)}\n" - message += "Attributes ignored by the include matcher:#{differ.diff( - actual_ignored, {} - )}" - message - else - <<~MESSAGE - Expected that FakeAnalytics would have received event #{event.inspect} - with #{attributes_matcher.inspect}. - - Events received: - #{actual.events.pretty_inspect} - MESSAGE - end - end - - def differ - RSpec::Support::Differ.new( - object_preparer: lambda do |object| - RSpec::Matchers::Composable.surface_descriptions_in(object) - end, - color: RSpec::Matchers.configuration.color?, - ) - end -end diff --git a/spec/support/fake_analytics_spec.rb b/spec/support/fake_analytics_spec.rb index b5c025fcb2f..e21f1c9894c 100644 --- a/spec/support/fake_analytics_spec.rb +++ b/spec/support/fake_analytics_spec.rb @@ -13,7 +13,6 @@ to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| assert_error_messages_equal(err, <<~MESSAGE) Expected that FakeAnalytics would have received event nil - with nil. Events received: {} @@ -45,7 +44,6 @@ to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| assert_error_messages_equal(err, <<~MESSAGE) Expected that FakeAnalytics would have received event :my_event - with nil. Events received: {} @@ -60,7 +58,6 @@ to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| assert_error_messages_equal(err, <<~MESSAGE) Expected that FakeAnalytics would have received event :my_event - with nil. Events received: {:my_other_event=>[{}]} @@ -81,6 +78,43 @@ expect(&code_under_test). not_to raise_error(RSpec::Expectations::ExpectationNotMetError) end + + describe '.once' do + let(:code_under_test) { -> { expect(analytics).to have_logged_event(:my_event).once } } + + it 'raises if no event has been logged' do + expect(&code_under_test). + to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| + assert_error_messages_equal(err, <<~MESSAGE) + Expected that FakeAnalytics would have received event :my_event once but it was received 0 times + + Events received: + {} + MESSAGE + end + end + + it 'does not raise if event was logged 1x' do + track_event.call + expect(&code_under_test). + not_to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + + it 'raises if event was logged 2x' do + track_event.call + track_event.call + + expect(&code_under_test). + to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| + assert_error_messages_equal(err, <<~MESSAGE) + Expected that FakeAnalytics would have received event :my_event once but it was received twice + + Events received: + {:my_event=>[{}, {}]} + MESSAGE + end + end + end end context 'event name + hash' do @@ -94,7 +128,7 @@ to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| assert_error_messages_equal(err, <<~MESSAGE) Expected that FakeAnalytics would have received event :my_event - with {:arg1=>42}. + with {:arg1=>42} Events received: {} @@ -109,7 +143,7 @@ to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| assert_error_messages_equal(err, <<~MESSAGE) Expected that FakeAnalytics would have received event :my_event - with {:arg1=>42}. + with {:arg1=>42} Events received: {:my_other_event=>[{}]} @@ -123,7 +157,7 @@ expect(&code_under_test). to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| assert_error_messages_equal(err, <<~MESSAGE) - Expected that FakeAnalytics would have received matching event my_event + Expected that FakeAnalytics would have received event :my_event expected: {:arg1=>42} got: {:arg1=>43} @@ -162,6 +196,49 @@ expect(&code_under_test). not_to raise_error(RSpec::Expectations::ExpectationNotMetError) end + + describe '.once' do + let(:code_under_test) do + -> { + expect(analytics).to have_logged_event(:my_event, arg1: 42).once + } + end + + it 'raises if no event has been logged' do + expect(&code_under_test). + to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| + assert_error_messages_equal(err, <<~MESSAGE) + Expected that FakeAnalytics would have received event :my_event once but it was received 0 times + with {:arg1=>42} + + Events received: + {} + MESSAGE + end + end + + it 'does not raise if event was logged 1x' do + track_event.call + expect(&code_under_test). + not_to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + + it 'raises if event was logged 2x' do + track_event.call + track_event.call + + expect(&code_under_test). + to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| + assert_error_messages_equal(err, <<~MESSAGE) + Expected that FakeAnalytics would have received event :my_event once but it was received twice + with {:arg1=>42} + + Events received: + {:my_event=>[{:arg1=>42}, {:arg1=>42}]} + MESSAGE + end + end + end end context 'event name + include() matcher' do @@ -183,8 +260,8 @@ expect(&code_under_test). to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| assert_error_messages_equal(err, <<~MESSAGE) - Expected that FakeAnalytics would have received event :my_event - with # @expecteds=[{:arg1=>42}]>. + Expected that FakeAnalytics would have received matching event :my_event + with include(arg1: 42) Events received: {} @@ -198,8 +275,8 @@ expect(&code_under_test). to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| assert_error_messages_equal(err, <<~MESSAGE) - Expected that FakeAnalytics would have received event :my_event - with # @expecteds=[{:arg1=>42}]>. + Expected that FakeAnalytics would have received matching event :my_event + with include(arg1: 42) Events received: {:my_other_event=>[{}]} @@ -213,15 +290,14 @@ expect(&code_under_test). to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| assert_error_messages_equal(err, <<~MESSAGE) - Expected that FakeAnalytics would have received matching event my_eventexpected: include {:arg1=>42} + Expected that FakeAnalytics would have received matching event :my_event + expected: include(arg1: 42) got: {:arg1=>43} Diff: @@ -1 +1 @@ -:arg1 => 42, +:arg1 => 43, - - Attributes ignored by the include matcher: MESSAGE end end @@ -253,6 +329,49 @@ expect(&code_under_test). not_to raise_error(RSpec::Expectations::ExpectationNotMetError) end + + describe '.once' do + let(:code_under_test) do + -> { + expect(analytics).to have_logged_event(:my_event, include(arg1: 42)).once + } + end + + it 'raises if no event has been logged' do + expect(&code_under_test). + to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| + assert_error_messages_equal(err, <<~MESSAGE) + Expected that FakeAnalytics would have received matching event :my_event once but it was received 0 times + with include(arg1: 42) + + Events received: + {} + MESSAGE + end + end + + it 'does not raise if event was logged 1x' do + track_event.call + expect(&code_under_test). + not_to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + + it 'raises if event was logged 2x' do + track_event.call + track_event.call + + expect(&code_under_test). + to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| + assert_error_messages_equal(err, <<~MESSAGE) + Expected that FakeAnalytics would have received matching event :my_event once but it was received twice + with include(arg1: 42) + + Events received: + {:my_event=>[{:arg1=>42}, {:arg1=>42}]} + MESSAGE + end + end + end end context 'event name + hash_including() matcher' do @@ -274,8 +393,8 @@ expect(&code_under_test). to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| assert_error_messages_equal(err, <<~MESSAGE) - Expected that FakeAnalytics would have received event :my_event - with # @expected={:arg1=>42}>. + Expected that FakeAnalytics would have received matching event :my_event + with hash_including(arg1: 42) Events received: {} @@ -289,8 +408,8 @@ expect(&code_under_test). to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| assert_error_messages_equal(err, <<~MESSAGE) - Expected that FakeAnalytics would have received event :my_event - with # @expected={:arg1=>42}>. + Expected that FakeAnalytics would have received matching event :my_event + with hash_including(arg1: 42) Events received: {:my_other_event=>[{}]} @@ -304,11 +423,14 @@ expect(&code_under_test). to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| assert_error_messages_equal(err, <<~MESSAGE) - Expected that FakeAnalytics would have received event :my_event - with # @expected={:arg1=>42}>. + Expected that FakeAnalytics would have received matching event :my_event + expected: hash_including(arg1: 42) + got: {:arg1=>43} - Events received: - {:my_event=>[{:arg1=>43}]} + Diff: + @@ -1 +1 @@ + -:arg1 => 42, + +:arg1 => 43, MESSAGE end end @@ -340,6 +462,49 @@ expect(&code_under_test). not_to raise_error(RSpec::Expectations::ExpectationNotMetError) end + + describe '.once' do + let(:code_under_test) do + -> { + expect(analytics).to have_logged_event(:my_event, hash_including(arg1: 42)).once + } + end + + it 'raises if no event has been logged' do + expect(&code_under_test). + to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| + assert_error_messages_equal(err, <<~MESSAGE) + Expected that FakeAnalytics would have received matching event :my_event once but it was received 0 times + with hash_including(arg1: 42) + + Events received: + {} + MESSAGE + end + end + + it 'does not raise if event was logged 1x' do + track_event.call + expect(&code_under_test). + not_to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + + it 'raises if event was logged 2x' do + track_event.call + track_event.call + + expect(&code_under_test). + to raise_error(RSpec::Expectations::ExpectationNotMetError) do |err| + assert_error_messages_equal(err, <<~MESSAGE) + Expected that FakeAnalytics would have received matching event :my_event once but it was received twice + with hash_including(arg1: 42) + + Events received: + {:my_event=>[{:arg1=>42}, {:arg1=>42}]} + MESSAGE + end + end + end end end diff --git a/spec/support/have_logged_event_matcher.rb b/spec/support/have_logged_event_matcher.rb new file mode 100644 index 00000000000..824d20da919 --- /dev/null +++ b/spec/support/have_logged_event_matcher.rb @@ -0,0 +1,162 @@ +require 'rspec/matchers/built_in/count_expectation' + +class HaveLoggedEventMatcher + include RSpec::Matchers::Composable + include RSpec::Matchers::BuiltIn::CountExpectation + + def initialize( + expected_event_name: nil, + expected_attributes: nil + ) + @expected_event_name = expected_event_name + @expected_attributes = expected_attributes + end + + def failure_message + matching_events = events[expected_event_name] + + if matching_events&.length == 1 + failure_message_with_diff(matching_events.first) + else + default_failure_message + end + end + + def matches?(actual) + @actual = actual + + matched_events = + if expected_event_name.nil? && expected_attributes.nil? + events.values.flatten + elsif expected_attributes.nil? + events[expected_event_name] || [] + else + (events[expected_event_name] || []).filter do |actual_attributes| + values_match?(expected_attributes, actual_attributes) + end + end + + if has_expected_count? + expected_count_matches?(matched_events.length) + else + matched_events.length > 0 + end + end + + private + + attr_reader :expected_event_name, :expected_attributes + + def default_failure_message + with_attributes = expected_attributes.nil? ? + nil : + "with #{format_attributes(expected_attributes)}" + + received = <<~RECEIVED + + Events received: + #{events.pretty_inspect} + RECEIVED + + [ + expectation_description, + with_attributes, + received, + ].compact.join("\n") + end + + def differ + RSpec::Support::Differ.new( + object_preparer: ->(object) { RSpec::Matchers::Composable.surface_descriptions_in(object) }, + color: RSpec::Matchers.configuration.color?, + ) + end + + def events + if @actual.respond_to?(:events) + @actual.events + else + @actual + end + end + + def expectation_description + adjective = attributes_matcher_description(expected_attributes) ? 'matching ' : '' + [ + "Expected that FakeAnalytics would have received #{adjective}event", + expected_event_name.inspect, + has_expected_count? ? count_failure_reason('it was received').strip : nil, + ].compact.join(' ') + end + + def expected_attributes_hash + resolve_attributes_hash(expected_attributes) + end + + def attributes_matcher_description(attributes) + if attributes.instance_of?(RSpec::Matchers::BuiltIn::Include) + 'include' + elsif attributes.instance_of?(RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher) + 'hash_including' + end + end + + def diff_description(actual_attributes) + <<~DIFF + expected: #{format_attributes(expected_attributes)} + got: #{format_attributes(actual_attributes)} + + Diff:#{differ.diff(actual_attributes, expected_attributes_hash)} + DIFF + end + + def failure_message_with_diff(actual_attributes) + [ + expectation_description, + diff_description(actual_attributes), + ignored_attributes_description(actual_attributes), + ].compact.join("\n") + end + + def format_attributes(attributes) + return '' if attributes.nil? + + desc = attributes_matcher_description(attributes) + resolved_attributes = resolve_attributes_hash(attributes) + + if desc + args = resolved_attributes.keys.map do |key| + "#{key}: #{resolved_attributes[key].inspect}" + end.join(', ') + + "#{desc}(#{args})" + else + resolved_attributes.inspect + end + end + + def ignored_attributes_description(actual_attributes) + ignored_attributes = actual_attributes.except(*expected_attributes_hash.keys) + return if ignored_attributes.empty? + + diff = differ.diff(ignored_attributes, {}) + + "Attributes ignored by the include matcher: #{diff}" + end + + def resolve_attributes_hash(attributes) + if attributes.instance_of?(RSpec::Matchers::BuiltIn::Include) + attributes.expecteds.first + elsif attributes.instance_of?(RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher) + attributes.instance_variable_get(:@expected) + else + attributes + end + end +end + +module RSpec::Matchers + def have_logged_event(expected_event_name = nil, expected_attributes = nil) + HaveLoggedEventMatcher.new(expected_event_name:, expected_attributes:) + end +end