lib/appium_lib_core/common/wait.rb



# frozen_string_literal: true

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require_relative 'wait/timer'

module Appium
  module Core
    module Wait
      class TimeoutError < StandardError; end

      DEFAULT_TIMEOUT  = 30
      DEFAULT_INTERVAL = 0.5

      class << self
        # Check every interval seconds to see if yield doesn't raise an exception.
        # Give up after timeout seconds.
        #
        # If only a number is provided then it's treated as the timeout value.
        #
        # @param [Integer] timeout Seconds to wait before timing out. Set default by +appium_wait_timeout+ (30).
        # @param [Integer] interval Seconds to sleep between polls. Set default by +appium_wait_interval+ (0.5).
        # @param [String] message Exception message if timed out.
        # @param [Array, Exception] ignored Exceptions to ignore while polling (default: Exception)
        # @param [Object, NilClass] object Object to evaluate block against
        #
        # @example
        #
        #     result = Appium::Core::Wait.until { @driver.find_element(:id, 'something') }
        #
        #     result = Appium::Core::Wait.until(timeout: 30, message: 'timeout') { @driver.find_element(:id, 'something') }
        #
        #     result = Appium::Core::Wait.until(object: 'some object') { |object|
        #        @driver.find_element(:id, object)
        #     }
        #
        def until(timeout: DEFAULT_TIMEOUT, interval: DEFAULT_INTERVAL, message: nil, ignored: nil, object: nil)
          ignored = Array(ignored || ::Exception)

          last_error = nil
          timer = Wait::Timer.new(timeout)

          until timer.timeout?
            begin
              return yield(object)
            rescue ::Errno::ECONNREFUSED => e
              raise e
            rescue *ignored => last_error
              # swallowed
            end
            sleep interval
          end

          msg = message_for timeout, message
          msg += " (#{last_error.message})" if last_error

          raise TimeoutError, msg
        end

        # Check every interval seconds to see if yield returns a truthy value.
        # Note this isn't a strict boolean true, any truthy value is accepted.
        # false and nil are considered failures.
        # Give up after timeout seconds.
        #
        # If only a number is provided then it's treated as the timeout value.
        #
        # @param [Integer] timeout Seconds to wait before timing out. Set default by +appium_wait_timeout+ (30).
        # @param [Integer] interval Seconds to sleep between polls. Set default by +appium_wait_interval+ (0.5).
        # @param [String] message Exception message if timed out.
        # @param [Array, Exception] ignored Exceptions to ignore while polling (default: Exception)
        # @param [Object, NilClass] object Object to evaluate block against
        #
        # @example
        #
        #     Appium::Core::Wait.until_true { @driver.find_element(:id, 'something') }
        #
        #     Appium::Core::Wait.until_true(timeout: 30) { @driver.find_element(:id, 'something') }
        #
        #     Appium::Core::Wait.until_true(object: 'some object') { |object|
        #        @driver.find_element(:id, object)
        #     }
        #
        def until_true(timeout: DEFAULT_TIMEOUT, interval: DEFAULT_INTERVAL, message: nil, ignored: nil, object: nil)
          ignored = Array(ignored || ::Exception)

          last_error = nil
          timer = Wait::Timer.new(timeout)

          until timer.timeout?
            begin
              result = yield(object)
              return result if result
            rescue ::Errno::ECONNREFUSED => e
              raise e
            rescue *ignored => last_error
              # swallowed
            end
            sleep interval
          end

          msg = message_for timeout, message
          msg += " (#{last_error.message})" if last_error

          raise TimeoutError, msg
        end

        private

        def message_for(timeout, message)
          msg = "timed out after #{timeout} seconds"
          msg += ", #{message}" if message
          msg
        end
      end # self
    end # module Wait

    module Waitable
      # Check every interval seconds to see if yield returns a truthy value.
      # Note this isn't a strict boolean true, any truthy value is accepted.
      # false and nil are considered failures.
      # Give up after timeout seconds.
      #
      # If only a number is provided then it's treated as the timeout value.
      #
      # @param [Integer] timeout Seconds to wait before timing out. Set default by +appium_wait_timeout+ (30).
      # @param [Integer] interval Seconds to sleep between polls. Set default by +appium_wait_interval+ (0.5).
      # @param [String] message Exception message if timed out.
      # @param [Array, Exception] ignored Exceptions to ignore while polling (default: Exception)
      #
      # @example With core instance
      #
      #   @core.wait_true { @driver.find_element :accessibility_id, 'something' }
      #   @core.wait_true(timeout: 30, interval: 2) { @driver.find_element :accessibility_id, 'something' }
      #
      #   @core.wait_until_true { @driver.find_element :accessibility_id, 'something' }
      #   @core.wait_until_true(timeout: 30, interval: 2) { @driver.find_element :accessibility_id, 'something' }
      #
      # @example With driver instance
      #
      #   @driver.wait_true { |d| d.find_element :accessibility_id, 'something' }
      #   @driver.wait_true(timeout: 30, interval: 2) { |d| driver.find_element :accessibility_id, 'something' }
      #
      #   @driver.wait_until_true { |d| d.find_element :accessibility_id, 'something' }
      #   @driver.wait_until_true(timeout: 30, interval: 2) { |d| driver.find_element :accessibility_id, 'something' }
      #
      def wait_until_true(timeout: nil, interval: nil, message: nil, ignored: nil, &block)
        Wait.until_true(timeout: timeout || @wait_timeout,
                        interval: interval || @wait_interval,
                        message: message,
                        ignored: ignored,
                        object: self,
                        &block)
      end
      alias wait_true wait_until_true

      # Check every interval seconds to see if yield doesn't raise an exception.
      # Give up after timeout seconds.
      #
      # If only a number is provided then it's treated as the timeout value.
      #
      # @param [Integer] timeout Seconds to wait before timing out. Set default by +appium_wait_timeout+ (30).
      # @param [Integer] interval Seconds to sleep between polls. Set default by +appium_wait_interval+ (0.5).
      # @param [String] message Exception message if timed out.
      # @param [Array, Exception] ignored Exceptions to ignore while polling (default: Exception)
      #
      # @example With core instance
      #
      #   @core.wait { @driver.find_element :accessibility_id, 'something' }
      #   @core.wait(timeout: 30, interval: 2) { @driver.find_element :accessibility_id, 'something' }
      #
      #   @core.wait_until { @driver.find_element :accessibility_id, 'something' }
      #   @core.wait_until(timeout: 30, interval: 2) { @driver.find_element :accessibility_id, 'something' }
      #
      # @example With driver instance
      #
      #   @driver.wait { @driver.find_element :accessibility_id, 'something' }
      #   @driver.wait(timeout: 30, interval: 2) { @driver.find_element :accessibility_id, 'something' }
      #
      #   @driver.wait_until { |d| d.find_element :accessibility_id, 'something' }
      #   @driver.wait_until(timeout: 30, interval: 2) { |d| d.find_element :accessibility_id, 'something' }
      #
      def wait_until(timeout: nil, interval: nil, message: nil, ignored: nil, &block)
        Wait.until(timeout: timeout || @wait_timeout,
                   interval: interval || @wait_interval,
                   message: message,
                   ignored: ignored,
                   object: self,
                   &block)
      end
      alias wait wait_until
    end
  end # module Core
end # module Appium