Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?


like image 959
alecxe Avatar asked Sep 25 '15 19:09

alecxe


People also ask

How do you use an actions class in Protractor?

mouseMove / hover in protractorWith the object of the action, you should first move to the menu element, and then move to the submenu item and click it or perform whatever action you wish.

How do you launch a browser with a Protractor?

Launching a browser locally Depending on the configuration file, protractor runner loads one of the driver providers. For a locally hosted selenium server, the driver provider will then use the selenium-webdriver node module (client) to create new session of the browser.


2 Answers

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.

like image 84
Louis Avatar answered Oct 01 '22 22:10

Louis


Here is what I did (based on the perfect @Louis's answer).

Put the following into onPrepare() in the protractor config:

// extending action sequences
protractor.ActionSequence.prototype.sleep = function (delay) {
    var driver = this.driver_;
    this.schedule_("sleep", function () { driver.sleep(delay); });
    return this;
};

protractor.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;
            if (typeof command === "function")
                driver.flow_.execute(command);
            else
                driver.schedule(command, action.description);
        });
    }, 'ActionSequence.perform');
};

protractor.ActionSequence.prototype.clickAndHold = function (elm) {
    return this.mouseDown(elm).sleep(3000).mouseUp(elm);
};

Now you'll have sleep() and clickAndHold() browser actions available. Example usage:

browser.actions().clickAndHold(element).perform();
like image 31
alecxe Avatar answered Oct 02 '22 00:10

alecxe