/ Question, tell me how you feel about this?
One of the most time consuming, sometimes frustrating things about automating checks in a browser is the hunt for the correct element. Some are easier than others, but if you have an ng-repeat, non-unique ids, a table or tags nested 10 levels deep, things can get a bit messy.
Responses on queries posted to various sites where questions asked along the lines of:
‘Help! Text isn’t being entered into the text field, why?’ or
‘Element is not being identified, how do I select the correct one?’
are quite often; your xpath is wrong, use relative xpath, or some other fix involving using the locators with the existing elements.
Rarely is the answer the solution, just a temporary fix for the problem.
In these situations, as testers, preferably before work has started on an application, we should be promoting testability, or in perhaps in this case, checkability. And for automation, a good way to do this is by using test id’s.
Here is an example of a custom angular FX trading date selector, where elements are reused and selectable dates are updated based on currency selected.
<div>
<table>
<thead>
<tr>
<th ng-click="prev()"></th>
<th ng-click="month()">March 2016</th
<th ng-click="next()"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="week in weeks>
<td ng-repeat="day in week">
<span ng-class="some stuff" role="button" tabindex="0">07</span>
<td ng-repeat="day in week">
<span ng-class="some stuff" role="button" tabindex="0">08</span>
<td ng-repeat="day in week">
<span ng-class="some stuff" role="button" tabindex="0">09</span>
....
</tr>
<tr ng-repeat="week in weeks>
<td ng-repeat="day in week">
<span ng-class="some stuff" role="button" tabindex="0">14</span>
<td ng-repeat="day in week">
<span ng-class="some stuff" role="button" tabindex="0">15</span>>
<td ng-repeat="day in week">
<span ng-class="some stuff" role="button" tabindex="0">16</span>
...
</tr>
Here, we have multiple elements all of the same class within repeats.
Which would lead to something like this as a page object:
tile.prototype = Object.create({}, {
previousMonth: {get: function() {return element(by.css('[ng-click="prev()'));}},
nextMonth: {get: function() {return element(by.css('[ng-click="next()"'));}},
weekday: {get: function() {return errr...????????);}}
While the previous and next month looks fairly simple, if the names of these change, those page objects are no longer going to work. And when it comes to selecting a day of the week in the calendar, we would have to return an array of elements, check that we are on the correct month, know how many days are in that month and then find the correct one to select, or have some monolithic flaky element locator.
This kind of SPA implementation makes selecting the correct elements reliably quite difficult and the page objects unwieldy, unreadable and inelegant.
/ Question, how’d you like this knowledge that I brought?
Here’s an example of the same elements with test ids.
<div>
<table>
<thead>
<tr>
<th ng-click="prev()" test-id="previous"></th>
<th ng-click="month()" test-id="current-month">March 2016</th
<th ng-click="next()" test-id="next"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="week in weeks>
<td ng-repeat="day in week">
<span ng-class="some stuff" role="button" tabindex="0" test-id="day-7">7</span>
<td ng-repeat="day in week">
<span ng-class="some stuff" role="button" tabindex="0" test-id="day-8>8</span>
<td ng-repeat="day in week">
<span ng-class="some stuff" role="button" tabindex="0" test-id="day-9">9</span>
....
</tr>
These id’s can be generated by the application and we can then write a bit of code for a custom locator into the OnPrepare method to find these ids. e.g.
by.addLocator('testId', function(value, parentElement){
parentElement = parentElement || document;
var nodes = parentElement.querySelectorAll('[test-id]');
return Array.prototype.filter.call(nodes, function(node){
return (node.getAttribute('test-id') === value;
])[0];
});
Then, our page object can use these to find the exact element we need.
calendar.prototype = Object.create({}, {
nextMonth: {get: function() {return element(by.testId('next'));}},
previousMonth: {get: function() {return element(by.testId('previous'));}},
currentMonth: {get: function() {return element(by.testId('current-month'));}},
weekday01: {get: function() {return element(by.testId('day-1'));}},
weekday02: {get: function() {return element(by.testId('day-2'));}},
....
Now we will always be able to select the correct weekday for the current month, whichever one it is.
We can also check whether the current month displays the correct days without element chaining or looping through an array.
We can select a known trading day, or validate the days are not selectable for non-trading days. And we can use the same page object no matter which month we are in, enabling us to run these checks any day of the week or month.
This also makes our checks easier to read and maintain.
So if you’re asked to add automation, make sure that you are able to locate elements easily. Add a task to your sprint, bring it up in a scrum, repeatedly. Whichever way works. This will save you having to ask the same questions that everyone else is.