Is there any way to optimize / speed up the sending of data to a UI with Protractor?
Asked Answered
S

5

13

I have code similar to this:

ExamPage.prototype.enterDetailsInputData = function (modifier) {
    page.sendKeys(this.modalExamName, 'Test Exam ' + modifier);
    page.sendKeys(this.modalExamVersionId, 'Test exam version ' + modifier);
    page.sendKeys(this.modalExamProductVersionId, 'Test exam product version ' + modifier);
    page.sendKeys(this.modalExamAudienceId, 'Test exam audience ' + modifier);
    page.sendKeys(this.modalExamPublishedId, '2014-06-1' + modifier);
    page.sendKeys(this.modalExamPriceId, '100' + modifier);
    page.sendKeys(this.modalExamDurationId, '6' + modifier);
};

Here's the page.sendKeys function. Note that currently this is not doing any return of promises or anything like that. If the function is not coded well then I welcome comments:

// page.sendkeys function
sendKeys(id: string, text: string) {
    element(by.id(id)).sendKeys(text);
} 

I watch as it slowly fills out each field on my screen and then repeats it again and again in more tests that follow.

Is there any way that this could be optimized or do I have to wait for one field after the other to fill and have to live with tests that take a long time to run?

I assume sendKeys is promise based. Could I for example use AngularJS $q to issue all the sendKeys at the same time and then use $q to wait for them to complete?

Stomatology answered 5/9, 2014 at 15:17 Comment(4)
Which browser do you use? Or, does it actually depend on the browser?Zayin
I am using the Chrome browser at the moment but I assume it's the same with all browsers. Tests run slowly as I see each field being filled out one by one. The problem is I have MANY fields being filled out and my test suite is taking longer and longer.Stomatology
One option could be to use protractor's mock module functionality such that your controller already has the values bound to the scope, thereby bypassing the need to fill out the form each timeIsosceles
Q won't help. Protractor is based on WebdriverJS which has a control flow. When you use sendKeys, it schedules them all and then execute them one after the other. You can look at code.google.com/p/selenium/wiki/WebDriverJs ("Defining multiple control flows").Hendrickson
B
11

Potential Solution I think at least a little hackery is required no matter how you optimize it - protractor doesn't give you this out of the box. However would a small helper function like this suit your needs? What else do you need to speed up sides text inputs with ng-models?

function setNgModelToString(element, value) {
    return element.getAttribute('ng-model').then(function (ngModel) {
        element.evaluate('$eval("' + ngModel + ' = \'' + value + '\'") && $digest()');
    });
}

Solution Example:

describe('angularjs homepage', function() {
  it('should have a title', function() {
    browser.get('http://juliemr.github.io/protractor-demo/');

    var inputString1 = '';
    var inputString2 = '';
    for (var i = 0; i < 1000; i++) {
        inputString1 += '1';
        inputString2 += '2';
    }

    /* Uncomment this to see it runs much much slower when you enter each key. */
    //element(by.model('second')).sendKeys(inputString1);   

    setNgModelToString(element(by.model('second')), inputString2);

    expect(element(by.model('second')).getAttribute('value')).toEqual(inputString2);
  });
});

Why does the solution work?

You need to use $eval to wrap the assignment and not just assignment as evaluate does not evaluate side effects (a nested evaluation, though... heh). Assuming that's truthy in angular expressions then $digest() is called from the &&; this causes a digest to happen, which you need to update everything since you set a value from outside the digest cycle.

Thoughts about the solution:

The whole idea behind an E2E test is to "emulate" an end-user using your app. This arguably doesn't do that as well as sending the keys one-by-one, or copy-and-paste (since pasting into elements is a valid way of entering input; it's just hard to set up due to flash, etc., see below).

Other Potential Solutions:

  • Copy and Paste: Create an element, enter text, copy it, paste the text sending Ctrl + V to the target element. This may require doing a bunch of fancy footwork, like using Flash (exposing the system clipboard is a security risk) and having "copy it" click an invisible flash player. See executeScript to evaluate functions on the target so that you have access to variables like window if you need that.

  • Parallelizing your tests. Read the official doc here and search for "shard" and then "multiple". If you're mainly worried about the duration of your entire test collection and not individual tests, scaling out your browser count is probably the way to go. However there's a good chance you are TDD-ing or something, hence needing each test to run faster.

Banshee answered 9/9, 2014 at 8:40 Comment(3)
very well thought outHeckman
Simply awesome, my simple login test went down from 29.27 seconds to 4.467.Fancyfree
Does this still work in Angular>=2? I tried it and seem to be running into #36202191Phytosociology
F
3

You can do following -

ExamPage.prototype.enterDetailsInputData = function (modifier) {
var arr=[
{id:this.modalExamName, text:'Test Exam ' + modifier},
{id:this.modalExamVersionId, text:'Test exam version ' + modifier },
{id:this.modalExamProductVersionId, text:'Test exam product version ' + modifier},
{id:this.modalExamAudienceId,text:'Test exam audience ' + modifier},
{id:this.modalExamPublishedId, text:'2014-06-1' + modifier},
{id:this.modalExamPriceId, text:'100' + modifier},
{this.modalExamDurationId, text:'6' + modifier}
];
var Q = require('q'),
    promises = [];
for (var i = 0; i < arr.length; i++) {
    promises.push(page.sendKeys(arr[i].id, arr[i].text));
}

Q.all(promises).done(function () {
    //do here whatever you want
});

};

sendKeys returns promise by default. See here -https://github.com/angular/protractor/blob/master/docs/api.md#api-webdriver-webelement-prototype-sendkeys

sendKeys(id: string, text: string) {
    return element(by.id(id)).sendKeys(text);
} 
Flavour answered 7/9, 2014 at 18:39 Comment(4)
Would I need to change page.sendKeys as presently it does not return a promise? If I need to change it then how could I do that?Stomatology
Sendkey returns promise by default. I guess it should work. I have also updated the answer.Flavour
I understand sendkey returns a promise but in my application that happense inside the function sendKeys(id: string, text: string) { element(by.id(id)).sendKeys(text); } So will the promise still be returned from page.sendKeys ?Stomatology
Yes, Use return before it: return element(by.id(id)).sendKeys(text);Flavour
S
2

If you really want to speed up the process of manipulating DOM in any way (including filling up data forms) one option to consider is to use: browser.executeScript or browser.executeAsyncScript. In such a case the webdriver let the browser execute the script on its own -- the only overhead is to send the script body to the browser, so I do not think there may be anything faster.

From what I see, you identify DOM elements by ids so it should smoothly work with the approach I propose.

Here is a scaffold of it -- tested it and it works fine:

browser.get('someUrlToBeTested');
browser.waitForAngular();
browser.executeScript(function(param1, param2, param3){

        // form doc: arguments may be a boolean, number, string, or webdriver.WebElement
        // here all are strings: param1 = "someClass", param2 = "someId", param3 = "someModifier"
        var el1 = document.getElementsByClassName(param1)[0];
        var el2 = document.getElementById(param2);

        el1.setAttribute('value', 'yoohoo ' + param3);
        el2.setAttribute('value', 'hooyoo ' + param3);

        // depending on your context it will probably
        // be needed to manually digest the changes
        window.angular.element(el1).scope().$apply();


},'someClass', 'someId', 'someModifier')

small remark: If you pass webdriver.WebElement as one of your argument it will be cast down to the corresponding DOM element.

Saintmihiel answered 9/9, 2014 at 10:53 Comment(0)
C
0

I also used browser.executeScript. But I didn't need to use $apply waitForAngular

I add a script when E2E tests are running, and put a function on global scope, don't worry it's only during E2E tests, and you can namespace if you want to.

'use strict';
function fastSendKeys(testId, value) {
    //test id is just an attribute we add in our elements
    //you can use whatever selector you want, test-id is just easy and repeatable
    var element = jQuery('[test-id=' + '"' + testId + '"' + ']');
    element.val(value);
    element.trigger('input');
}

Then in your protractor test something like this (in a page object):

this.enterText = function (testId, value) {
        var call = 'fastSendKeys(' + '"' + testId + '"' + ',' + '"' + value + '"' + ')';
        console.log('call is ', call);
        browser.executeScript(call);
    }; 
Cantrip answered 4/8, 2015 at 22:9 Comment(0)
R
0

The following worked for me when testing an angular 2 application

await browser.executeScript("arguments[0].value='" + inputText + "';", await element.by.css("#cssID"))

inspired by https://mcmap.net/q/211897/-set-value-of-input-instead-of-sendkeys-selenium-webdriver-nodejs

Roughspoken answered 18/1, 2021 at 11:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.