Skip to content

Commit 53f2cd3

Browse files
committed
[js] If the remote end indicates it does not support the new actions 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)
1 parent 9976795 commit 53f2cd3

File tree

6 files changed

+409
-72
lines changed

6 files changed

+409
-72
lines changed

javascript/node/selenium-webdriver/lib/command.js

+5
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,11 @@ const Name = {
185185

186186
ACTIONS: 'actions',
187187
CLEAR_ACTIONS: 'clearActions',
188+
189+
LEGACY_ACTION_MOUSE_DOWN: 'legacyAction:mouseDown',
190+
LEGACY_ACTION_MOUSE_UP: 'legacyAction:mouseUp',
191+
LEGACY_ACTION_MOUSE_MOVE: 'legacyAction:mouseMove',
192+
LEGACY_ACTION_SEND_KEYS: 'legacyAction:sendKeys',
188193
};
189194

190195

javascript/node/selenium-webdriver/lib/http.js

+4
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ const COMMAND_MAP = new Map([
231231
[cmd.Name.GET_AVAILABLE_LOG_TYPES, get('/session/:sessionId/log/types')],
232232
[cmd.Name.GET_SESSION_LOGS, post('/logs')],
233233
[cmd.Name.UPLOAD_FILE, post('/session/:sessionId/file')],
234+
[cmd.Name.LEGACY_ACTION_MOUSE_DOWN, post('/session/:sessionId/buttondown')],
235+
[cmd.Name.LEGACY_ACTION_MOUSE_UP, post('/session/:sessionId/buttonup')],
236+
[cmd.Name.LEGACY_ACTION_MOUSE_MOVE, post('/session/:sessionId/moveto')],
237+
[cmd.Name.LEGACY_ACTION_SEND_KEYS, post('/session/:sessionId/keys')],
234238
]);
235239

236240

javascript/node/selenium-webdriver/lib/input.js

+2
Original file line numberDiff line numberDiff line change
@@ -556,8 +556,10 @@ class PointerSequence extends Sequence {
556556

557557

558558
module.exports = {
559+
ActionType,
559560
Button,
560561
Device,
562+
DeviceType,
561563
Key,
562564
Keyboard,
563565
KeySequence,

javascript/node/selenium-webdriver/lib/webdriver.js

+124-4
Original file line numberDiff line numberDiff line change
@@ -2515,16 +2515,136 @@ class ActionSequence {
25152515
* @return {!Promise<void>} a promise that will resolve when all actions have
25162516
* been completed.
25172517
*/
2518-
perform() {
2518+
async perform() {
25192519
let actions = [
25202520
this.keyboard_,
25212521
this.mouse_,
25222522
this.touch_
25232523
].filter(sequence => !sequence.isIdle());
2524-
return this.driver_.execute(
2525-
new command.Command(command.Name.ACTIONS)
2526-
.setParameter('actions', actions));
2524+
2525+
try {
2526+
await this.driver_.execute(
2527+
new command.Command(command.Name.ACTIONS)
2528+
.setParameter('actions', actions));
2529+
} catch (err) {
2530+
if (!(err instanceof error.UnknownCommandError)
2531+
&& !(err instanceof error.UnsupportedOperationError)) {
2532+
throw err;
2533+
}
2534+
2535+
const commands = await translateInputSequences(actions);
2536+
for (let cmd of commands) {
2537+
await this.driver_.execute(cmd);
2538+
}
2539+
}
2540+
}
2541+
}
2542+
2543+
2544+
const MODIFIER_KEYS = new Set([
2545+
input.Key.ALT,
2546+
input.Key.CONTROL,
2547+
input.Key.SHIFT,
2548+
input.Key.COMMAND
2549+
]);
2550+
2551+
2552+
/**
2553+
* Translates input sequences to commands against the legacy actions API.
2554+
* @param {!Array<!input.Sequence>} sequences The input sequences to translate.
2555+
* @return {!Promise<!Array<command.Command>>} The translated commands.
2556+
*/
2557+
async function translateInputSequences(sequences) {
2558+
let devices = await toWireValue(sequences);
2559+
if (!Array.isArray(devices)) {
2560+
throw TypeError(`expected an array, got: ${typeof devices}`);
2561+
}
2562+
2563+
const commands = [];
2564+
const maxLength =
2565+
devices.reduce((len, device) => Math.max(device.actions.length, len), 0);
2566+
for (let i = 0; i < maxLength; i++) {
2567+
let next;
2568+
for (let device of devices) {
2569+
if (device.type === input.DeviceType.POINTER
2570+
&& device.parameters.pointerType !== input.Pointer.Type.MOUSE) {
2571+
throw new error.UnsupportedOperationError(
2572+
`${device.parameters.pointerType} pointer not supported `
2573+
+ `by the legacy API`);
2574+
}
2575+
2576+
let action = device.actions[i];
2577+
if (!action || action.type === input.ActionType.PAUSE) {
2578+
continue;
2579+
}
2580+
2581+
if (next) {
2582+
throw new error.UnsupportedOperationError(
2583+
'Parallel action sequences are not supported for this browser');
2584+
} else {
2585+
next = action;
2586+
}
2587+
2588+
switch (action.type) {
2589+
case input.ActionType.KEY_DOWN: {
2590+
// If this action is a keydown for a non-modifier key, the next action
2591+
// must be a keyup for the same key, otherwise it cannot be translated
2592+
// to the legacy action API.
2593+
if (!MODIFIER_KEYS.has(action.value)) {
2594+
const np1 = device.actions[i + 1];
2595+
if (!np1
2596+
|| np1.type !== input.ActionType.KEY_UP
2597+
|| np1.value !== action.value) {
2598+
throw new error.UnsupportedOperationError(
2599+
'Unable to translate sequence to legacy API: keydown for '
2600+
+ `<${action.value}> must be followed by a keyup for the `
2601+
+ 'same key');
2602+
}
2603+
}
2604+
commands.push(
2605+
new command.Command(command.Name.LEGACY_ACTION_SEND_KEYS)
2606+
.setParameter('value', [action.value]));
2607+
break;
2608+
}
2609+
case input.ActionType.KEY_UP: {
2610+
// The legacy API always treats sendKeys for a non-modifier to be a
2611+
// keydown/up pair. For modifiers, the sendKeys toggles the key state,
2612+
// so we can omit any keyup actions for non-modifier keys.
2613+
if (MODIFIER_KEYS.has(action.value)) {
2614+
commands.push(
2615+
new command.Command(command.Name.LEGACY_ACTION_SEND_KEYS)
2616+
.setParameter('value', [action.value]));
2617+
}
2618+
break;
2619+
}
2620+
case input.ActionType.POINTER_DOWN:
2621+
commands.push(
2622+
new command.Command(command.Name.LEGACY_ACTION_MOUSE_DOWN)
2623+
.setParameter('button', action.button));
2624+
break;
2625+
case input.ActionType.POINTER_UP:
2626+
commands.push(
2627+
new command.Command(command.Name.LEGACY_ACTION_MOUSE_UP)
2628+
.setParameter('button', action.button));
2629+
break;
2630+
case input.ActionType.POINTER_MOVE: {
2631+
let cmd = new command.Command(command.Name.LEGACY_ACTION_MOUSE_MOVE)
2632+
.setParameter('xoffset', action.x)
2633+
.setParameter('yoffset', action.y);
2634+
if (WebElement.isId(action.origin)) {
2635+
cmd.setParameter('element', WebElement.extractId(action.origin));
2636+
}
2637+
commands.push(cmd);
2638+
break;
2639+
}
2640+
default:
2641+
throw new error.UnsupportedOperationError(
2642+
'Unable to translate action to legacy API: '
2643+
+ JSON.stringify(action));
2644+
}
2645+
}
25272646
}
2647+
return commands;
25282648
}
25292649

25302650

javascript/node/selenium-webdriver/test/actions_test.js

+58-6
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,36 @@
1919

2020
const assert = require('assert');
2121

22-
const {Browser, By, until} = require('..');
23-
const test = require('../lib/test');
22+
const error = require('../lib/error');
2423
const fileServer = require('../lib/test/fileserver');
24+
const test = require('../lib/test');
25+
const {Key} = require('../lib/input');
26+
const {Browser, By, until} = require('..');
2527

2628
test.suite(function(env) {
27-
test.ignore(env.browsers(Browser.CHROME, Browser.SAFARI)).
29+
test.ignore(env.browsers(Browser.SAFARI)).
2830
describe('WebDriver.actions()', function() {
2931
let driver;
3032

31-
before(async function() { driver = await env.builder().build(); });
32-
afterEach(function() { return driver.actions().clear(); });
33-
after(function() { return driver.quit(); });
33+
before(async function() {
34+
driver = await env.builder().build();
35+
});
36+
37+
afterEach(async function() {
38+
try {
39+
await driver.actions().clear();
40+
} catch (e) {
41+
if (e instanceof error.UnsupportedOperationError
42+
|| e instanceof error.UnknownCommandError) {
43+
return;
44+
}
45+
throw e;
46+
}
47+
});
48+
49+
after(function() {
50+
return driver.quit();
51+
});
3452

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

68+
it('can send keys to focused element', async function() {
69+
await driver.get(test.Pages.formPage);
70+
71+
let el = await driver.findElement(By.id('email'));
72+
assert.equal(await el.getAttribute('value'), '');
73+
74+
await driver.executeScript('arguments[0].focus()', el);
75+
76+
let actions = driver.actions();
77+
actions.keyboard().sendKeys('foobar');
78+
await actions.perform();
79+
80+
assert.equal(await el.getAttribute('value'), 'foobar');
81+
});
82+
83+
it('can send keys to focused element (with modifiers)', async function() {
84+
await driver.get(test.Pages.formPage);
85+
86+
let el = await driver.findElement(By.id('email'));
87+
assert.equal(await el.getAttribute('value'), '');
88+
89+
await driver.executeScript('arguments[0].focus()', el);
90+
91+
let actions = driver.actions();
92+
actions.keyboard().sendKeys('fo');
93+
actions.keyboard().keyDown(Key.SHIFT);
94+
actions.keyboard().sendKeys('OB');
95+
actions.keyboard().keyUp(Key.SHIFT);
96+
actions.keyboard().sendKeys('ar');
97+
await actions.perform();
98+
99+
assert.equal(await el.getAttribute('value'), 'foOBar');
100+
});
101+
50102
it('can interact with simple form elements', async function() {
51103
await driver.get(test.Pages.formPage);
52104

0 commit comments

Comments
 (0)