diff options
Diffstat (limited to 'qa/qa/support/matchers')
-rw-r--r-- | qa/qa/support/matchers/eventually_matcher.rb | 138 | ||||
-rw-r--r-- | qa/qa/support/matchers/have_matcher.rb | 36 | ||||
-rw-r--r-- | qa/qa/support/matchers/have_text.rb | 52 |
3 files changed, 226 insertions, 0 deletions
diff --git a/qa/qa/support/matchers/eventually_matcher.rb b/qa/qa/support/matchers/eventually_matcher.rb new file mode 100644 index 00000000000..ff8adab424b --- /dev/null +++ b/qa/qa/support/matchers/eventually_matcher.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +# Rspec matcher with build in retry logic +# +# USAGE: +# +# Basic +# expect { Something.that.takes.time.to_appear }.to eventually_eq(expected_result) +# expect { Something.that.takes.time.to_appear }.not_to eventually_eq(expected_result) +# +# With duration and attempts override +# expect { Something.that.takes.time.to_appear }.to( +# eventually_eq(expected_result).within(max_duration: 10, max_attempts: 5) +# ) + +module QA + module Support + module Matchers + module EventuallyMatcher + %w[ + eq + be + include + be_truthy + be_falsey + be_empty + ].each do |op| + RSpec::Matchers.define(:"eventually_#{op}") do |*expected| + chain(:within) do |kwargs = {}| + @retry_args = kwargs + @retry_args[:sleep_interval] = 0.5 unless @retry_args[:sleep_interval] + end + + def supports_block_expectations? + true + end + + match { |actual| wait_and_check(actual, :default_expectation) } + + match_when_negated { |actual| wait_and_check(actual, :when_negated_expectation) } + + description do + "eventually #{operator_msg} #{expected.inspect}" + end + + failure_message do + "#{e}:\nexpected to #{description}, last attempt was #{@result.nil? ? 'nil' : @result}" + end + + failure_message_when_negated do + "#{e}:\nexpected not to #{description}, last attempt was #{@result.nil? ? 'nil' : @result}" + end + + # Execute rspec expectation within retrier + # + # @param [Proc] actual + # @param [Symbol] expectation_name + # @return [Boolean] + def wait_and_check(actual, expectation_name) + attempt = 0 + + QA::Runtime::Logger.debug("Running eventually matcher with '#{operator_msg}' operator") + QA::Support::Retrier.retry_until(**@retry_args) do + QA::Runtime::Logger.debug("evaluating expectation, attempt: #{attempt += 1}") + + public_send(expectation_name, actual) + rescue RSpec::Expectations::ExpectationNotMetError, QA::Resource::ApiFabricator::ResourceNotFoundError + false + end + rescue QA::Support::Repeater::RetriesExceededError, QA::Support::Repeater::WaitExceededError => e + @e = e + false + end + + # Execute rspec expectation + # + # @param [Proc] actual + # @return [void] + def default_expectation(actual) + expect(result(&actual)).to public_send(*expectation_args) + end + + # Execute negated rspec expectation + # + # @param [Proc] actual + # @return [void] + def when_negated_expectation(actual) + expect(result(&actual)).not_to public_send(*expectation_args) + end + + # Result of actual block + # + # @return [Object] + def result + @result = yield + end + + # Error message placeholder to indicate waiter did not fail properly + # This message should not appear under normal circumstances since it should + # always be assigned from repeater + # + # @return [String] + def e + @e ||= 'Waiter did not fail!' + end + + # Operator message + # + # @return [String] + def operator_msg + operator == 'eq' ? 'equal' : operator + end + + # Expect operator + # + # @return [String] + def operator + @operator ||= name.to_s.match(/eventually_(.+?)$/).to_a[1].to_s + end + + # Expectation args + # + # @return [String, Array] + def expectation_args + if operator.include?('truthy') || operator.include?('falsey') || operator.include?('empty') + operator + elsif operator == 'include' && expected.is_a?(Array) + [operator, *expected] + else + [operator, expected] + end + end + end + end + end + end + end +end diff --git a/qa/qa/support/matchers/have_matcher.rb b/qa/qa/support/matchers/have_matcher.rb new file mode 100644 index 00000000000..7001f53a7b7 --- /dev/null +++ b/qa/qa/support/matchers/have_matcher.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module QA + module Support + module Matchers + module HaveMatcher + PREDICATE_TARGETS = %w[ + element + file_content + assignee + child_pipeline + content + design + file + issue + job + package + pipeline + related_issue_item + snippet_description + tag + ].each do |predicate| + RSpec::Matchers.define "have_#{predicate}" do |*args, **kwargs| + match do |page_object| + page_object.public_send("has_#{predicate}?", *args, **kwargs) + end + + match_when_negated do |page_object| + page_object.public_send("has_no_#{predicate}?", *args, **kwargs) + end + end + end + end + end + end +end diff --git a/qa/qa/support/matchers/have_text.rb b/qa/qa/support/matchers/have_text.rb new file mode 100644 index 00000000000..2bae2971be3 --- /dev/null +++ b/qa/qa/support/matchers/have_text.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module QA + module Support + module Matchers + class HaveText + def initialize(expected_text, **kwargs) + @expected_text = expected_text + @kwargs = kwargs + end + + def matches?(actual) + @actual = wrap(actual) + @actual.has_text?(@expected_text, **@kwargs) + end + + def does_not_match?(actual) + @actual = wrap(actual) + @actual.has_no_text?(@expected_text, **@kwargs) + end + + def failure_message + "expected to find text \"#{@expected_text}\" in \"#{normalized_actual_text}\"" + end + + def failure_message_when_negated + "expected not to find text \"#{@expected_text}\" in \"#{normalized_actual_text}\"" + end + + def normalized_actual_text + @actual.text.gsub(/\s+/, " ") + end + + # From https://github.com/teamcapybara/capybara/blob/fe5940c6afbfe32152df936ce03ad1371ae05354/lib/capybara/rspec/matchers/base.rb#L66 + def wrap(actual) + actual = actual.to_capybara_node if actual.respond_to?(:to_capybara_node) + @context_el = if actual.respond_to?(:has_selector?) + actual + else + Capybara.string(actual.to_s) + end + end + end + + def have_text(text, **kwargs) # rubocop:disable Naming/PredicateName + HaveText.new(text, **kwargs) + end + + alias_method :have_content, :have_text + end + end +end |