summaryrefslogtreecommitdiff
path: root/qa/qa/support/matchers
diff options
context:
space:
mode:
Diffstat (limited to 'qa/qa/support/matchers')
-rw-r--r--qa/qa/support/matchers/eventually_matcher.rb138
-rw-r--r--qa/qa/support/matchers/have_matcher.rb36
-rw-r--r--qa/qa/support/matchers/have_text.rb52
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