Skip to content

Commit

Permalink
[js] If the remote end indicates it does not support the new actions …
Browse files Browse the repository at this point in the history
…API,

translate actions to a command sequence against the legacy API and try again.

This change only supports translating mouse and keyboard actions. A subsequent
change will add support for translating touch pointers.

(For #4564)
  • Loading branch information
jleyba committed Dec 21, 2017
1 parent 9976795 commit 53f2cd3
Show file tree
Hide file tree
Showing 6 changed files with 409 additions and 72 deletions.
5 changes: 5 additions & 0 deletions javascript/node/selenium-webdriver/lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ const Name = {

ACTIONS: 'actions',
CLEAR_ACTIONS: 'clearActions',

LEGACY_ACTION_MOUSE_DOWN: 'legacyAction:mouseDown',
LEGACY_ACTION_MOUSE_UP: 'legacyAction:mouseUp',
LEGACY_ACTION_MOUSE_MOVE: 'legacyAction:mouseMove',
LEGACY_ACTION_SEND_KEYS: 'legacyAction:sendKeys',
};


Expand Down
4 changes: 4 additions & 0 deletions javascript/node/selenium-webdriver/lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ const COMMAND_MAP = new Map([
[cmd.Name.GET_AVAILABLE_LOG_TYPES, get('/session/:sessionId/log/types')],
[cmd.Name.GET_SESSION_LOGS, post('/logs')],
[cmd.Name.UPLOAD_FILE, post('/session/:sessionId/file')],
[cmd.Name.LEGACY_ACTION_MOUSE_DOWN, post('/session/:sessionId/buttondown')],
[cmd.Name.LEGACY_ACTION_MOUSE_UP, post('/session/:sessionId/buttonup')],
[cmd.Name.LEGACY_ACTION_MOUSE_MOVE, post('/session/:sessionId/moveto')],
[cmd.Name.LEGACY_ACTION_SEND_KEYS, post('/session/:sessionId/keys')],
]);


Expand Down
2 changes: 2 additions & 0 deletions javascript/node/selenium-webdriver/lib/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -556,8 +556,10 @@ class PointerSequence extends Sequence {


module.exports = {
ActionType,
Button,
Device,
DeviceType,
Key,
Keyboard,
KeySequence,
Expand Down
128 changes: 124 additions & 4 deletions javascript/node/selenium-webdriver/lib/webdriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -2515,16 +2515,136 @@ class ActionSequence {
* @return {!Promise<void>} a promise that will resolve when all actions have
* been completed.
*/
perform() {
async perform() {
let actions = [
this.keyboard_,
this.mouse_,
this.touch_
].filter(sequence => !sequence.isIdle());
return this.driver_.execute(
new command.Command(command.Name.ACTIONS)
.setParameter('actions', actions));

try {
await this.driver_.execute(
new command.Command(command.Name.ACTIONS)
.setParameter('actions', actions));
} catch (err) {
if (!(err instanceof error.UnknownCommandError)
&& !(err instanceof error.UnsupportedOperationError)) {
throw err;
}

const commands = await translateInputSequences(actions);
for (let cmd of commands) {
await this.driver_.execute(cmd);
}
}
}
}


const MODIFIER_KEYS = new Set([
input.Key.ALT,
input.Key.CONTROL,
input.Key.SHIFT,
input.Key.COMMAND
]);


/**
* Translates input sequences to commands against the legacy actions API.
* @param {!Array<!input.Sequence>} sequences The input sequences to translate.
* @return {!Promise<!Array<command.Command>>} The translated commands.
*/
async function translateInputSequences(sequences) {
let devices = await toWireValue(sequences);
if (!Array.isArray(devices)) {
throw TypeError(`expected an array, got: ${typeof devices}`);
}

const commands = [];
const maxLength =
devices.reduce((len, device) => Math.max(device.actions.length, len), 0);
for (let i = 0; i < maxLength; i++) {
let next;
for (let device of devices) {
if (device.type === input.DeviceType.POINTER
&& device.parameters.pointerType !== input.Pointer.Type.MOUSE) {
throw new error.UnsupportedOperationError(
`${device.parameters.pointerType} pointer not supported `
+ `by the legacy API`);
}

let action = device.actions[i];
if (!action || action.type === input.ActionType.PAUSE) {
continue;
}

if (next) {
throw new error.UnsupportedOperationError(
'Parallel action sequences are not supported for this browser');
} else {
next = action;
}

switch (action.type) {
case input.ActionType.KEY_DOWN: {
// If this action is a keydown for a non-modifier key, the next action
// must be a keyup for the same key, otherwise it cannot be translated
// to the legacy action API.
if (!MODIFIER_KEYS.has(action.value)) {
const np1 = device.actions[i + 1];
if (!np1
|| np1.type !== input.ActionType.KEY_UP
|| np1.value !== action.value) {
throw new error.UnsupportedOperationError(
'Unable to translate sequence to legacy API: keydown for '
+ `<${action.value}> must be followed by a keyup for the `
+ 'same key');
}
}
commands.push(
new command.Command(command.Name.LEGACY_ACTION_SEND_KEYS)
.setParameter('value', [action.value]));
break;
}
case input.ActionType.KEY_UP: {
// The legacy API always treats sendKeys for a non-modifier to be a
// keydown/up pair. For modifiers, the sendKeys toggles the key state,
// so we can omit any keyup actions for non-modifier keys.
if (MODIFIER_KEYS.has(action.value)) {
commands.push(
new command.Command(command.Name.LEGACY_ACTION_SEND_KEYS)
.setParameter('value', [action.value]));
}
break;
}
case input.ActionType.POINTER_DOWN:
commands.push(
new command.Command(command.Name.LEGACY_ACTION_MOUSE_DOWN)
.setParameter('button', action.button));
break;
case input.ActionType.POINTER_UP:
commands.push(
new command.Command(command.Name.LEGACY_ACTION_MOUSE_UP)
.setParameter('button', action.button));
break;
case input.ActionType.POINTER_MOVE: {
let cmd = new command.Command(command.Name.LEGACY_ACTION_MOUSE_MOVE)
.setParameter('xoffset', action.x)
.setParameter('yoffset', action.y);
if (WebElement.isId(action.origin)) {
cmd.setParameter('element', WebElement.extractId(action.origin));
}
commands.push(cmd);
break;
}
default:
throw new error.UnsupportedOperationError(
'Unable to translate action to legacy API: '
+ JSON.stringify(action));
}
}
}
return commands;
}


Expand Down
64 changes: 58 additions & 6 deletions javascript/node/selenium-webdriver/test/actions_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,36 @@

const assert = require('assert');

const {Browser, By, until} = require('..');
const test = require('../lib/test');
const error = require('../lib/error');
const fileServer = require('../lib/test/fileserver');
const test = require('../lib/test');
const {Key} = require('../lib/input');
const {Browser, By, until} = require('..');

test.suite(function(env) {
test.ignore(env.browsers(Browser.CHROME, Browser.SAFARI)).
test.ignore(env.browsers(Browser.SAFARI)).
describe('WebDriver.actions()', function() {
let driver;

before(async function() { driver = await env.builder().build(); });
afterEach(function() { return driver.actions().clear(); });
after(function() { return driver.quit(); });
before(async function() {
driver = await env.builder().build();
});

afterEach(async function() {
try {
await driver.actions().clear();
} catch (e) {
if (e instanceof error.UnsupportedOperationError
|| e instanceof error.UnknownCommandError) {
return;
}
throw e;
}
});

after(function() {
return driver.quit();
});

it('can move to and click element in an iframe', async function() {
await driver.get(fileServer.whereIs('click_tests/click_in_iframe.html'));
Expand All @@ -47,6 +65,40 @@ test.suite(function(env) {
return driver.wait(until.titleIs('Submitted Successfully!'), 5000);
});

it('can send keys to focused element', async function() {
await driver.get(test.Pages.formPage);

let el = await driver.findElement(By.id('email'));
assert.equal(await el.getAttribute('value'), '');

await driver.executeScript('arguments[0].focus()', el);

let actions = driver.actions();
actions.keyboard().sendKeys('foobar');
await actions.perform();

assert.equal(await el.getAttribute('value'), 'foobar');
});

it('can send keys to focused element (with modifiers)', async function() {
await driver.get(test.Pages.formPage);

let el = await driver.findElement(By.id('email'));
assert.equal(await el.getAttribute('value'), '');

await driver.executeScript('arguments[0].focus()', el);

let actions = driver.actions();
actions.keyboard().sendKeys('fo');
actions.keyboard().keyDown(Key.SHIFT);
actions.keyboard().sendKeys('OB');
actions.keyboard().keyUp(Key.SHIFT);
actions.keyboard().sendKeys('ar');
await actions.perform();

assert.equal(await el.getAttribute('value'), 'foOBar');
});

it('can interact with simple form elements', async function() {
await driver.get(test.Pages.formPage);

Expand Down
Loading

0 comments on commit 53f2cd3

Please sign in to comment.