Extracting table data

Nightwatch's custom command

Nightwatch is a great help with creating in-browser tests for web applications.

It’s very convenient to use CSS selectors to get information out of the DOM. The very same language used for day to day development.

When selecting one element at the time checking the content of a table can become quite a complex task. It requires writing assertion against each table cell separately. With more complex tables this can quickly become unwieldy.

The ideal would be to get the whole table at once. Unfortunately, nightwatch lacks a way of doing it. Fortunately, it’s not that hard to add.

Following is a nightwatch’s custom command which will return the content of a table as an array of arrays.

const idElements = (browser, element, selector) => {
  return new Promise((resolve, reject) => {
    const cb = result => {
      if (result.state === 'success') {
        resolve(result.value);
      } else {
        reject(result);
      }
    };
    browser.elementIdElements(
      element.ELEMENT, 'css selector', selector, cb);
  });
};

const text = (browser, element) => {
  return new Promise((resolve, reject) => {
    browser.elementIdText(element.ELEMENT, result => {
      if (result.state === 'success') {
        resolve(result.value);
      } else {
        reject(result);
      }
    });
  });
};

exports.command = function (selector, callback) {
  const getText = text.bind(null, this);
  const cb = result => {
    return Promise.all(result.value.map(element => {
      return idElements(this, element, 'td, th')
      .then(cells => Promise.all(cells.map(getText)));
    }))
    .then(data => {
      callback(data);
    });
  };
  this.elements('css selector', `${selector} tr`, cb);
  return this;
};

The biggest complication with getting text content of a table is that the task requires three levels of asynchronous calls. Each one depends on the result of the previous one.

Managing multiple asynchronous calls is quite a common task in the node and there is a popular solution: the async module. Instead of managing state between all callbacks one can depend on familiar set of functions: map, filter, etc.

Another approach is to use Promises. They were designed to help with reasoning about asynchronous code. It’s easy to turn regular callback-style code into one using Promises.

Here’s an example how to use that new command.

module.exports = {
  'Test example': browser => {
    browser
      .url('http://www.example.com')
      .waitForElementVisible('body', 1000)
      .perform((client, done) => {
        client.tableContent('table', rows => {
          client.assert.equal(rows[0], ['1', 'one']);
          client.assert.equal(rows[1], ['2', 'two']);
          done();
        });
      })
      .end();
  }
};

Because .tableContent can make a lot of asynchronous calls it’s paramount to wait for all of them to finish. Exactly why it’s required is described in the previous post.