2

I am asserting on the contents of a particular row from an html table using webdriver via protractor. I have the following code which works correctly, but looks horrible. I want advice on how to better organise this code idiomatically with promises; in particular, I'd like to make it more obvious that there are 3 parts to this code:

  1. Find the rows with a td containing the specified matchText on the page
  2. Check that only one row matched, and handle the error cases with useful debug info
  3. Check that the text content of the tds in this matched row is as expected

Is there a way I can organise this better to make this more readable, perhaps by chaining the promises or something?

browser.findElements(by.xpath("//td[text() = '" + matchText + "']/..")).then(function(trs) {
  if (trs.length == 0) {
    throw 'Unable to find td element equal to ' + matchText
  } else if (trs.size > 1) {
    protractor.promise.all(trs.map(function(tr){return tr.getInnerHtml()})).then(function(trsInnerHtml) {
      throw 'Matched multiple td elements for ' + matchText + ':' + trsInnerHtml;
    })
  } else {
    trs[0].findElements(by.tagName('td')).then(function(tds) {
      protractor.promise.all(tds.map(function(td) {return td.getText()})).then(function(tdContent){
        expect(tdContent).toEqual(expectedContent);
      })
    });
  }
});
Cœur
  • 37,241
  • 25
  • 195
  • 267
robd
  • 9,646
  • 5
  • 40
  • 59
  • I wonder why you need to try to get the innerhtmls in case of multiple trs, you do unconditionally throw anyway after that? – Bergi Mar 10 '15 at 20:28
  • @Bergi The intention of that is to include the innerHtml of the trs to aid in the debugging process if the test failed. Instead of just seeing an error like 'Expected 1, was 2', I hoped to see the full innerHtml of the matched trs. But this is untested, and also I unhelpfully named the tr innerHtml with the same variable name as the tr promises (`trs`). I have updated the question to try to better show what I mean. – robd Mar 10 '15 at 23:15
  • Ah, I missed the function parameter. Thanks for the clarifying edit. – Bergi Mar 10 '15 at 23:17

3 Answers3

2

How about using element, element.all() and letting expect() resolve the promises for us and get some help from jasmine-matchers package that introduces a handy toBeArrayOfSize() matcher:

element.all(by.xpath("//td[text() = '" + matchText + "']/..")).then(function(trs) {
    expect(trs).toBeArrayOfSize(1);

    expect(trs[0].element(by.tagName('td')).getText()).toEqual(expectedContent);
});
robd
  • 9,646
  • 5
  • 40
  • 59
alecxe
  • 462,703
  • 120
  • 1,088
  • 1,195
  • I wasn't able to get toBeArrayOfSize to work, but your advice about the element API set me down the right route to solve this much more elegantly - thanks. – robd Mar 11 '15 at 00:45
  • @robd glad to help! Do you have `require("jasmine-expect");` at the top of the spec? (This is all I did except installing the package). Thanks. – alecxe Mar 11 '15 at 00:48
  • No I didn't, I had added `"jasmine-expect": "^2.0.0-beta1"` to my `package.json`, but I stupidly forgot to require it! – robd Mar 11 '15 at 00:50
2

Yes, you can unwrap the promise callbacks from the non-throw case to a chained version:

browser.findElements(by.xpath("//td[text()='" + matchText + "']/..")).then(function(trs) {
  if (trs.length == 0) {
    throw 'Unable to find td element equal to ' + matchText
  } else if (trs.size > 1) {
    return protractor.promise.all(trs.map(function(tr) {
      return tr.getInnerHtml()
    })).then(function(trsInnerHtml) {
      throw 'Matched multiple td elements for ' + matchText + ':' + trsInnerHtml;
    });
  } else {
    return trs[0].findElements(by.tagName('td'));
  }
}).then(function(tds) {
  return protractor.promise.all(tds.map(function(td) {return td.getText()}));
}).then(function(tdContent){
  expect(tdContent).toEqual(expectedContent);
});
Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Your linked answer on nested promises was really helpful in understanding how to think about them - thanks very much – robd Mar 11 '15 at 00:48
1

In the end I took chaining promises from @Bergi and the element api from @alexce (thanks both!) and came up with this:

it('Matches tds', function() {
  browser.get('index.html');      
  var textToMatch = 'TDs';
  expect(
    element.all(trsContainingTd(textToMatch)).then(extractTds)
  ).toEqual(
    [['Expected', 'TDs', 'content']]
  );
});

function trsContainingTd(matchText) {
  return by.xpath("//td[text() = '" + matchText + "']/..");
}

function extractTds(trs) {
  return protractor.promise.all(trs.map(function(tr) {
    return tr.all(by.tagName('td')).getText();
  }));
}

This has a few advantages to my eye:

  1. It's a single expectation
  2. It will print out the helpful debug if there are more / fewer matching rows than expected
  3. The trsContainingTd and extractTds functions are general enough to be used elsewhere
robd
  • 9,646
  • 5
  • 40
  • 59