Skip to content

Commit af370f5

Browse files
committed
[js] Expand on ActionSequence documentation and make it easier to define and
execute a single device sequence. For SeleniumHQ#4564
1 parent 53f2cd3 commit af370f5

4 files changed

Lines changed: 144 additions & 29 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ const Name = {
196196

197197
/**
198198
* Handles the execution of WebDriver {@link Command commands}.
199-
* @interface
199+
* @record
200200
*/
201201
class Executor {
202202
/**

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

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -289,15 +289,32 @@ const ActionType = {
289289
* @see <https://www.w3.org/TR/webdriver/#actions>
290290
*/
291291
class Sequence {
292-
/** @param {!Device} device the device to generate this action sequence. */
293-
constructor(device) {
292+
/**
293+
* @param {!Device} device the device to generate this action sequence.
294+
* @param {function(): !Promise<void>} onPerform the function to call to
295+
* actually handle class to {@link #perform}.
296+
*/
297+
constructor(device, onPerform) {
294298
/** @private @const */
295299
this.device_ = device;
296300

301+
/** @private @const */
302+
this.onPerform_ = onPerform;
303+
297304
/** @private @const {!Array<!Action>} */
298305
this.actions_ = [];
299306
}
300307

308+
/**
309+
* Executes this sequence.
310+
*
311+
* @return {!Promise<void>} a promise that will resolve when the sequence
312+
* has finished executing.
313+
*/
314+
perform() {
315+
return (0, this.onPerform_)();
316+
}
317+
301318
/**
302319
* Clears all actions in this sequence.
303320
* @final
@@ -352,9 +369,13 @@ class Sequence {
352369
* @final
353370
*/
354371
class KeySequence extends Sequence {
355-
/** @param {!Keyboard} keyboard the keyboard to use. */
356-
constructor(keyboard) {
357-
super(keyboard);
372+
/**
373+
* @param {!Keyboard} keyboard the keyboard to use.
374+
* @param {function(): !Promise<void>} onPerform the function to call to
375+
* actually handle class to {@link #perform}.
376+
*/
377+
constructor(keyboard, onPerform) {
378+
super(keyboard, onPerform);
358379
}
359380

360381
/**
@@ -457,9 +478,13 @@ const Origin = {
457478
* @final
458479
*/
459480
class PointerSequence extends Sequence {
460-
/** @param {!Pointer} pointer the pointer to use. */
461-
constructor(pointer) {
462-
super(pointer);
481+
/**
482+
* @param {!Pointer} pointer the pointer to use.
483+
* @param {function(): !Promise<void>} onPerform the function to call to
484+
* actually handle class to {@link #perform}.
485+
*/
486+
constructor(pointer, onPerform) {
487+
super(pointer, onPerform);
463488
}
464489

465490
/**

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

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2410,9 +2410,30 @@ class AlertPromise extends Alert {
24102410
*
24112411
* This input sequence can be produced using the ActionSequence API with
24122412
* the code below. Actions must be defined individually for each device and
2413-
* explicitly kept in sync by the user. The list of actions for the mouse is
2414-
* far longer than the keyboard. Rather than keeping track of the number of
2415-
* steps for the keyboard to pause, there is a single call to
2413+
* explicitly kept in sync by the user.
2414+
*
2415+
* let actions = driver.actions();
2416+
* actions.keyboard().keyDown(Key.SHIFT);
2417+
* actions.mouse().click(element1).click(element2);
2418+
*
2419+
* // Insert no-op pauses so the keyboard does not act again until the
2420+
* // mouse has finished it's sequence. Note, we allow the mouse to move
2421+
* // to element1 during tick 1.
2422+
* action.keyboard()
2423+
* .pause() // mouse down
2424+
* .pause() // mouse up
2425+
* .pause() // move to element 2
2426+
* .pause() // mouse down
2427+
* .pause() // mouse up
2428+
* .keyUp(Key.SHIFT);
2429+
*
2430+
* actions.perform();
2431+
*
2432+
* Notice that `mouse().click(element1)` is a compound action of
2433+
* "move to element 1", "mouse down", "mouse up". This generates a sequence
2434+
* far longer than the keyboard, so we have to insert pauses so the keyboard
2435+
* is idle until the mouse has finished. Rather than trying to keep track of
2436+
* the number of steps to pause, we can use a single call to
24162437
* {@link #synchronize()}. This instructs the ActionSequence to ensure the
24172438
* input sequence for all devices are the same length - inserting explicit
24182439
* {@linkplain input.Sequence#pause pauses} as necessary.
@@ -2427,31 +2448,50 @@ class AlertPromise extends Alert {
24272448
* actions.keyboard().keyUp(Key.SHIFT);
24282449
* actions.perform();
24292450
*
2451+
*
2452+
* > __N.B.__ not all browsers support parallel action sequences that define
2453+
* > actions for multiple devices at a single tick. Our example above is one
2454+
* > such sequence: the very first tick moves the mouse while press the SHIFT
2455+
* > key. For maximum portability, users must ensure only one action is defined
2456+
* > at each tick.
2457+
*
2458+
* > __TIP:__ Action sequences involving a single device can be defined and
2459+
* > executed in a single, fluent statement by calling
2460+
* > {@linkplain input.Sequence#perform perform} on the specific device you wish
2461+
* > to execute:
2462+
* >
2463+
* > driver.actions().keyboard().sendKeys('hello').perform();
2464+
* >
2465+
*
24302466
* @final
24312467
* @see <https://www.w3.org/TR/webdriver/#actions>
24322468
*/
24332469
class ActionSequence {
24342470
/**
2435-
* @param {!IWebDriver} driver The driver instance to execute the configured
2471+
* @param {!command.Executor} executor The object to execute the configured
24362472
* actions with.
24372473
*/
2438-
constructor(driver) {
2474+
constructor(executor) {
24392475
/** @private @const */
2440-
this.driver_ = driver;
2476+
this.executor_ = executor;
24412477

24422478
/** @private @const */
24432479
this.keyboard_ =
2444-
new input.KeySequence(new input.Keyboard('default keyboard'));
2480+
new input.KeySequence(
2481+
new input.Keyboard('default keyboard'),
2482+
() => this.perform(this.keyboard_));
24452483

24462484
/** @private @const */
24472485
this.mouse_ =
24482486
new input.PointerSequence(
2449-
new input.Pointer('default mouse', input.Pointer.Type.MOUSE));
2487+
new input.Pointer('default mouse', input.Pointer.Type.MOUSE),
2488+
() => this.perform(this.mouse_));
24502489

24512490
/** @private @const */
24522491
this.touch_ =
24532492
new input.PointerSequence(
2454-
new input.Pointer('default touch', input.Pointer.Type.TOUCH));
2493+
new input.Pointer('default touch', input.Pointer.Type.TOUCH),
2494+
() => this.perform(this.touch_));
24552495
}
24562496

24572497
/**
@@ -2505,36 +2545,41 @@ class ActionSequence {
25052545
this.keyboard_.clear();
25062546
this.mouse_.clear();
25072547
this.touch_.clear();
2508-
return this.driver_.execute(
2548+
return this.executor_.execute(
25092549
new command.Command(command.Name.CLEAR_ACTIONS));
25102550
}
25112551

25122552
/**
25132553
* Performs the configured action sequence.
25142554
*
2555+
* @param {input.Sequence=} device the specific device sequence to execute.
2556+
* If unspecified, this will default to executing the sequence for every
2557+
* defined device.
25152558
* @return {!Promise<void>} a promise that will resolve when all actions have
25162559
* been completed.
25172560
*/
2518-
async perform() {
2519-
let actions = [
2520-
this.keyboard_,
2521-
this.mouse_,
2522-
this.touch_
2523-
].filter(sequence => !sequence.isIdle());
2561+
async perform(device) {
2562+
let devices =
2563+
device ? [device] : [this.keyboard_, this.mouse_, this.touch_];
2564+
2565+
devices = devices.filter(d => !d.isIdle());
2566+
if (devices.length === 0) {
2567+
return Promise.resolve();
2568+
}
25242569

25252570
try {
2526-
await this.driver_.execute(
2571+
await this.executor_.execute(
25272572
new command.Command(command.Name.ACTIONS)
2528-
.setParameter('actions', actions));
2573+
.setParameter('actions', devices));
25292574
} catch (err) {
25302575
if (!(err instanceof error.UnknownCommandError)
25312576
&& !(err instanceof error.UnsupportedOperationError)) {
25322577
throw err;
25332578
}
25342579

2535-
const commands = await translateInputSequences(actions);
2580+
const commands = await translateInputSequences(devices);
25362581
for (let cmd of commands) {
2537-
await this.driver_.execute(cmd);
2582+
await this.executor_.execute(cmd);
25382583
}
25392584
}
25402585
}

‎javascript/node/selenium-webdriver/test/lib/webdriver_test.js‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1568,6 +1568,25 @@ describe('WebDriver', function() {
15681568
(e) => assert.ok(e instanceof error.UnsupportedOperationError));
15691569
});
15701570
});
1571+
1572+
it('perform() only executes keyboard sequence', function() {
1573+
let executor = new FakeExecutor()
1574+
.expect(CName.ACTIONS, {
1575+
actions: [{
1576+
type: 'key',
1577+
id: 'default keyboard',
1578+
actions: [
1579+
{type: 'keyDown', value: 'a'}, {type: 'keyUp', value: 'a'},
1580+
]
1581+
}]
1582+
})
1583+
.end();
1584+
1585+
let driver = executor.createDriver();
1586+
let actions = driver.actions();
1587+
actions.mouse().pointerMove({x: 1, y: 2});
1588+
return actions.keyboard().sendKeys('a').perform();
1589+
});
15711590
});
15721591

15731592
describe('mouse()', function() {
@@ -1617,6 +1636,32 @@ describe('WebDriver', function() {
16171636
actions.mouse().doubleClick(element1);
16181637
return actions.perform();
16191638
});
1639+
1640+
it('perform() only executes mouse sequence', function() {
1641+
let executor = new FakeExecutor()
1642+
.expect(CName.ACTIONS, {
1643+
actions: [{
1644+
type: 'pointer',
1645+
id: 'default mouse',
1646+
parameters: {
1647+
pointerType: 'mouse',
1648+
},
1649+
actions: [{
1650+
type: 'pointerMove',
1651+
origin: 'viewport',
1652+
duration: 100,
1653+
x: 1,
1654+
y: 2
1655+
}]
1656+
}]
1657+
})
1658+
.end();
1659+
1660+
let driver = executor.createDriver();
1661+
let actions = driver.actions();
1662+
actions.keyboard().sendKeys('a');
1663+
return actions.mouse().pointerMove({x: 1, y: 2}).perform();
1664+
});
16201665
});
16211666

16221667
describe('multi-device action sequence', function() {

0 commit comments

Comments
 (0)