For my selenium tests, I try and write most of the code in DSL format in a object termed a Solvent. We use this term at my place of employment and I think it is a good term and have continued to use it. This allows me to encapsulate the underlying implementation while presenting a functional level to the selenium tests. For form controls, I use a generic Attribute class that along with an AttributeHandler knows how to process any attribute type (for example, checkboxes, textfields, textareas, selections, date pickets etc.). Thus we come to the "code" of how I handle ChosenJS Selections.
First off, lets look at what the generated HTML looks like for the AngularJS control (this represents a drop-down for my stamp conditions):
<select class="condition-selector ng-pristine ng-valid ng-valid-required" name="condition" data-jq-chosen data-options="conditions" data-placeholder="Select a condition" data-ng-model="model.activeCatalogueNumber.condition" data-ng-required="true" data-ng-change="validate()" tabindex="-1" required="required" style="display: none;"> <option value="? string:0 ?"></option> <!-- ngRepeat: cond in conditions --> <option data-ng-repeat="cond in conditions" value="0" class="ng-scope ng-binding">Mint</option> <option data-ng-repeat="cond in conditions" value="1" class="ng-scope ng-binding">Mint (NH)</option> <option data-ng-repeat="cond in conditions" value="2" class="ng-scope ng-binding">Used</option> <option data-ng-repeat="cond in conditions" value="3" class="ng-scope ng-binding">Cancel to Order</option> <option data-ng-repeat="cond in conditions" value="4" class="ng-scope ng-binding">Mint No Gum</option> </select>
Mostly this is Angular, although you may notice the
data-optionsvariables which are instructions/directives for my directive (one indicates it should turn this drop-down into a ChosenJS control and the other that it should watch/bind to the conditions variable on the controller). If you are not using AngularJS this probably doesn't mean to much to you.
If ChosenJS was not present we could simply operate on this HTML and use an element selector like
By.ByName("condition-selector")and click it and choose the appropriate
<option>tag matching the value desired. But when ChosenJS is present, it will add additional HTML into the DOM. Specifically for the above example we have this:
<div class="chosen-container chosen-container-single chosen-container-single-nosearch" style="width: 121px;" title=""> <a class="chosen-single chosen-single-with-deselect" tabindex="-1"><span>Mint</span><abbr class="search-choice-close"></abbr></a> <div class="chosen-drop"> <div class="chosen-search"><input type="text" autocomplete="off" readonly="" tabindex="21"></div> <ul class="chosen-results"> <li class="active-result result-selected ng-scope ng-binding" data-option-array-index="1" style="">Mint</li> <li class="active-result ng-scope ng-binding" data-option-array-index="2" style="">Mint (NH)</li> <li class="active-result ng-scope ng-binding" data-option-array-index="3" style="">Used</li> <li class="active-result ng-scope ng-binding" data-option-array-index="4" style="">Cancel to Order</li> <li class="active-result ng-scope ng-binding" data-option-array-index="5" style="">Mint No Gum</li> </ul> </div> </div>
And it is this fragment that we need to interact with (since the
chosen-containerfloats on top of the
selectDOM element). This ChosenJS DOM will always appear as a peer to the
selectDOM elements. I order to get this to work in Selenium we need to:
- Click the
chosen-containerto invoke the drop-down (this is unpopulated until shown for performance reasons)
- Click the appropriate
case Select: elm = parent.findElement(attr.asBy()); String chosen = "../div[contains(@class,'chosen-container')]"; elm.findElement(SolventHelper.ngWait(By.xpath(chosen + "/a"))).click(); elm.findElement(SolventHelper.ngWait(By.xpath(chosen + "/div[contains(@class,'chosen-drop')]/ul/li[.=\"" + attr.getValue() + "\"]"))).click(); assertEquals(attr.getValue(), elm.findElement(SolventHelper.ngWait(By.xpath(chosen + "/a"))).getText()); break;
In the code fragment above, the parent is passed to the AttributeHandler as a WebElement. This is the container where that form element is located. It could be a fieldset, a panel div or a popup window etc. It is just a starting point so I can find the appropriate element in close proximity. The ngWait( ) method comes from the Protractor project (obviously it is a Java version of this) and simply waits for Angular to complete its digest (The fluent angular project uses this and you can see the source here.)
You may also notice I do an assertion check just to ensure that the value got set on the link. I may ultimately remove this.
But wait, I see you are using xpaths.... that is so last year.... You are right. CSS Selectors is definitely a better way to go. Except.... there is no CSS selector for parent. Often I have found I am starting to use a more mix of CSS Selectors and XPaths. I suppose I could have gotten a Web Element for the
"chosen"variable and then simply used CSS selectors from that. Overall, what is nice about this, is I have yet to run into a problem with timing issues like I had with GXT and EXT-JS selectors. Also, the
litags for the values will be ALL the values in a scrollable window, so selenium will select the right value even if it is not on the screen.
I have been a little slow putting together a regression suite in Selenium for my redesigned Stamp Web application written in AngularJS, but I want to upgrade to 1.2.0 and in order to do so wanted to have about 15-20 critical test cases covered first. So far my experiences has been very positive and the number of timing issues is almost negligible (the only issue I have had is when I launch the application I maximize it and sometimes the app will start the login before maximized which causes some issues). Other than that, I have yet to have to use a single "
pause()" method which is really nice.