All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openqa.selenium.interactions.Actions Maven / Gradle / Ivy

Go to download

Selenium automates browsers. That's it! What you do with that power is entirely up to you.

There is a newer version: 4.26.0
Show newest version
// 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.

package org.openqa.selenium.interactions;

import static org.openqa.selenium.interactions.PointerInput.MouseButton.LEFT;
import static org.openqa.selenium.interactions.PointerInput.MouseButton.RIGHT;

import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.IntConsumer;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.PointerInput.Origin;
import org.openqa.selenium.internal.Require;

/**
 * The user-facing API for emulating complex user gestures. Use this class rather than using the
 * Keyboard or Mouse directly.
 *
 * 

Implements the builder pattern: Builds a CompositeAction containing all actions specified by * the method calls. * *

Call {@link #perform()} at the end of the method chain to actually perform the actions. */ public class Actions { private final WebDriver driver; // W3C private final Map sequences = new HashMap<>(); private PointerInput activePointer; private KeyInput activeKeyboard; private WheelInput activeWheel; private Duration actionDuration; public Actions(WebDriver driver) { this(driver, Duration.ofMillis(250)); } public Actions(WebDriver driver, Duration duration) { this.driver = Require.nonNull("Driver", driver); this.actionDuration = duration; } /** * Performs a modifier key press. Does not release the modifier key - subsequent interactions may * assume it's kept pressed. Note that the modifier key is never released implicitly - * either keyUp(theKey) or sendKeys(Keys.NULL) must be called to release the * modifier. * * @param key * @return A self reference. */ public Actions keyDown(CharSequence key) { return addKeyAction(key, codePoint -> tick(getActiveKeyboard().createKeyDown(codePoint))); } /** * Performs a modifier key press after focusing on an element. Equivalent to: * Actions.click(element).sendKeys(theKey); * * @see #keyDown(CharSequence) * @param key * @param target WebElement to perform the action * @return A self reference. */ public Actions keyDown(WebElement target, CharSequence key) { return focusInTicks(target) .addKeyAction(key, codepoint -> tick(getActiveKeyboard().createKeyDown(codepoint))); } /** * Performs a modifier key release. Releasing a non-depressed modifier key will yield undefined * behaviour. * * @param key * @return A self reference. */ public Actions keyUp(CharSequence key) { return addKeyAction(key, codePoint -> tick(getActiveKeyboard().createKeyUp(codePoint))); } /** * Performs a modifier key release after focusing on an element. Equivalent to: * Actions.click(element).sendKeys(theKey); * * @see #keyUp(CharSequence) on behaviour regarding non-depressed modifier keys. * @param key * @param target WebElement to perform the action on * @return A self reference. */ public Actions keyUp(WebElement target, CharSequence key) { return focusInTicks(target) .addKeyAction(key, codePoint -> tick(getActiveKeyboard().createKeyUp(codePoint))); } /** * Sends keys to the active element. This differs from calling {@link * WebElement#sendKeys(CharSequence...)} on the active element in two ways: * *

    *
  • The modifier keys included in this call are not released. *
  • There is no attempt to re-focus the element - so sendKeys(Keys.TAB) for switching * elements should work. *
* * @see WebElement#sendKeys(CharSequence...) * @param keys The keys. * @return A self reference. * @throws IllegalArgumentException if keys is null */ public Actions sendKeys(CharSequence... keys) { return sendKeysInTicks(keys); } /** * Equivalent to calling: Actions.click(element).sendKeys(keysToSend). This method is * different from {@link WebElement#sendKeys(CharSequence...)} - see {@link * #sendKeys(CharSequence...)} for details how. * * @see #sendKeys(java.lang.CharSequence[]) * @param target element to focus on. * @param keys The keys. * @return A self reference. * @throws IllegalArgumentException if keys is null */ public Actions sendKeys(WebElement target, CharSequence... keys) { return focusInTicks(target).sendKeysInTicks(keys); } private Actions sendKeysInTicks(CharSequence... keys) { if (keys == null) { throw new IllegalArgumentException("Keys should be a not null CharSequence"); } for (CharSequence key : keys) { key.codePoints() .forEach( codePoint -> { tick(getActiveKeyboard().createKeyDown(codePoint)); tick(getActiveKeyboard().createKeyUp(codePoint)); }); } return this; } private Actions addKeyAction(CharSequence key, IntConsumer consumer) { // Verify that we only have a single character to type. if (key.codePoints().count() != 1) { throw new IllegalStateException( String.format("Only one code point is allowed at a time: %s", key)); } key.codePoints().forEach(consumer); return this; } /** * Clicks (without releasing) in the middle of the given element. This is equivalent to: * Actions.moveToElement(onElement).clickAndHold() * * @param target Element to move to and click. * @return A self reference. */ public Actions clickAndHold(WebElement target) { return moveInTicks(target, 0, 0).tick(getActivePointer().createPointerDown(LEFT.asArg())); } /** * Clicks (without releasing) at the current mouse location. * * @return A self reference. */ public Actions clickAndHold() { return tick(getActivePointer().createPointerDown(LEFT.asArg())); } /** * Releases the depressed left mouse button, in the middle of the given element. This is * equivalent to: Actions.moveToElement(onElement).release() * *

Invoking this action without invoking {@link #clickAndHold()} first will result in undefined * behaviour. * * @param target Element to release the mouse button above. * @return A self reference. */ public Actions release(WebElement target) { return moveInTicks(target, 0, 0).tick(getActivePointer().createPointerUp(LEFT.asArg())); } /** * If the element is outside the viewport, scrolls the bottom of the element to the bottom of the * viewport. * * @param element Which element to scroll into the viewport. * @return A self reference. */ public Actions scrollToElement(WebElement element) { WheelInput.ScrollOrigin scrollOrigin = WheelInput.ScrollOrigin.fromElement(element); return tick(getActiveWheel().createScroll(0, 0, 0, 0, this.actionDuration, scrollOrigin)); } /** * Scrolls by provided amounts with the origin in the top left corner of the viewport. * * @param deltaX The distance along X axis to scroll using the wheel. A negative value scrolls * left. * @param deltaY The distance along Y axis to scroll using the wheel. A negative value scrolls up. * @return A self reference. */ public Actions scrollByAmount(int deltaX, int deltaY) { WheelInput.ScrollOrigin scrollOrigin = WheelInput.ScrollOrigin.fromViewport(); return tick( getActiveWheel().createScroll(0, 0, deltaX, deltaY, this.actionDuration, scrollOrigin)); } /** * Scrolls by provided amount based on a provided origin. The scroll origin is either the center * of an element or the upper left of the viewport plus any offsets. If the origin is an element, * and the element is not in the viewport, the bottom of the element will first be scrolled to the * bottom of the viewport. * * @param scrollOrigin Where scroll originates (viewport or element center) plus provided offsets. * @param deltaX The distance along X axis to scroll using the wheel. A negative value scrolls * left. * @param deltaY The distance along Y axis to scroll using the wheel. A negative value scrolls up. * @return A self reference. * @throws MoveTargetOutOfBoundsException If the origin with offset is outside the viewport. */ public Actions scrollFromOrigin(WheelInput.ScrollOrigin scrollOrigin, int deltaX, int deltaY) { int x = scrollOrigin.getxOffset(); int y = scrollOrigin.getyOffset(); return tick( getActiveWheel().createScroll(x, y, deltaX, deltaY, this.actionDuration, scrollOrigin)); } /** * Releases the depressed left mouse button at the current mouse location. * * @see #release(org.openqa.selenium.WebElement) * @return A self reference. */ public Actions release() { return tick(getActivePointer().createPointerUp(LEFT.asArg())); } /** * Clicks in the middle of the given element. Equivalent to: * Actions.moveToElement(onElement).click() * * @param target Element to click. * @return A self reference. */ public Actions click(WebElement target) { return moveInTicks(target, 0, 0).clickInTicks(LEFT); } /** * Clicks at the current mouse location. Useful when combined with {@link * #moveToElement(org.openqa.selenium.WebElement, int, int)} or {@link #moveByOffset(int, int)}. * * @return A self reference. */ public Actions click() { return clickInTicks(LEFT); } private Actions clickInTicks(PointerInput.MouseButton button) { tick(getActivePointer().createPointerDown(button.asArg())); tick(getActivePointer().createPointerUp(button.asArg())); return this; } private Actions focusInTicks(WebElement target) { return moveInTicks(target, 0, 0).clickInTicks(LEFT); } /** * Performs a double-click at middle of the given element. Equivalent to: * Actions.moveToElement(element).doubleClick() * * @param target Element to move to. * @return A self reference. */ public Actions doubleClick(WebElement target) { return moveInTicks(target, 0, 0).clickInTicks(LEFT).clickInTicks(LEFT); } /** * Performs a double-click at the current mouse location. * * @return A self reference. */ public Actions doubleClick() { return clickInTicks(LEFT).clickInTicks(LEFT); } /** * Moves the mouse to the middle of the element. The element is scrolled into view and its * location is calculated using getClientRects. * * @param target element to move to. * @return A self reference. */ public Actions moveToElement(WebElement target) { return moveInTicks(target, 0, 0); } /** * Moves the mouse to an offset from the element's in-view center point. * * @param target element to move to. * @param xOffset Offset from the element's in-view center point. A negative value means an offset * left of the point. * @param yOffset Offset from the element's in-view center point. A negative value means an offset * above the point. * @return A self reference. */ public Actions moveToElement(WebElement target, int xOffset, int yOffset) { return moveInTicks(target, xOffset, yOffset); } private Actions moveInTicks(WebElement target, int xOffset, int yOffset) { return tick( getActivePointer() .createPointerMove( Duration.ofMillis(100), Origin.fromElement(target), xOffset, yOffset)); } /** * Moves the mouse from its current position (or 0,0) by the given offset. If the final * coordinates of the move are outside the viewport (the mouse will end up outside the browser * window), an exception is raised. * * @param xOffset horizontal offset. A negative value means moving the mouse left. * @param yOffset vertical offset. A negative value means moving the mouse up. * @return A self reference. * @throws MoveTargetOutOfBoundsException if the provided offset is outside the document's * boundaries. */ public Actions moveByOffset(int xOffset, int yOffset) { return tick( getActivePointer() .createPointerMove(Duration.ofMillis(200), Origin.pointer(), xOffset, yOffset)); } /** * Moves the mouse to provided coordinates on screen regardless of starting position of the mouse. * If the coordinates provided are outside the viewport (the mouse will end up outside the browser * window), an exception is raised. * * @param xCoordinate positive pixel value along horizontal axis in viewport. Numbers increase * going right. * @param yCoordinate positive pixel value along vertical axis in viewport. Numbers increase going * down. * @return A self reference. * @throws MoveTargetOutOfBoundsException if the provided offset is outside the document's * boundaries. */ public Actions moveToLocation(int xCoordinate, int yCoordinate) { return tick( getActivePointer() .createPointerMove( Duration.ofMillis(250), Origin.viewport(), xCoordinate, yCoordinate)); } /** * Performs a context-click at middle of the given element. First performs a mouseMove to the * location of the element. * * @param target Element to move to. * @return A self reference. */ public Actions contextClick(WebElement target) { return moveInTicks(target, 0, 0).clickInTicks(RIGHT); } /** * Performs a context-click at the current mouse location. * * @return A self reference. */ public Actions contextClick() { return clickInTicks(RIGHT); } /** * 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. * * @param source element to emulate button down at. * @param target element to move to and release the mouse at. * @return A self reference. */ public Actions dragAndDrop(WebElement source, WebElement target) { return moveInTicks(source, 0, 0) .tick(getActivePointer().createPointerDown(LEFT.asArg())) .moveInTicks(target, 0, 0) .tick(getActivePointer().createPointerUp(LEFT.asArg())); } /** * A convenience method that performs click-and-hold at the location of the source element, moves * by a given offset, then releases the mouse. * * @param source element to emulate button down at. * @param xOffset horizontal move offset. * @param yOffset vertical move offset. * @return A self reference. */ public Actions dragAndDropBy(WebElement source, int xOffset, int yOffset) { return moveInTicks(source, 0, 0) .tick(getActivePointer().createPointerDown(LEFT.asArg())) .tick( getActivePointer() .createPointerMove(Duration.ofMillis(250), Origin.pointer(), xOffset, yOffset)) .tick(getActivePointer().createPointerUp(LEFT.asArg())); } /** * Performs a pause. * * @param pause pause duration, in milliseconds. * @return A self reference. */ public Actions pause(long pause) { return tick(new Pause(getActivePointer(), Duration.ofMillis(pause))); } public Actions pause(Duration duration) { Require.nonNegative("Duration of pause", duration); return tick(new Pause(getActivePointer(), duration)); } public Actions tick(Interaction... actions) { // All actions must be for a unique source. Set seenSources = new HashSet<>(); for (Interaction action : actions) { boolean freshlyAdded = seenSources.add(action.getSource()); if (!freshlyAdded) { throw new IllegalStateException( String.format( "You may only add one action per input source per tick: %s", Arrays.asList(actions))); } } // Add all actions to sequences for (Interaction action : actions) { Sequence sequence = getSequence(action.getSource()); sequence.addAction(action); } // And now pad the remaining sequences with a pause. Set unseen = new HashSet<>(sequences.keySet()); unseen.removeAll(seenSources); for (InputSource source : unseen) { getSequence(source).addAction(new Pause(source, Duration.ZERO)); } return this; } public Actions setActiveKeyboard(String name) { InputSource inputSource = sequences.keySet().stream() .filter(input -> Objects.equals(input.getName(), name)) .findFirst() .orElse(null); if (inputSource == null) { this.activeKeyboard = new KeyInput(name); } else { this.activeKeyboard = (KeyInput) inputSource; } return this; } public Actions setActivePointer(PointerInput.Kind kind, String name) { InputSource inputSource = sequences.keySet().stream() .filter(input -> Objects.equals(input.getName(), name)) .findFirst() .orElse(null); if (inputSource == null) { this.activePointer = new PointerInput(kind, name); } else { this.activePointer = (PointerInput) inputSource; } return this; } public Actions setActiveWheel(String name) { InputSource inputSource = sequences.keySet().stream() .filter(input -> Objects.equals(input.getName(), name)) .findFirst() .orElse(null); if (inputSource == null) { this.activeWheel = new WheelInput(name); } else { this.activeWheel = (WheelInput) inputSource; } return this; } public KeyInput getActiveKeyboard() { if (this.activeKeyboard == null) { setActiveKeyboard("default keyboard"); } return this.activeKeyboard; } public PointerInput getActivePointer() { if (this.activePointer == null) { setActivePointer(PointerInput.Kind.MOUSE, "default mouse"); } return this.activePointer; } public WheelInput getActiveWheel() { if (this.activeWheel == null) { setActiveWheel("default wheel"); } return this.activeWheel; } public Duration getActionDuration() { return this.actionDuration; } /** * Generates a composite action containing all actions so far, ready to be performed (and resets * the internal builder state, so subsequent calls to this method will contain fresh sequences). * *

Warning: you may want to call {@link #perform()} instead to actually perform the * actions. * * @return the composite action */ public Action build() { Action toReturn = new BuiltAction(driver, new LinkedHashMap<>(sequences)); sequences.clear(); return toReturn; } /** A convenience method for performing the actions without calling build() first. */ public void perform() { build().perform(); } private Sequence getSequence(InputSource source) { Sequence sequence = sequences.get(source); if (sequence != null) { return sequence; } int longest = 0; for (Sequence examining : sequences.values()) { longest = Math.max(longest, examining.size()); } sequence = new Sequence(source, longest); sequences.put(source, sequence); return sequence; } public Collection getSequences() { return sequences.values(); } private static class BuiltAction implements Action { private final WebDriver driver; private final Map sequences; private BuiltAction(WebDriver driver, Map sequences) { this.driver = driver; this.sequences = sequences; } @Override public void perform() { ((Interactive) driver).perform(sequences.values()); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy