Custom browser actions in Protractor

2019-01-06 20:11发布

问题:

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?


回答1:

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.



回答2:

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();


回答3:

I think it is possible to extend the browser.actions() function but that is currently above my skill level so I'll lay out the route that I would take to solve this issue. I would recommend setting up a "HelperFunctions.js" Page Object that will contain all of these Global Helper Functions. In that file you can list your browser functions and reference it in multiple tests with all of the code in one location.

This is the code for the "HelperFunctions.js" file that I would recommend setting up:

var HelperFunctions = function() {
    this.longClick = function(targetElement) {
        browser.actions().mouseDown(targetElement).perform();
        browser.sleep(5000);
        browser.actions().mouseUp(targetElement).perform();
    };
};

module.exports = new HelperFunctions();

Then in your Test you can reference the Helper file like this:

var HelperFunctions = require('../File_Path_To/HelperFunctions.js');

describe('Example Test', function() {
    beforeEach(function() {
        this.helperFunctions = HelperFunctions;

        browser.get('http://www.example.com/');
    });

    it('Should test something.', function() {
        var Element = element(by.className('targetedClassName'));
        this.helperFunctions.longClick(Element);
    });
});

In my Test Suite I have a few Helper files setup and they are referenced through out all of my Tests.



回答4:

I have very little knowledge of selenium or protractor, but I'll give it a shot.

This assumes that

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

is valid syntax for your issue, if so then this would likely do the trick

browser.action().sleep = function(){
    browser.sleep.apply(this, arguments);
    return browser.action()
}