Java tutorial
// 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.Kind.MOUSE; import static org.openqa.selenium.interactions.PointerInput.MouseButton.LEFT; import static org.openqa.selenium.interactions.PointerInput.MouseButton.RIGHT; import org.openqa.selenium.Keys; import org.openqa.selenium.UnsupportedCommandException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.interactions.PointerInput.Origin; import org.openqa.selenium.interactions.internal.MouseAction.Button; import java.time.Duration; import java.util.Arrays; 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 java.util.logging.Logger; /** * The user-facing API for emulating complex user gestures. Use this class rather than using the * Keyboard or Mouse directly. * <p> * Implements the builder pattern: Builds a CompositeAction containing all actions specified by the * method calls. */ public class Actions { private final static Logger LOG = Logger.getLogger(Actions.class.getName()); private final WebDriver driver; // W3C private final Map<InputSource, Sequence> sequences = new HashMap<>(); private final PointerInput defaultMouse = new PointerInput(MOUSE, "default mouse"); private final KeyInput defaultKeyboard = new KeyInput("default keyboard"); // JSON-wire protocol private final Keyboard jsonKeyboard; private final Mouse jsonMouse; protected CompositeAction action = new CompositeAction(); public Actions(WebDriver driver) { this.driver = Objects.requireNonNull(driver); if (driver instanceof HasInputDevices) { HasInputDevices deviceOwner = (HasInputDevices) driver; this.jsonKeyboard = deviceOwner.getKeyboard(); this.jsonMouse = deviceOwner.getMouse(); } else { this.jsonKeyboard = null; this.jsonMouse = null; } } /** * 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 <b>never</b> released implicitly - either * <i>keyUp(theKey)</i> or <i>sendKeys(Keys.NULL)</i> * must be called to release the modifier. * @param key Either {@link Keys#SHIFT}, {@link Keys#ALT} or {@link Keys#CONTROL}. If the * provided key is none of those, {@link IllegalArgumentException} is thrown. * @return A self reference. */ public Actions keyDown(CharSequence key) { if (isBuildingActions()) { action.addAction(new KeyDownAction(jsonKeyboard, jsonMouse, asKeys(key))); } return addKeyAction(key, codePoint -> tick(defaultKeyboard.createKeyDown(codePoint))); } /** * Performs a modifier key press after focusing on an element. Equivalent to: * <i>Actions.click(element).sendKeys(theKey);</i> * @see #keyDown(CharSequence) * * @param key Either {@link Keys#SHIFT}, {@link Keys#ALT} or {@link Keys#CONTROL}. If the * provided key is none of those, {@link IllegalArgumentException} is thrown. * @param target WebElement to perform the action * @return A self reference. */ public Actions keyDown(WebElement target, CharSequence key) { if (isBuildingActions()) { action.addAction(new KeyDownAction(jsonKeyboard, jsonMouse, (Locatable) target, asKeys(key))); } return focusInTicks(target).addKeyAction(key, codepoint -> tick(defaultKeyboard.createKeyDown(codepoint))); } /** * Performs a modifier key release. Releasing a non-depressed modifier key will yield undefined * behaviour. * * @param key Either {@link Keys#SHIFT}, {@link Keys#ALT} or {@link Keys#CONTROL}. * @return A self reference. */ public Actions keyUp(CharSequence key) { if (isBuildingActions()) { action.addAction(new KeyUpAction(jsonKeyboard, jsonMouse, asKeys(key))); } return addKeyAction(key, codePoint -> tick(defaultKeyboard.createKeyUp(codePoint))); } /** * Performs a modifier key release after focusing on an element. Equivalent to: * <i>Actions.click(element).sendKeys(theKey);</i> * @see #keyUp(CharSequence) on behaviour regarding non-depressed modifier keys. * * @param key Either {@link Keys#SHIFT}, {@link Keys#ALT} or {@link Keys#CONTROL}. * @param target WebElement to perform the action on * @return A self reference. */ public Actions keyUp(WebElement target, CharSequence key) { if (isBuildingActions()) { action.addAction(new KeyUpAction(jsonKeyboard, jsonMouse, (Locatable) target, asKeys(key))); } return focusInTicks(target).addKeyAction(key, codePoint -> tick(defaultKeyboard.createKeyUp(codePoint))); } /** * Sends keys to the active element. This differs from calling * {@link WebElement#sendKeys(CharSequence...)} on the active element in two ways: * <ul> * <li>The modifier keys included in this call are not released.</li> * <li>There is no attempt to re-focus the element - so sendKeys(Keys.TAB) for switching * elements should work. </li> * </ul> * * @see WebElement#sendKeys(CharSequence...) * * @param keys The keys. * @return A self reference. * * @throws IllegalArgumentException if keys is null */ public Actions sendKeys(CharSequence... keys) { if (isBuildingActions()) { action.addAction(new SendKeysAction(jsonKeyboard, jsonMouse, null, keys)); } return sendKeysInTicks(keys); } /** * Equivalent to calling: * <i>Actions.click(element).sendKeys(keysToSend).</i> * 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) { if (isBuildingActions()) { action.addAction(new SendKeysAction(jsonKeyboard, jsonMouse, (Locatable) target, keys)); } return focusInTicks(target).sendKeysInTicks(keys); } private Keys asKeys(CharSequence key) { if (!(key instanceof Keys)) { throw new IllegalArgumentException("keyDown argument must be an instanceof Keys: " + key); } return (Keys) key; } 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(defaultKeyboard.createKeyDown(codePoint)); tick(defaultKeyboard.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: * <i>Actions.moveToElement(onElement).clickAndHold()</i> * * @param target Element to move to and click. * @return A self reference. */ public Actions clickAndHold(WebElement target) { if (isBuildingActions()) { action.addAction(new ClickAndHoldAction(jsonMouse, (Locatable) target)); } return moveInTicks(target, 0, 0).tick(defaultMouse.createPointerDown(LEFT.asArg())); } /** * Clicks (without releasing) at the current mouse location. * @return A self reference. */ public Actions clickAndHold() { if (isBuildingActions()) { action.addAction(new ClickAndHoldAction(jsonMouse, null)); } return tick(defaultMouse.createPointerDown(LEFT.asArg())); } /** * Releases the depressed left mouse button, in the middle of the given element. * This is equivalent to: * <i>Actions.moveToElement(onElement).release()</i> * * 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) { if (isBuildingActions()) { action.addAction(new ButtonReleaseAction(jsonMouse, (Locatable) target)); } return moveInTicks(target, 0, 0).tick(defaultMouse.createPointerUp(LEFT.asArg())); } /** * Releases the depressed left mouse button at the current mouse location. * @see #release(org.openqa.selenium.WebElement) * @return A self reference. */ public Actions release() { if (isBuildingActions()) { action.addAction(new ButtonReleaseAction(jsonMouse, null)); } return tick(defaultMouse.createPointerUp(Button.LEFT.asArg())); } /** * Clicks in the middle of the given element. Equivalent to: * <i>Actions.moveToElement(onElement).click()</i> * * @param target Element to click. * @return A self reference. */ public Actions click(WebElement target) { if (isBuildingActions()) { action.addAction(new ClickAction(jsonMouse, (Locatable) 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() { if (isBuildingActions()) { action.addAction(new ClickAction(jsonMouse, null)); } return clickInTicks(LEFT); } private Actions clickInTicks(PointerInput.MouseButton button) { tick(defaultMouse.createPointerDown(button.asArg())); tick(defaultMouse.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: * <i>Actions.moveToElement(element).doubleClick()</i> * * @param target Element to move to. * @return A self reference. */ public Actions doubleClick(WebElement target) { if (isBuildingActions()) { action.addAction(new DoubleClickAction(jsonMouse, (Locatable) 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() { if (isBuildingActions()) { action.addAction(new DoubleClickAction(jsonMouse, null)); } 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 getBoundingClientRect. * @param target element to move to. * @return A self reference. */ public Actions moveToElement(WebElement target) { if (isBuildingActions()) { action.addAction(new MoveMouseAction(jsonMouse, (Locatable) target)); } return moveInTicks(target, 0, 0); } /** * Moves the mouse to an offset from the center of the element. * The element is scrolled into view and its location is calculated using getBoundingClientRect. * @param target element to move to. * @param xOffset Offset from the center. A negative value means coordinates left from * the element. * @param yOffset Offset from the center. A negative value means coordinates above * the element. * @return A self reference. */ public Actions moveToElement(WebElement target, int xOffset, int yOffset) { if (isBuildingActions()) { action.addAction(new MoveToOffsetAction(jsonMouse, (Locatable) target, xOffset, yOffset)); } // Of course, this is the offset from the centre of the element. We have no idea what the width // and height are once we execute this method. LOG.info("When using the W3C Action commands, offsets are from the center of element"); return moveInTicks(target, xOffset, yOffset); } private Actions moveInTicks(WebElement target, int xOffset, int yOffset) { return tick(defaultMouse.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 coordinates * provided are outside the viewport (the mouse will end up outside the browser window) then * the viewport is scrolled to match. * @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) { if (isBuildingActions()) { action.addAction(new MoveToOffsetAction(jsonMouse, null, xOffset, yOffset)); } return tick(defaultMouse.createPointerMove(Duration.ofMillis(200), Origin.pointer(), xOffset, yOffset)); } /** * 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) { if (isBuildingActions()) { action.addAction(new ContextClickAction(jsonMouse, (Locatable) target)); } return moveInTicks(target, 0, 0).clickInTicks(RIGHT); } /** * Performs a context-click at the current mouse location. * @return A self reference. */ public Actions contextClick() { if (isBuildingActions()) { action.addAction(new ContextClickAction(jsonMouse, null)); } 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) { if (isBuildingActions()) { action.addAction(new ClickAndHoldAction(jsonMouse, (Locatable) source)); action.addAction(new MoveMouseAction(jsonMouse, (Locatable) target)); action.addAction(new ButtonReleaseAction(jsonMouse, (Locatable) target)); } return moveInTicks(source, 0, 0).tick(defaultMouse.createPointerDown(LEFT.asArg())) .moveInTicks(target, 0, 0).tick(defaultMouse.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) { if (isBuildingActions()) { action.addAction(new ClickAndHoldAction(jsonMouse, (Locatable) source)); action.addAction(new MoveToOffsetAction(jsonMouse, null, xOffset, yOffset)); action.addAction(new ButtonReleaseAction(jsonMouse, null)); } return moveInTicks(source, 0, 0).tick(defaultMouse.createPointerDown(LEFT.asArg())) .tick(defaultMouse.createPointerMove(Duration.ofMillis(250), Origin.pointer(), xOffset, yOffset)) .tick(defaultMouse.createPointerUp(LEFT.asArg())); } /** * Performs a pause. * * @param pause pause duration, in milliseconds. * @return A self reference. */ public Actions pause(long pause) { if (isBuildingActions()) { action.addAction(new PauseAction(pause)); } return tick(new Pause(defaultMouse, Duration.ofMillis(pause))); } public Actions pause(Duration duration) { Objects.requireNonNull(duration, "Duration of pause not set"); if (isBuildingActions()) { action.addAction(new PauseAction(duration.toMillis())); } return tick(new Pause(defaultMouse, duration)); } public Actions tick(Interaction... actions) { // All actions must be for a unique source. Set<InputSource> 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<InputSource> unseen = new HashSet<>(sequences.keySet()); unseen.removeAll(seenSources); for (InputSource source : unseen) { getSequence(source).addAction(new Pause(source, Duration.ZERO)); } return this; } public Actions tick(Action action) { if (!(action instanceof IsInteraction)) { throw new IllegalStateException("Expected action to implement IsInteraction"); } for (Interaction interaction : ((IsInteraction) action).asInteractions(defaultMouse, defaultKeyboard)) { tick(interaction); } if (isBuildingActions()) { this.action.addAction(action); } return this; } /** * Generates a composite action containing all actions so far, ready to be performed (and * resets the internal builder state, so subsequent calls to {@link #build()} will contain fresh * sequences). * * @return the composite action */ public Action build() { Action toReturn = new BuiltAction(driver, new LinkedHashMap<>(sequences), action); action = new CompositeAction(); 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; } private boolean isBuildingActions() { return jsonMouse != null || jsonKeyboard != null; } private static class BuiltAction implements Action { private final WebDriver driver; private final Map<InputSource, Sequence> sequences; private final Action fallBack; private BuiltAction(WebDriver driver, Map<InputSource, Sequence> sequences, Action fallBack) { this.driver = driver; this.sequences = sequences; this.fallBack = fallBack; } @Override public void perform() { if (driver == null) { // One of the deprecated constructors was used. Fall back to the old way for now. fallBack.perform(); return; } try { ((Interactive) driver).perform(sequences.values()); } catch (ClassCastException | UnsupportedCommandException e) { // Fall back to the old way of doing things. Old Skool #ftw fallBack.perform(); } } } }