2

I was recently tasked with implementing some automated testing for an AngularJS application developed by my company. After doing some research into the different testing frameworks available, I decided to use Protractor as it seems to provide all of the functionality that we will need, and appears to be well documented & maintained.

When I started designing & writing the tests, I noticed that most of the HTML elements in the code had not been given ID attributes- so I started assigning ID attributes to the various elements that would be used in the tests, so that I could access them using functions such as element(by.id('abc')); to assign them to variables for use within the test scripts.

However, I have just been informed that not giving the HTML elements ID attributes was a deliberate design choice in order to make the source code easier to compress for distribution to our customers. So this means that I now need to re-write my tests so that they no longer use ID attributes to get hold of the elements, but I'm not sure quite how to do this...

When I inspect those menu item in the browser, it shows the following HTML structure:

<ul id="nav" class="nav" data-slim-scroll ... >
    <li data-ng-mouseenter="setMenuTop($event)" ...
        <a href="#/pages" id="pagesMenuBtn" ... >
            ...
        <a href="#/alarms" id="alarmsMenuBtn" ... >
            ...
        ...
    </li>
    ...
</ul>

The goal is to be able to remove the id attributes from the tags above, but still be able to access those HTML elements to run tests on them.

For example, where I previously had:

describe('myApp', function() {
    var alarmsMenuBtn = element(by.id('alarmsMenuBtn'));
    var chartsMenuBtn = element(by.id('chartsMenuBtn'));
    ...
    it('should navigate to the Charts page', function(){
        browser.waitForAngularEnabled(false);
        browser.actions().mouseMove(chartsMenuBtn).perform();
        chartsMenuBtn.click();
        browser.waitForAngularEnabled(true);
        browser.call(closeDlg).then(function(){
            expect(browser.getCurrentUrl()).toBe('chartsURL');
    });

I now have:

describe('myApp', function() {
    var menuItems = $(".slimScrollDiv").$(".nav").$$("li");
    var alarmsMenuBtn = menuItems.get(1).$("a");
    var chartsMenuBtn = menuItems.get(2).$("a");

But when I run this with the same tests, my tests now fail, giving me messages such as:

Failures: 1) App should navigate to the Alarms page Message: Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL. Stack: Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL. at ontimeout (timers.js:386:11) at tryOnTimeout (timers.js:250:5) at Timer.listOnTimeout (timers.js:214:5) Message: Failed: element not visible (Session info: chrome=61.0.3163.100) (Driver info: chromedriver=2.33.506120 (e3e53437346286c0bc2d2dc9aa4915ba81d9023f),platform=Windows NT 10.0.15063 x86_64) Stack: ...

and:

2) App should navigate to the Charts page Message: Failed: element not visible (Session info: chrome=61.0.3163.100) (Driver info: chromedriver=2.33.506120 (e3e53437346286c0bc2d2dc9aa4915ba81d9023f),platform=Windows NT 10.0.15063 x86_64)

The HTML for these elements is:

<li><a href="#/alarms" id="alarmsMenuBtn" target="_self"><i class="ti-alert"></i><span data-i18n="Alarms"></span></a></li>
<li><a href="#/charts" id="chartsMenuBtn" target="_self"><i class="ti-bar-chart"></i><span data-i18n="Charts"></span></a></li>

So my question is, how can I access these (or any other) HTML elements using something other than ID attributes to get them?

Noble-Surfer
  • 3,052
  • 11
  • 73
  • 118
  • First of all, you likely have a syntax error in your code: `browser.actions().mouuseMove(chartsMenuBtn).perform();` (`mouuse`!). That said, either use `by.xpath` (some browsers like chrome supports getting a query selector by right clicking an element), or just get deeper in protractor. I could go on for a while there, but two great sources that helped me are the following: http://luxiyalu.com/protractor-locators-selectors/ and http://blog.ng-book.com/10-protractor-tips-for-better-e2e-tests/ . In any case, are elements meant to be somewhat static or are they meant to change over time? – briosheje Oct 31 '17 at 11:34
  • 1
    About xpath on chrome -> http://prntscr.com/h4bfl8 (example). Result, in this case, is `//*[@id="comment-81014907"]/td[2]/div/span[1]` (which is UNIQUE for this specific case though). About element being not visible, check this: https://stackoverflow.com/questions/37809915/element-not-visible-error-not-able-to-click-an-element . Moreover, I would suggest you to run protractor on some specific environments, there are some great starters on yeoman that gives you a starter kit and debugging in visual studio code. That's very helpful, sometimes. – briosheje Oct 31 '17 at 11:35
  • Apologies- that was a copy-paste typo somehow... – Noble-Surfer Oct 31 '17 at 11:39
  • The issue you have is visibility of the element. Visibility means that the element is not only displayed but also has a height and width that is greater than 0. Are there any menuItems that are presented to the DOM but aren't visible? – MatthewThomas.dev Oct 31 '17 at 12:23
  • 1
    No, all of the menu items are visible. – Noble-Surfer Oct 31 '17 at 13:07

2 Answers2

2

The Protractor API documentation provides a full list of locators you may use in addition to ID to locate DOM elements. The documentation also provides sample code to demonstrate how each locator can be implemented in practice.

Here is the full list as of Protractor version 5.2.0:

ProtractorBy

The Protractor Locators. These provide ways of finding elements in Angular applications by binding, model, etc.

addLocator - Add a locator to this instance of ProtractorBy.

binding - Find an element by text binding.

exactBinding - Find an element by exact binding.

model - Find an element by ng-model expression.

buttonText - Find a button by text.

partialButtonText - Find a button by partial text.

repeater - Find elements inside an ng-repeat.

exactRepeater - Find an element by exact repeater.

cssContainingText - Find elements by CSS which contain a certain string.

options - Find an element by ng-options expression.

deepCss - Find an element by css selector within the Shadow DOM.

Extends webdriver.By

className - Locates elements that have a specific class name.

css - Locates elements using a CSS selector.

id - Locates an element by its ID.

linkText - Locates link elements whose visible text matches the given string.

js - Locates an elements by evaluating a JavaScript expression, which may be either a function or a string.

name - Locates elements whose name attribute has the given value.

partialLinkText - Locates link elements whose visible text contains the given substring.

tagName - Locates elements with a given tag name.

xpath - Locates elements matching a XPath selector.

MatthewThomas.dev
  • 1,045
  • 10
  • 24
  • Thanks for your answer. I did look into this, but the issue that I thought using something like `className` or some other 'non-unique' attribute might give me was that it might return a number of elements when I am looking for just one particular one (hence wanting to use the ID as a unique value)... I just gave `by.className(...)` a go, and sure enough, I got a load of messages stating `W/element - more than one element found for locator By(css selector, .ti-bar-chart)- the first result will be used` (for example). – Noble-Surfer Oct 31 '17 at 13:05
  • The test then also fails, because it's trying to perform a click on the wrong element... – Noble-Surfer Oct 31 '17 at 13:07
  • I am already using the `repeater` to find elements inside `ng-repeat` elements, and this works correctly. The particular elements I don't seem to be able to get hold of without IDs are `` elements nested inside `
  • ` elements. – Noble-Surfer Oct 31 '17 at 13:10
  • 1
    Just gave `by.linkText()` a go, passing it the value given to the `data-i18n` attribute of the HTML tag, and that appears to have worked- thanks a lot! – Noble-Surfer Oct 31 '17 at 13:27
  • I have a follow up query: since doing this, when I now run my tests, I seem to keep getting a `FA Jasmine spec timed out. Resetting the WebDriver Control Flow.` error, which is always thrown at the same point- the point where my `closeDlg()` function is called to close a dialog that is opened automatically when browsing to a specific page. I tried adding a `browser.waitForAngularEnabled(true);` call within that function (before assigning the 'Cancel' button to a variable), but this doesn't seem to have made a difference... – Noble-Surfer Oct 31 '17 at 16:28