lib/selenium/webdriver/common/action_builder.rb



# frozen_string_literal: true

# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The SFC licenses this file
# to you 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.

module Selenium
  module WebDriver
    class ActionBuilder
      include KeyActions # Actions specific to key inputs
      include PointerActions # Actions specific to pointer inputs
      include WheelActions # Actions specific to wheel inputs

      attr_reader :devices

      #
      # Initialize a W3C Action Builder. Differs from previous by requiring a bridge and allowing asynchronous actions.
      # The W3C implementation allows asynchronous actions per device. e.g. A key can be pressed at the same time that
      # the mouse is moving. Keep in mind that pauses must be added for other devices in order to line up the actions
      # correctly when using asynchronous.
      #
      # @param [Selenium::WebDriver::Remote::Bridge] bridge the bridge for the current driver instance.
      # @param [Array<Selenium::WebDriver::Interactions::InputDevices>] devices list of valid sources of input.
      # @param [Boolean] async Whether to perform the actions asynchronously per device.
      # @return [ActionBuilder] A self reference.
      #

      def initialize(bridge, devices: [], async: false, duration: 250)
        @bridge = bridge
        @duration = duration
        @async = async
        @devices = []

        Array(devices).each { |device| add_input(device) }
      end

      #
      # Adds a PointerInput device of the given kind
      #
      # @example Add a touch pointer input device
      #
      #    builder = device.action
      #    builder.add_pointer_input('touch', :touch)
      #
      # @param [String] name name for the device
      # @param [Symbol] kind kind of pointer device to create
      # @return [Interactions::PointerInput] The pointer input added
      #
      #

      def add_pointer_input(kind, name)
        add_input(Interactions.pointer(kind, name: name))
      end

      #
      # Adds a KeyInput device
      #
      # @example Add a key input device
      #
      #    builder = device.action
      #    builder.add_key_input('keyboard2')
      #
      # @param [String] name name for the device
      # @return [Interactions::KeyInput] The key input added
      #

      def add_key_input(name)
        add_input(Interactions.key(name))
      end

      #
      # Adds a WheelInput device
      #
      # @example Add a wheel input device
      #
      #    builder = device.action
      #    builder.add_wheel_input('wheel2')
      #
      # @param [String] name name for the device
      # @return [Interactions::WheelInput] The wheel input added
      #

      def add_wheel_input(name)
        add_input(Interactions.wheel(name))
      end

      #
      # Retrieves the input device for the given name or type
      #
      # @param [String] name name of the input device
      # @param [String] type name of the input device
      # @return [Selenium::WebDriver::Interactions::InputDevice] input device with given name or type
      #

      def device(name: nil, type: nil)
        input = @devices.find { |device| (device.name == name.to_s || name.nil?) && (device.type == type || type.nil?) }

        raise(ArgumentError, "Can not find device: #{name}") if name && input.nil?

        input
      end

      #
      # Retrieves the current PointerInput devices
      #
      # @return [Array] array of current PointerInput devices
      #

      def pointer_inputs
        @devices.select { |device| device.type == Interactions::POINTER }
      end

      #
      # Retrieves the current KeyInput device
      #
      # @return [Selenium::WebDriver::Interactions::InputDevice] current KeyInput device
      #

      def key_inputs
        @devices.select { |device| device.type == Interactions::KEY }
      end

      #
      # Retrieves the current WheelInput device
      #
      # @return [Selenium::WebDriver::Interactions::InputDevice] current WheelInput devices
      #

      def wheel_inputs
        @devices.select { |device| device.type == Interactions::WHEEL }
      end

      #
      # Creates a pause for the given device of the given duration. If no duration is given, the pause will only wait
      # for all actions to complete in that tick.
      #
      # @example Send keys to an element
      #
      #   action_builder = driver.action
      #   keyboard = action_builder.key_input
      #   el = driver.find_element(id: "some_id")
      #   driver.action.click(el).pause(keyboard).pause(keyboard).pause(keyboard).send_keys('keys').perform
      #
      # @param [InputDevice] device Input device to pause
      # @param [Float] duration Duration to pause
      # @return [ActionBuilder] A self reference.
      #

      def pause(device: nil, duration: 0)
        device ||= pointer_input
        device.create_pause(duration)
        self
      end

      #
      # Creates multiple pauses for the given device of the given duration.
      #
      # @example Send keys to an element
      #
      #   action_builder = driver.action
      #   keyboard = action_builder.key_input
      #   el = driver.find_element(id: "some_id")
      #   driver.action.click(el).pauses(keyboard, 3).send_keys('keys').perform
      #
      # @param [InputDevice] device Input device to pause
      # @param [Integer] number of pauses to add for the device
      # @param [Float] duration Duration to pause
      # @return [ActionBuilder] A self reference.
      #

      def pauses(device: nil, number: nil, duration: 0)
        number ||= 2
        device ||= pointer_input
        duration ||= 0

        number.times { device.create_pause(duration) }
        self
      end

      #
      # Executes the actions added to the builder.
      #

      def perform
        @bridge.send_actions @devices.filter_map(&:encode)
        clear_all_actions
        nil
      end

      #
      # Clears all actions from the builder.
      #

      def clear_all_actions
        @devices.each(&:clear_actions)
      end

      #
      # Releases all action states from the browser.
      #

      def release_actions
        @bridge.release_actions
      end

      private

      #
      # Adds pauses for all devices but the given devices
      #
      # @param [Array[InputDevice]] action_devices Array of Input Devices performing an action in this tick.
      #

      def tick(*action_devices)
        return if @async

        @devices.each { |device| device.create_pause unless action_devices.include? device }
      end

      #
      # Adds an InputDevice
      #

      def add_input(device)
        device = Interactions.send(device) if device.is_a?(Symbol) && Interactions.respond_to?(device)

        raise TypeError, "#{device.inspect} is not a valid InputDevice" unless device.is_a?(Interactions::InputDevice)

        unless @async
          max_device = @devices.max { |a, b| a.actions.length <=> b.actions.length }
          pauses(device: device, number: max_device.actions.length) if max_device
        end
        @devices << device
        device
      end
    end # ActionBuilder
  end # WebDriver
end # Selenium