blob: 0e34f3783084dcc4ac270b590b5ab2a67d880642 [file] [log] [blame]
// 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.
'use strict';
const command = require('./command');
const error = require('./error');
const input = require('./input');
/**
* @param {!IArrayLike} args .
* @return {!Array} .
*/
function flatten(args) {
let result = [];
for (let i = 0; i < args.length; i++) {
let element = args[i];
if (Array.isArray(element)) {
result.push.apply(result, flatten(element));
} else {
result.push(element);
}
}
return result;
}
const MODIFIER_KEYS = new Set([
input.Key.ALT,
input.Key.CONTROL,
input.Key.SHIFT,
input.Key.COMMAND
]);
/**
* Checks that a key is a modifier key.
* @param {!input.Key} key The key to check.
* @throws {error.InvalidArgumentError} If the key is not a modifier key.
* @private
*/
function checkModifierKey(key) {
if (!MODIFIER_KEYS.has(key)) {
throw new error.InvalidArgumentError('Not a modifier key');
}
}
/**
* Class for defining sequences of complex user interactions. Each sequence
* will not be executed until {@link #perform} is called.
*
* This class should not be instantiated directly. Instead, obtain an instance
* using {@link ./webdriver.WebDriver#actions() WebDriver.actions()}.
*
* Sample usage:
*
* driver.actions().
* keyDown(Key.SHIFT).
* click(element1).
* click(element2).
* dragAndDrop(element3, element4).
* keyUp(Key.SHIFT).
* perform();
*
*/
class ActionSequence {
/**
* @param {!./webdriver.WebDriver} driver The driver that should be used to
* perform this action sequence.
*/
constructor(driver) {
/** @private {!./webdriver.WebDriver} */
this.driver_ = driver;
/** @private {!Array<{description: string, command: !command.Command}>} */
this.actions_ = [];
}
/**
* Schedules an action to be executed each time {@link #perform} is called on
* this instance.
*
* @param {string} description A description of the command.
* @param {!command.Command} command The command.
* @private
*/
schedule_(description, command) {
this.actions_.push({
description: description,
command: command
});
}
/**
* Executes this action sequence.
*
* @return {!./promise.Thenable} A promise that will be resolved once
* this sequence has completed.
*/
perform() {
// Make a protected copy of the scheduled actions. This will protect against
// users defining additional commands before this sequence is actually
// executed.
let actions = this.actions_.concat();
let driver = this.driver_;
return driver.controlFlow().execute(function() {
let results = actions.map(action => {
return driver.schedule(action.command, action.description);
});
return Promise.all(results);
}, 'ActionSequence.perform');
}
/**
* Moves the mouse. The location to move to may be specified in terms of the
* mouse's current location, an offset relative to the top-left corner of an
* element, or an element (in which case the middle of the element is used).
*
* @param {(!./webdriver.WebElement|{x: number, y: number})} location The
* location to drag to, as either another WebElement or an offset in
* pixels.
* @param {{x: number, y: number}=} opt_offset If the target {@code location}
* is defined as a {@link ./webdriver.WebElement}, this parameter defines
* an offset within that element. The offset should be specified in pixels
* relative to the top-left corner of the element's bounding box. If
* omitted, the element's center will be used as the target offset.
* @return {!ActionSequence} A self reference.
*/
mouseMove(location, opt_offset) {
let cmd = new command.Command(command.Name.MOVE_TO);
if (typeof location.x === 'number') {
setOffset(/** @type {{x: number, y: number}} */(location));
} else {
cmd.setParameter('element', location.getId());
if (opt_offset) {
setOffset(opt_offset);
}
}
this.schedule_('mouseMove', cmd);
return this;
/** @param {{x: number, y: number}} offset The offset to use. */
function setOffset(offset) {
cmd.setParameter('xoffset', offset.x || 0);
cmd.setParameter('yoffset', offset.y || 0);
}
}
/**
* Schedules a mouse action.
* @param {string} description A simple descriptive label for the scheduled
* action.
* @param {!command.Name} commandName The name of the command.
* @param {(./webdriver.WebElement|input.Button)=} opt_elementOrButton Either
* the element to interact with or the button to click with.
* Defaults to {@link input.Button.LEFT} if neither an element nor
* button is specified.
* @param {input.Button=} opt_button The button to use. Defaults to
* {@link input.Button.LEFT}. Ignored if the previous argument is
* provided as a button.
* @return {!ActionSequence} A self reference.
* @private
*/
scheduleMouseAction_(
description, commandName, opt_elementOrButton, opt_button) {
let button;
if (typeof opt_elementOrButton === 'number') {
button = opt_elementOrButton;
} else {
if (opt_elementOrButton) {
this.mouseMove(
/** @type {!./webdriver.WebElement} */ (opt_elementOrButton));
}
button = opt_button !== void(0) ? opt_button : input.Button.LEFT;
}
let cmd = new command.Command(commandName).
setParameter('button', button);
this.schedule_(description, cmd);
return this;
}
/**
* Presses a mouse button. The mouse button will not be released until
* {@link #mouseUp} is called, regardless of whether that call is made in this
* sequence or another. The behavior for out-of-order events (e.g. mouseDown,
* click) is undefined.
*
* If an element is provided, the mouse will first be moved to the center
* of that element. This is equivalent to:
*
* sequence.mouseMove(element).mouseDown()
*
* Warning: this method currently only supports the left mouse button. See
* [issue 4047](http://code.google.com/p/selenium/issues/detail?id=4047).
*
* @param {(./webdriver.WebElement|input.Button)=} opt_elementOrButton Either
* the element to interact with or the button to click with.
* Defaults to {@link input.Button.LEFT} if neither an element nor
* button is specified.
* @param {input.Button=} opt_button The button to use. Defaults to
* {@link input.Button.LEFT}. Ignored if a button is provided as the
* first argument.
* @return {!ActionSequence} A self reference.
*/
mouseDown(opt_elementOrButton, opt_button) {
return this.scheduleMouseAction_('mouseDown',
command.Name.MOUSE_DOWN, opt_elementOrButton, opt_button);
}
/**
* Releases a mouse button. Behavior is undefined for calling this function
* without a previous call to {@link #mouseDown}.
*
* If an element is provided, the mouse will first be moved to the center
* of that element. This is equivalent to:
*
* sequence.mouseMove(element).mouseUp()
*
* Warning: this method currently only supports the left mouse button. See
* [issue 4047](http://code.google.com/p/selenium/issues/detail?id=4047).
*
* @param {(./webdriver.WebElement|input.Button)=} opt_elementOrButton Either
* the element to interact with or the button to click with.
* Defaults to {@link input.Button.LEFT} if neither an element nor
* button is specified.
* @param {input.Button=} opt_button The button to use. Defaults to
* {@link input.Button.LEFT}. Ignored if a button is provided as the
* first argument.
* @return {!ActionSequence} A self reference.
*/
mouseUp(opt_elementOrButton, opt_button) {
return this.scheduleMouseAction_('mouseUp',
command.Name.MOUSE_UP, opt_elementOrButton, opt_button);
}
/**
* Convenience function for performing a "drag and drop" manuever. The target
* element may be moved to the location of another element, or by an offset (in
* pixels).
*
* @param {!./webdriver.WebElement} element The element to drag.
* @param {(!./webdriver.WebElement|{x: number, y: number})} location The
* location to drag to, either as another WebElement or an offset in
* pixels.
* @return {!ActionSequence} A self reference.
*/
dragAndDrop(element, location) {
return this.mouseDown(element).mouseMove(location).mouseUp();
}
/**
* Clicks a mouse button.
*
* If an element is provided, the mouse will first be moved to the center
* of that element. This is equivalent to:
*
* sequence.mouseMove(element).click()
*
* @param {(./webdriver.WebElement|input.Button)=} opt_elementOrButton Either
* the element to interact with or the button to click with.
* Defaults to {@link input.Button.LEFT} if neither an element nor
* button is specified.
* @param {input.Button=} opt_button The button to use. Defaults to
* {@link input.Button.LEFT}. Ignored if a button is provided as the
* first argument.
* @return {!ActionSequence} A self reference.
*/
click(opt_elementOrButton, opt_button) {
return this.scheduleMouseAction_('click',
command.Name.CLICK, opt_elementOrButton, opt_button);
}
/**
* Double-clicks a mouse button.
*
* If an element is provided, the mouse will first be moved to the center of
* that element. This is equivalent to:
*
* sequence.mouseMove(element).doubleClick()
*
* Warning: this method currently only supports the left mouse button. See
* [issue 4047](http://code.google.com/p/selenium/issues/detail?id=4047).
*
* @param {(./webdriver.WebElement|input.Button)=} opt_elementOrButton Either
* the element to interact with or the button to click with.
* Defaults to {@link input.Button.LEFT} if neither an element nor
* button is specified.
* @param {input.Button=} opt_button The button to use. Defaults to
* {@link input.Button.LEFT}. Ignored if a button is provided as the
* first argument.
* @return {!ActionSequence} A self reference.
*/
doubleClick(opt_elementOrButton, opt_button) {
return this.scheduleMouseAction_('doubleClick',
command.Name.DOUBLE_CLICK, opt_elementOrButton, opt_button);
}
/**
* Schedules a keyboard action.
*
* @param {string} description A simple descriptive label for the scheduled
* action.
* @param {!Array<(string|!input.Key)>} keys The keys to send.
* @return {!ActionSequence} A self reference.
* @private
*/
scheduleKeyboardAction_(description, keys) {
let cmd = new command.Command(command.Name.SEND_KEYS_TO_ACTIVE_ELEMENT)
.setParameter('value', keys);
this.schedule_(description, cmd);
return this;
}
/**
* Performs a modifier key press. The modifier key is <em>not released</em>
* until {@link #keyUp} or {@link #sendKeys} is called. The key press will be
* targeted at the currently focused element.
*
* @param {!input.Key} key The modifier key to push. Must be one of
* {ALT, CONTROL, SHIFT, COMMAND, META}.
* @return {!ActionSequence} A self reference.
* @throws {error.InvalidArgumentError} If the key is not a valid modifier
* key.
*/
keyDown(key) {
checkModifierKey(key);
return this.scheduleKeyboardAction_('keyDown', [key]);
}
/**
* Performs a modifier key release. The release is targeted at the currently
* focused element.
* @param {!input.Key} key The modifier key to release. Must be one of
* {ALT, CONTROL, SHIFT, COMMAND, META}.
* @return {!ActionSequence} A self reference.
* @throws {error.InvalidArgumentError} If the key is not a valid modifier
* key.
*/
keyUp(key) {
checkModifierKey(key);
return this.scheduleKeyboardAction_('keyUp', [key]);
}
/**
* Simulates typing multiple keys. Each modifier key encountered in the
* sequence will not be released until it is encountered again. All key events
* will be targeted at the currently focused element.
*
* @param {...(string|!input.Key|!Array<(string|!input.Key)>)} var_args
* The keys to type.
* @return {!ActionSequence} A self reference.
* @throws {Error} If the key is not a valid modifier key.
*/
sendKeys(var_args) {
let keys = flatten(arguments);
return this.scheduleKeyboardAction_('sendKeys', keys);
}
}
/**
* Class for defining sequences of user touch interactions. Each sequence
* will not be executed until {@link #perform} is called.
*
* This class should not be instantiated directly. Instead, obtain an instance
* using {@link ./webdriver.WebDriver#touchActions() WebDriver.touchActions()}.
*
* Sample usage:
*
* driver.touchActions().
* tapAndHold({x: 0, y: 0}).
* move({x: 3, y: 4}).
* release({x: 10, y: 10}).
* perform();
*
*/
class TouchSequence {
/**
* @param {!./webdriver.WebDriver} driver The driver that should be used to
* perform this action sequence.
*/
constructor(driver) {
/** @private {!./webdriver.WebDriver} */
this.driver_ = driver;
/** @private {!Array<{description: string, command: !command.Command}>} */
this.actions_ = [];
}
/**
* Schedules an action to be executed each time {@link #perform} is called on
* this instance.
* @param {string} description A description of the command.
* @param {!command.Command} command The command.
* @private
*/
schedule_(description, command) {
this.actions_.push({
description: description,
command: command
});
}
/**
* Executes this action sequence.
* @return {!./promise.Thenable} A promise that will be resolved once
* this sequence has completed.
*/
perform() {
// Make a protected copy of the scheduled actions. This will protect against
// users defining additional commands before this sequence is actually
// executed.
let actions = this.actions_.concat();
let driver = this.driver_;
return driver.controlFlow().execute(function() {
let results = actions.map(action => {
return driver.schedule(action.command, action.description);
});
return Promise.all(results);
}, 'TouchSequence.perform');
}
/**
* Taps an element.
*
* @param {!./webdriver.WebElement} elem The element to tap.
* @return {!TouchSequence} A self reference.
*/
tap(elem) {
let cmd = new command.Command(command.Name.TOUCH_SINGLE_TAP).
setParameter('element', elem.getId());
this.schedule_('tap', cmd);
return this;
}
/**
* Double taps an element.
*
* @param {!./webdriver.WebElement} elem The element to double tap.
* @return {!TouchSequence} A self reference.
*/
doubleTap(elem) {
let cmd = new command.Command(command.Name.TOUCH_DOUBLE_TAP).
setParameter('element', elem.getId());
this.schedule_('doubleTap', cmd);
return this;
}
/**
* Long press on an element.
*
* @param {!./webdriver.WebElement} elem The element to long press.
* @return {!TouchSequence} A self reference.
*/
longPress(elem) {
let cmd = new command.Command(command.Name.TOUCH_LONG_PRESS).
setParameter('element', elem.getId());
this.schedule_('longPress', cmd);
return this;
}
/**
* Touch down at the given location.
*
* @param {{x: number, y: number}} location The location to touch down at.
* @return {!TouchSequence} A self reference.
*/
tapAndHold(location) {
let cmd = new command.Command(command.Name.TOUCH_DOWN).
setParameter('x', location.x).
setParameter('y', location.y);
this.schedule_('tapAndHold', cmd);
return this;
}
/**
* Move a held {@linkplain #tapAndHold touch} to the specified location.
*
* @param {{x: number, y: number}} location The location to move to.
* @return {!TouchSequence} A self reference.
*/
move(location) {
let cmd = new command.Command(command.Name.TOUCH_MOVE).
setParameter('x', location.x).
setParameter('y', location.y);
this.schedule_('move', cmd);
return this;
}
/**
* Release a held {@linkplain #tapAndHold touch} at the specified location.
*
* @param {{x: number, y: number}} location The location to release at.
* @return {!TouchSequence} A self reference.
*/
release(location) {
let cmd = new command.Command(command.Name.TOUCH_UP).
setParameter('x', location.x).
setParameter('y', location.y);
this.schedule_('release', cmd);
return this;
}
/**
* Scrolls the touch screen by the given offset.
*
* @param {{x: number, y: number}} offset The offset to scroll to.
* @return {!TouchSequence} A self reference.
*/
scroll(offset) {
let cmd = new command.Command(command.Name.TOUCH_SCROLL).
setParameter('xoffset', offset.x).
setParameter('yoffset', offset.y);
this.schedule_('scroll', cmd);
return this;
}
/**
* Scrolls the touch screen, starting on `elem` and moving by the specified
* offset.
*
* @param {!./webdriver.WebElement} elem The element where scroll starts.
* @param {{x: number, y: number}} offset The offset to scroll to.
* @return {!TouchSequence} A self reference.
*/
scrollFromElement(elem, offset) {
let cmd = new command.Command(command.Name.TOUCH_SCROLL).
setParameter('element', elem.getId()).
setParameter('xoffset', offset.x).
setParameter('yoffset', offset.y);
this.schedule_('scrollFromElement', cmd);
return this;
}
/**
* Flick, starting anywhere on the screen, at speed xspeed and yspeed.
*
* @param {{xspeed: number, yspeed: number}} speed The speed to flick in each
direction, in pixels per second.
* @return {!TouchSequence} A self reference.
*/
flick(speed) {
let cmd = new command.Command(command.Name.TOUCH_FLICK).
setParameter('xspeed', speed.xspeed).
setParameter('yspeed', speed.yspeed);
this.schedule_('flick', cmd);
return this;
}
/**
* Flick starting at elem and moving by x and y at specified speed.
*
* @param {!./webdriver.WebElement} elem The element where flick starts.
* @param {{x: number, y: number}} offset The offset to flick to.
* @param {number} speed The speed to flick at in pixels per second.
* @return {!TouchSequence} A self reference.
*/
flickElement(elem, offset, speed) {
let cmd = new command.Command(command.Name.TOUCH_FLICK).
setParameter('element', elem.getId()).
setParameter('xoffset', offset.x).
setParameter('yoffset', offset.y).
setParameter('speed', speed);
this.schedule_('flickElement', cmd);
return this;
}
}
// PUBLIC API
module.exports = {
ActionSequence: ActionSequence,
TouchSequence: TouchSequence,
};