blog‎ > ‎

Let's speed up Protractor's data lookup

posted Jun 20, 2014, 4:18 AM by Max Tardiveau
Protractor is the favored testing framework for end-to-end testing of AngularJS apps. It's quite good, and I especially like the smooth integration with Sauce Labs.

But Protractor can sometimes be very slow. For instance, if we have a page that shows a data table with (say) 3 columns (name, balance and creditLimit) and 20 rows, and I want to get all the values displayed in that table, the "normal" way to retrieve the data in that table would be something like:

// Parameters:
// tableSelector: the css selector for the table rows, e.g. "#leftGridContainer .ngRow"
// columnSelector: the additional css selector for the columns in a row, e.g. ".ng-binding"
// colNames: an array of strings with the names of the columns

var getTableValues = function(tableSelector, columnSelector, colNames) {
  return element.all(by.css(tableSelector)).map(function(row, index) {
    var columns = row.all(by.css(columnSelector));
    return columns.then(function(cols){
      var result = {};
      cols.forEach(function(col, idx) {
        result[colNames[idx]] = col.getText();
        result.rowElm = row;
      });
      return result;
    });
  });
};

This works. The problem is that it takes about 7 seconds -- and that's every time we want to get these values. Because these values may change, we do need to re-fetch them every time we want a fresh look at them. That makes our test script run like molasses on a cold day -- there is a 7 second delay every time we need to check something in that table.

There is a workaround, which is to bypass WebDriver and go straight to the browser. This is more convoluted, but it's well worth it. The idea is to generate a piece of code that will then be run (carefully -- we don't want to disturb it) in the browser. We'll then parse its output.

var getTableValues = function(tableSelector, columnSelector, colNames) {
  return browser.driver.executeScript("return (function(){" +
    "var rows = []; var row = {}; var colNames = " + JSON.stringify(colNames) + ";" +
    "angular.element('" + tableSelector + " " + columnSelector + "')" +
    ".each(function(idx, c) {" +
    "  var colIdx = idx % colNames.length;" +
    "  row[colNames[colIdx]] = $(c).text();" +
    "  if (colIdx == colNames.length - 1) {" +
    "    rows.push(row);" +
    "    row = {};" +
    "  }" +
    "});" +
    "return JSON.stringify(rows);" +
    "})();").
  then(function(s) {
  var data = JSON.parse(s);
  // We have the table data, now supplement it with the WebElement for each row
  return element.all(by.css(tableSelector)).then(function(rows) {
    _.each(rows, function(row, idx) {
      data[idx].rowElm = row;
    });
    return data;
  });
});
};

This can be called like this:

var tableDataPromise = getTableValues('#leftTableDiv .ngRow', '.ngCellText .ng-binding', ["name", "balance", "creditLimit"]);

On my machine, this runs in under 100ms. That is a lot quicker than 7 seconds.

For a relatively simple script, this brought our total execution time from 2.5 minutes to under a minute -- a big improvement!

I wish WebDriver wasn't this sluggish when navigating the DOM, but with this approach, at least, there is a way to address the biggest bottlenecks.

Comments