alecxe alecxe - 1 month ago 9
Javascript Question

Custom browser actions in Protractor

The problem:

In one of our tests we have a "long click"/"click and hold" functionality that we solve by using:

browser.actions().mouseDown(element).perform();
browser.sleep(5000);
browser.actions().mouseUp(element).perform();


Which we would like to ideally solve in one line by having
sleep()
a part of the action chain:

browser.actions().mouseDown(element).sleep(5000).mouseUp(element).perform();


Clearly, this would not work since there is no "sleep" action.

Another practical example could be the "human-like typing". For instance:

browser.actions().mouseMove(element).click()
.sendKeys("t").sleep(50) // we should randomize the delays, strictly speaking
.sendKeys("e").sleep(10)
.sendKeys("s").sleep(20)
.sendKeys("t")
.perform();


Note that these are just examples, the question is meant to be generic.

The Question:

Is it possible to extend
browser.actions()
action sequences and introduce custom actions?



Answer Source

Yes, you can extend the actions framework. But, strictly speaking, getting something like:

browser.actions().mouseDown(element).sleep(5000).mouseUp(element).perform();

means messing with Selenium's guts. So, YMMV.

Note that the Protractor documentation refers to webdriver.WebDriver.prototype.actions when explaining actions, which I take to mean that it does not modify or add to what Selenium provides.

The class of object returned by webdriver.WebDriver.prototype.actions is webdriver.ActionSequence. The method that actually causes the sequence to do anything is webdriver.ActionSequence.prototype.perform. In the default implementation, this function takes the commands that were recorded when you called .sendKeys() or .mouseDown() and has the driver to which the ActionSequence is associated schedule them in order. So adding a .sleep method CANNOT be done this way:

webdriver.ActionSequence.prototype.sleep = function (delay) {
    var driver = this.driver_;
    driver.sleep(delay);
    return this;
};

Otherwise, the sleep would happen out of order. What you have to do is record the effect you want so that it is executed later.

Now, the other thing to consider is that the default .perform() only expects to execute webdriver.Command, which are commands to be sent to the browser. Sleeping is not one such command. So .perform() has to be modified to handle what we are going to record with .sleep(). In the code below I've opted to have .sleep() record a function and modified .perform() to handle functions in addition to webdriver.Command.

Here is what the whole thing looks like, once put together. I've first given an example using stock Selenium and then added the patches and an example using the modified code.

var webdriver = require('selenium-webdriver');
var By = webdriver.By;
var until = webdriver.until;
var chrome = require('selenium-webdriver/chrome');

// Do it using what Selenium inherently provides.

var browser = new chrome.Driver();

browser.get("http://www.google.com");

browser.findElement(By.name("q")).click();
browser.actions().sendKeys("foo").perform();
browser.sleep(2000);
browser.actions().sendKeys("bar").perform();
browser.sleep(2000);

// Do it with an extended ActionSequence.

webdriver.ActionSequence.prototype.sleep = function (delay) {
    var driver = this.driver_;
    // This just records the action in an array. this.schedule_ is part of
    // the "stock" code.
    this.schedule_("sleep", function () { driver.sleep(delay); });
    return this;
};

webdriver.ActionSequence.prototype.perform = function () {
    var actions = this.actions_.slice();
    var driver = this.driver_;
    return driver.controlFlow().execute(function() {
        actions.forEach(function(action) {
            var command = action.command;
            // This is a new test to distinguish functions, which 
            // require handling one way and the usual commands which
            // require a different handling.
            if (typeof command === "function")
                // This puts the command in its proper place within
                // the control flow that was created above
                // (driver.controlFlow()).
                driver.flow_.execute(command);
            else
                driver.schedule(command, action.description);
        });
    }, 'ActionSequence.perform');
};

browser.get("http://www.google.com");

browser.findElement(By.name("q")).click();
browser.actions().sendKeys("foo")
    .sleep(2000)
    .sendKeys("bar")
    .sleep(2000)
    .perform();
browser.quit();

In my implementation of .perform() I've replaced the goog... functions that Selenium's code uses with stock JavaScript.