lib/selenium/webdriver/common/interactions/pointer_actions.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
    module PointerActions
      attr_writer :default_move_duration

      #
      # By default this is set to 250ms in the ActionBuilder constructor
      # It can be overridden with default_move_duration=
      #

      def default_move_duration
        @default_move_duration ||= @duration / 1000.0 # convert ms to seconds
      end

      #
      # Presses (without releasing) at the current location of the PointerInput device. This is equivalent to:
      #
      #   driver.action.click_and_hold(nil)
      #
      # @example Clicking and holding at the current location
      #
      #    driver.action.pointer_down(:left).perform
      #
      # @param [Selenium::WebDriver::Interactions::PointerPress::BUTTONS] button the button to press.
      # @param [Symbol || String] device optional name of the PointerInput device with the button
      #   that will be pressed
      # @return [ActionBuilder] A self reference.
      #

      def pointer_down(button = :left, device: nil, **opts)
        button_action(button, :create_pointer_down, device: device, **opts)
      end

      #
      # Releases the pressed mouse button at the current mouse location of the PointerInput device.
      #
      # @example Releasing a button after clicking and holding
      #
      #    driver.action.pointer_down(:left).pointer_up(:left).perform
      #
      # @param [Selenium::WebDriver::Interactions::PointerPress::BUTTONS] button the button to release.
      # @param [Symbol || String] device optional name of the PointerInput device with the button that will
      #   be released
      # @return [ActionBuilder] A self reference.
      #

      def pointer_up(button = :left, device: nil, **opts)
        button_action(button, :create_pointer_up, device: device, **opts)
      end

      #
      # Moves the pointer to the in-view center point of the given element.
      # Then the pointer is moved to optional offset coordinates.
      #
      # The element is not scrolled into view.
      # MoveTargetOutOfBoundsError will be raised if element with offset is outside the viewport
      #
      # When using offsets, both coordinates need to be passed.
      #
      # @example Move the pointer to element
      #
      #   el = driver.find_element(id: "some_id")
      #   driver.action.move_to(el).perform
      #
      # @example
      #
      #   el = driver.find_element(id: "some_id")
      #   driver.action.move_to(el, 100, 100).perform
      #
      # @param [Selenium::WebDriver::Element] element to move to.
      # @param [Integer] right_by Optional offset from the in-view center of the
      #   element. A negative value means coordinates to the left of the center.
      # @param [Integer] down_by Optional offset from the in-view center of the
      #   element. A negative value means coordinates to the top of the center.
      # @return [ActionBuilder] A self reference.
      #

      def move_to(element, right_by = nil, down_by = nil, **opts)
        pointer = pointer_input(opts.delete(:device))
        pointer.create_pointer_move(duration: opts.delete(:duration) || default_move_duration,
                                    x: right_by || 0,
                                    y: down_by || 0,
                                    origin: element,
                                    **opts)
        tick(pointer)
        self
      end

      #
      # Moves the pointer from its current position by the given offset.
      #
      # The viewport is not scrolled if the coordinates provided are outside the viewport.
      # MoveTargetOutOfBoundsError will be raised if the offsets are outside the viewport
      #
      # @example Move the pointer to a certain offset from its current position
      #
      #    driver.action.move_by(100, 100).perform
      #
      # @param [Integer] right_by horizontal offset. A negative value means moving the pointer left.
      # @param [Integer] down_by vertical offset. A negative value means moving the pointer up.
      # @param [Symbol || String] device optional name of the PointerInput device to move
      # @return [ActionBuilder] A self reference.
      # @raise [MoveTargetOutOfBoundsError] if the provided offset is outside the document's boundaries.
      #

      def move_by(right_by, down_by, device: nil, duration: default_move_duration, **opts)
        pointer = pointer_input(device)
        pointer.create_pointer_move(duration: duration,
                                    x: Integer(right_by),
                                    y: Integer(down_by),
                                    origin: Interactions::PointerMove::POINTER,
                                    **opts)
        tick(pointer)
        self
      end

      #
      # Moves the pointer to a given location in the viewport.
      #
      # The viewport is not scrolled if the coordinates provided are outside the viewport.
      # MoveTargetOutOfBoundsError will be raised if the offsets are outside the viewport
      #
      # @example Move the pointer to a certain position in the viewport
      #
      #    driver.action.move_to_location(100, 100).perform
      #
      # @param [Integer] x horizontal position. Equivalent to a css 'left' value.
      # @param [Integer] y vertical position. Equivalent to a css 'top' value.
      # @param [Symbol || String] device optional name of the PointerInput device to move
      # @return [ActionBuilder] A self reference.
      # @raise [MoveTargetOutOfBoundsError] if the provided x or y value is outside the document's boundaries.
      #

      def move_to_location(x, y, device: nil, duration: default_move_duration, **opts)
        pointer = pointer_input(device)
        pointer.create_pointer_move(duration: duration,
                                    x: Integer(x),
                                    y: Integer(y),
                                    origin: Interactions::PointerMove::VIEWPORT,
                                    **opts)
        tick(pointer)
        self
      end

      #
      # Clicks (without releasing) in the middle of the given element. This is
      # equivalent to:
      #
      #   driver.action.move_to(element).click_and_hold
      #
      # @example Clicking and holding on some element
      #
      #    el = driver.find_element(id: "some_id")
      #    driver.action.click_and_hold(el).perform
      #
      # @param [Selenium::WebDriver::Element] element the element to move to and click.
      # @param [Symbol || String] device optional name of the PointerInput device to click with
      # @return [ActionBuilder] A self reference.
      #

      def click_and_hold(element = nil, button: nil, device: nil)
        move_to(element, device: device) if element
        pointer_down(button || :left, device: device)
        self
      end

      #
      # Releases the depressed left mouse button at the current mouse location.
      #
      # @example Releasing an element after clicking and holding it
      #
      #    el = driver.find_element(id: "some_id")
      #    driver.action.click_and_hold(el).release.perform
      #
      # @param [Symbol || String] device optional name of the PointerInput device with the button
      #   that will be released
      # @return [ActionBuilder] A self reference.
      #

      def release(button: nil, device: nil)
        pointer_up(button || :left, device: device)
        self
      end

      #
      # Clicks in the middle of the given element. Equivalent to:
      #
      #   driver.action.move_to(element).click
      #
      # When no element is passed, the current mouse position will be clicked.
      #
      # @example Clicking on an element
      #
      #    el = driver.find_element(id: "some_id")
      #    driver.action.click(el).perform
      #
      # @example Clicking at the current mouse position
      #
      #    driver.action.click.perform
      #
      # @param [Selenium::WebDriver::Element] element An optional element to click.
      # @param [Symbol || String] device optional name of the PointerInput device with the button
      #   that will be clicked
      # @return [ActionBuilder] A self reference.
      #

      def click(element = nil, button: nil, device: nil)
        move_to(element, device: device) if element
        pointer_down(button || :left, device: device)
        pointer_up(button || :left, device: device)
        self
      end

      #
      # Performs a double-click at middle of the given element. Equivalent to:
      #
      #   driver.action.move_to(element).double_click
      #
      # When no element is passed, the current mouse position will be double-clicked.
      #
      # @example Double-click an element
      #
      #    el = driver.find_element(id: "some_id")
      #    driver.action.double_click(el).perform
      #
      # @example Double-clicking at the current mouse position
      #
      #    driver.action.double_click.perform
      #
      # @param [Selenium::WebDriver::Element] element An optional element to move to.
      # @param [Symbol || String] device optional name of the PointerInput device with the button
      #   that will be double-clicked
      # @return [ActionBuilder] A self reference.
      #

      def double_click(element = nil, device: nil)
        move_to(element, device: device) if element
        click(device: device)
        click(device: device)
        self
      end

      #
      # Performs a context-click at middle of the given element. First performs
      # a move_to to the location of the element.
      #
      # When no element is passed, the current mouse position will be context-clicked.
      #
      # @example Context-click at middle of given element
      #
      #   el = driver.find_element(id: "some_id")
      #   driver.action.context_click(el).perform
      #
      # @example Context-clicking at the current mouse position
      #
      #    driver.action.context_click.perform
      #
      # @param [Selenium::WebDriver::Element] element An element to context click.
      # @param [Symbol || String] device optional name of the PointerInput device with the button
      #   that will be context-clicked
      # @return [ActionBuilder] A self reference.
      #

      def context_click(element = nil, device: nil)
        click(element, button: :right, device: device)
      end

      #
      # A convenience method that performs click-and-hold at the location of the
      # source element, moves to the location of the target element, then
      # releases the mouse.
      #
      # @example Drag and drop one element onto another
      #
      #   el1 = driver.find_element(id: "some_id1")
      #   el2 = driver.find_element(id: "some_id2")
      #   driver.action.drag_and_drop(el1, el2).perform
      #
      # @param [Selenium::WebDriver::Element] source element to emulate button down at.
      # @param [Selenium::WebDriver::Element] target element to move to and release the
      #   mouse at.
      # @param [Symbol || String] device optional name of the PointerInput device with the button
      #   that will perform the drag and drop
      # @return [ActionBuilder] A self reference.
      #

      def drag_and_drop(source, target, device: nil)
        click_and_hold(source, device: device)
        move_to(target, device: device)
        release(device: device)
        self
      end

      #
      # A convenience method that performs click-and-hold at the location of
      # the source element, moves by a given offset, then releases the mouse.
      #
      # @example Drag and drop an element by offset
      #
      #   el = driver.find_element(id: "some_id1")
      #   driver.action.drag_and_drop_by(el, 100, 100).perform
      #
      # @param [Selenium::WebDriver::Element] source Element to emulate button down at.
      # @param [Integer] right_by horizontal move offset.
      # @param [Integer] down_by vertical move offset.
      # @param [Symbol || String] device optional name of the PointerInput device with the button
      #   that will perform the drag and drop
      # @return [ActionBuilder] A self reference.
      #

      def drag_and_drop_by(source, right_by, down_by, device: nil)
        click_and_hold(source, device: device)
        move_by(right_by, down_by, device: device)
        release(device: device)
        self
      end

      private

      def button_action(button, action, device: nil, **opts)
        pointer = pointer_input(device)
        pointer.send(action, button, **opts)
        tick(pointer)
        self
      end

      def pointer_input(name = nil)
        device(name: name, type: Interactions::POINTER) || add_pointer_input(:mouse, 'mouse')
      end
    end # PointerActions
  end # WebDriver
end # Selenium