123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604 |
- // 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,
- };
|