# lib/webdriver/actionsequence.js

    1// Copyright 2012 Selenium comitters2// Copyright 2012 Software Freedom Conservancy3//4// Licensed under the Apache License, Version 2.0 (the "License");5// you may not use this file except in compliance with the License.6// You may obtain a copy of the License at7//8//      http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing, software11// distributed under the License is distributed on an "AS IS" BASIS,12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13// See the License for the specific language governing permissions and14// limitations under the License.1516goog.provide('webdriver.ActionSequence');1718goog.require('goog.array');19goog.require('webdriver.Button');20goog.require('webdriver.Command');21goog.require('webdriver.CommandName');22goog.require('webdriver.Key');23242526/**27 * Class for defining sequences of complex user interactions. Each sequence28 * will not be executed until {@link #perform} is called.29 *30 * <p>Example:<pre><code>31 *   new webdriver.ActionSequence(driver).32 *       keyDown(webdriver.Key.SHIFT).33 *       click(element1).34 *       click(element2).35 *       dragAndDrop(element3, element4).36 *       keyUp(webdriver.Key.SHIFT).37 *       perform();38 * </pre></code>39 *40 * @param {!webdriver.WebDriver} driver The driver instance to use.41 * @constructor42 */43webdriver.ActionSequence = function(driver) {4445  /** @private {!webdriver.WebDriver} */46  this.driver_ = driver;4748  /** @private {!Array.<{description: string, command: !webdriver.Command}>} */49  this.actions_ = [];50};515253/**54 * Schedules an action to be executed each time {@link #perform} is called on55 * this instance.56 * @param {string} description A description of the command.57 * @param {!webdriver.Command} command The command.58 * @private59 */60webdriver.ActionSequence.prototype.schedule_ = function(description, command) {61  this.actions_.push({62    description: description,63    command: command64  });65};666768/**69 * Executes this action sequence.70 * @return {!webdriver.promise.Promise} A promise that will be resolved once71 *     this sequence has completed.72 */73webdriver.ActionSequence.prototype.perform = function() {74  // Make a protected copy of the scheduled actions. This will protect against75  // users defining additional commands before this sequence is actually76  // executed.77  var actions = goog.array.clone(this.actions_);78  var driver = this.driver_;79  return driver.controlFlow().execute(function() {80    goog.array.forEach(actions, function(action) {81      driver.schedule(action.command, action.description);82    });83  }, 'ActionSequence.perform');84};858687/**88 * Moves the mouse.  The location to move to may be specified in terms of the89 * mouse's current location, an offset relative to the top-left corner of an90 * element, or an element (in which case the middle of the element is used).91 * @param {(!webdriver.WebElement|{x: number, y: number})} location The92 *     location to drag to, as either another WebElement or an offset in pixels.93 * @param {{x: number, y: number}=} opt_offset If the target {@code location}94 *     is defined as a {@link webdriver.WebElement}, this parameter defines an95 *     offset within that element. The offset should be specified in pixels96 *     relative to the top-left corner of the element's bounding box. If97 *     omitted, the element's center will be used as the target offset.98 * @return {!webdriver.ActionSequence} A self reference.99 */100webdriver.ActionSequence.prototype.mouseMove = function(location, opt_offset) {101  var command = new webdriver.Command(webdriver.CommandName.MOVE_TO);102103  if (goog.isNumber(location.x)) {104    setOffset(/** @type {{x: number, y: number}} */(location));105  } else {106    // The interactions API expect the element ID to be encoded as a simple107    // string, not the usual JSON object.108    var id = /** @type {!webdriver.WebElement} */ (location).toWireValue().109        then(function(value) {110          return value['ELEMENT'];111        });112    command.setParameter('element', id);113    if (opt_offset) {114      setOffset(opt_offset);115    }116  }117118  this.schedule_('mouseMove', command);119  return this;120121  /** @param {{x: number, y: number}} offset The offset to use. */122  function setOffset(offset) {123    command.setParameter('xoffset', offset.x || 0);124    command.setParameter('yoffset', offset.y || 0);125  }126};127128129/**130 * Schedules a mouse action.131 * @param {string} description A simple descriptive label for the scheduled132 *     action.133 * @param {!webdriver.CommandName} commandName The name of the command.134 * @param {(webdriver.WebElement|webdriver.Button)=} opt_elementOrButton Either135 *     the element to interact with or the button to click with.136 *     Defaults to {@link webdriver.Button.LEFT} if neither an element nor137 *     button is specified.138 * @param {webdriver.Button=} opt_button The button to use. Defaults to139 *     {@link webdriver.Button.LEFT}. Ignored if the previous argument is140 *     provided as a button.141 * @return {!webdriver.ActionSequence} A self reference.142 * @private143 */144webdriver.ActionSequence.prototype.scheduleMouseAction_ = function(145    description, commandName, opt_elementOrButton, opt_button) {146  var button;147  if (goog.isNumber(opt_elementOrButton)) {148    button = opt_elementOrButton;149  } else {150    if (opt_elementOrButton) {151      this.mouseMove(152          /** @type {!webdriver.WebElement} */ (opt_elementOrButton));153    }154    button = goog.isDef(opt_button) ? opt_button : webdriver.Button.LEFT;155  }156157  var command = new webdriver.Command(commandName).158      setParameter('button', button);159  this.schedule_(description, command);160  return this;161};162163164/**165 * Presses a mouse button. The mouse button will not be released until166 * {@link #mouseUp} is called, regardless of whether that call is made in this167 * sequence or another. The behavior for out-of-order events (e.g. mouseDown,168 * click) is undefined.169 *170 * <p>If an element is provided, the mouse will first be moved to the center171 * of that element. This is equivalent to:172 * <pre><code>sequence.mouseMove(element).mouseDown()</code></pre>173 *174 * <p>Warning: this method currently only supports the left mouse button. See175 * http://code.google.com/p/selenium/issues/detail?id=4047176 *177 * @param {(webdriver.WebElement|webdriver.Button)=} opt_elementOrButton Either178 *     the element to interact with or the button to click with.179 *     Defaults to {@link webdriver.Button.LEFT} if neither an element nor180 *     button is specified.181 * @param {webdriver.Button=} opt_button The button to use. Defaults to182 *     {@link webdriver.Button.LEFT}. Ignored if a button is provided as the183 *     first argument.184 * @return {!webdriver.ActionSequence} A self reference.185 */186webdriver.ActionSequence.prototype.mouseDown = function(opt_elementOrButton,187                                                        opt_button) {188  return this.scheduleMouseAction_('mouseDown',189      webdriver.CommandName.MOUSE_DOWN, opt_elementOrButton, opt_button);190};191192193/**194 * Releases a mouse button. Behavior is undefined for calling this function195 * without a previous call to {@link #mouseDown}.196 *197 * <p>If an element is provided, the mouse will first be moved to the center198 * of that element. This is equivalent to:199 * <pre><code>sequence.mouseMove(element).mouseUp()</code></pre>200 *201 * <p>Warning: this method currently only supports the left mouse button. See202 * http://code.google.com/p/selenium/issues/detail?id=4047203 *204 * @param {(webdriver.WebElement|webdriver.Button)=} opt_elementOrButton Either205 *     the element to interact with or the button to click with.206 *     Defaults to {@link webdriver.Button.LEFT} if neither an element nor207 *     button is specified.208 * @param {webdriver.Button=} opt_button The button to use. Defaults to209 *     {@link webdriver.Button.LEFT}. Ignored if a button is provided as the210 *     first argument.211 * @return {!webdriver.ActionSequence} A self reference.212 */213webdriver.ActionSequence.prototype.mouseUp = function(opt_elementOrButton,214                                                      opt_button) {215  return this.scheduleMouseAction_('mouseUp',216      webdriver.CommandName.MOUSE_UP, opt_elementOrButton, opt_button);217};218219220/**221 * Convenience function for performing a "drag and drop" manuever. The target222 * element may be moved to the location of another element, or by an offset (in223 * pixels).224 * @param {!webdriver.WebElement} element The element to drag.225 * @param {(!webdriver.WebElement|{x: number, y: number})} location The226 *     location to drag to, either as another WebElement or an offset in pixels.227 * @return {!webdriver.ActionSequence} A self reference.228 */229webdriver.ActionSequence.prototype.dragAndDrop = function(element, location) {230  return this.mouseDown(element).mouseMove(location).mouseUp();231};232233234/**235 * Clicks a mouse button.236 *237 * <p>If an element is provided, the mouse will first be moved to the center238 * of that element. This is equivalent to:239 * <pre><code>sequence.mouseMove(element).click()</code></pre>240 *241 * @param {(webdriver.WebElement|webdriver.Button)=} opt_elementOrButton Either242 *     the element to interact with or the button to click with.243 *     Defaults to {@link webdriver.Button.LEFT} if neither an element nor244 *     button is specified.245 * @param {webdriver.Button=} opt_button The button to use. Defaults to246 *     {@link webdriver.Button.LEFT}. Ignored if a button is provided as the247 *     first argument.248 * @return {!webdriver.ActionSequence} A self reference.249 */250webdriver.ActionSequence.prototype.click = function(opt_elementOrButton,251                                                    opt_button) {252  return this.scheduleMouseAction_('click',253      webdriver.CommandName.CLICK, opt_elementOrButton, opt_button);254};255256257/**258 * Double-clicks a mouse button.259 *260 * <p>If an element is provided, the mouse will first be moved to the center of261 * that element. This is equivalent to:262 * <pre><code>sequence.mouseMove(element).doubleClick()</code></pre>263 *264 * <p>Warning: this method currently only supports the left mouse button. See265 * http://code.google.com/p/selenium/issues/detail?id=4047266 *267 * @param {(webdriver.WebElement|webdriver.Button)=} opt_elementOrButton Either268 *     the element to interact with or the button to click with.269 *     Defaults to {@link webdriver.Button.LEFT} if neither an element nor270 *     button is specified.271 * @param {webdriver.Button=} opt_button The button to use. Defaults to272 *     {@link webdriver.Button.LEFT}. Ignored if a button is provided as the273 *     first argument.274 * @return {!webdriver.ActionSequence} A self reference.275 */276webdriver.ActionSequence.prototype.doubleClick = function(opt_elementOrButton,277                                                          opt_button) {278  return this.scheduleMouseAction_('doubleClick',279      webdriver.CommandName.DOUBLE_CLICK, opt_elementOrButton, opt_button);280};281282283/**284 * Schedules a keyboard action.285 * @param {string} description A simple descriptive label for the scheduled286 *     action.287 * @param {!Array.<(string|!webdriver.Key)>} keys The keys to send.288 * @return {!webdriver.ActionSequence} A self reference.289 * @private290 */291webdriver.ActionSequence.prototype.scheduleKeyboardAction_ = function(292    description, keys) {293  var command =294      new webdriver.Command(webdriver.CommandName.SEND_KEYS_TO_ACTIVE_ELEMENT).295          setParameter('value', keys);296  this.schedule_(description, command);297  return this;298};299300301/**302 * Checks that a key is a modifier key.303 * @param {!webdriver.Key} key The key to check.304 * @throws {Error} If the key is not a modifier key.305 * @private306 */307webdriver.ActionSequence.checkModifierKey_ = function(key) {308  if (key !== webdriver.Key.ALT && key !== webdriver.Key.CONTROL &&309      key !== webdriver.Key.SHIFT && key !== webdriver.Key.COMMAND) {310    throw Error('Not a modifier key');311  }312};313314315/**316 * Performs a modifier key press. The modifier key is <em>not released</em>317 * until {@link #keyUp} or {@link #sendKeys} is called. The key press will be318 * targetted at the currently focused element.319 * @param {!webdriver.Key} key The modifier key to push. Must be one of320 *     {ALT, CONTROL, SHIFT, COMMAND, META}.321 * @return {!webdriver.ActionSequence} A self reference.322 * @throws {Error} If the key is not a valid modifier key.323 */324webdriver.ActionSequence.prototype.keyDown = function(key) {325  webdriver.ActionSequence.checkModifierKey_(key);326  return this.scheduleKeyboardAction_('keyDown', [key]);327};328329330/**331 * Performs a modifier key release. The release is targetted at the currently332 * focused element.333 * @param {!webdriver.Key} key The modifier key to release. Must be one of334 *     {ALT, CONTROL, SHIFT, COMMAND, META}.335 * @return {!webdriver.ActionSequence} A self reference.336 * @throws {Error} If the key is not a valid modifier key.337 */338webdriver.ActionSequence.prototype.keyUp = function(key) {339  webdriver.ActionSequence.checkModifierKey_(key);340  return this.scheduleKeyboardAction_('keyUp', [key]);341};342343344/**345 * Simulates typing multiple keys. Each modifier key encountered in the346 * sequence will not be released until it is encountered again. All key events347 * will be targetted at the currently focused element.348 * @param {...(string|!webdriver.Key|!Array.<(string|!webdriver.Key)>)} var_args349 *     The keys to type.350 * @return {!webdriver.ActionSequence} A self reference.351 * @throws {Error} If the key is not a valid modifier key.352 */353webdriver.ActionSequence.prototype.sendKeys = function(var_args) {354  var keys = goog.array.flatten(goog.array.slice(arguments, 0));355  return this.scheduleKeyboardAction_('sendKeys', keys);356};