Reputation: 2445
I'm writing a Typescript/Protractor test that needs to both evaluate and select checkbox elements on a grid. These checkboxes lack IDs. Since I can get a handle on input elements that reside on the same row as these checkboxes, I'm attempting to get them via a combination of element finders (by.id, by.xpath, and by.css). But my attempts are failing.
What I would like to write is a small function that will return the targeted checkbox element from the row, based on a distant "sibling" element on the row and the checkbox index on the row. I've tried several things, and what I consider the most promising attempts are posted here.
This is a section of markup from the grid that I'm targeting. Several columns of information are omitted for clarity. This is what I'm contending with. It's not within my power to change the markup, I've only been asked to write the Protractor tests.
<div ng-repeat="(rowRenderIndex, row) in rowContainer.renderedRows track by $index" class="ui-grid-row ng-scope" ng-style="Viewport.rowStyle(rowRenderIndex)">
<div ui-grid-row="row" row-render-index="rowRenderIndex" class="ng-isolate-scope">
<div ng-repeat="(colRenderIndex, col) in colContainer.renderedColumns track by col.uid" class="ui-grid-cell ng-scope ui-grid-coluiGrid-09T" ng-class="{ 'ui-grid-row-header-cell': col.isRowHeader }" ui-grid-cell="">
<div ng-click="grid.appScope.rowColumnSelect(row, 'rating')" ng-class="{'px-grid-row-selected': grid.appScope.highlightCell(row, 'rating'), 'px-can-edit': grid.appScope.clickableCell(row.entity, 'rating'), 'px-grid-total-row': row.entity.isTotal, 'px-grid-notable-row': row.entity.isNotable}" class="ng-scope">
<div class="ui-grid-cell-contents ng-hide" ng-hide="grid.appScope.showCell(row, 'rating')">
</div>
<div ng-show="grid.appScope.showCell(row, 'rating')" name="loanLossAssumption3" class="ui-grid-cell-contents">
<div ng-if="grid.appScope.haveData(row)" class="text-right ng-scope">
<div error-display-strategy="grid" px-input-field="" id="loanLossAssumptionrating3" name="loanLossAssumptionrating3" ng-if="grid.appScope.inEditMode(row, 'rating')" input-type="number" px-input-required="true" px-round-value="0" ng-model="row.entity.rating" class="ng-pristine ng-untouched ng-valid ng-scope ng-isolate-scope">
<div ng-class="{'row': !vm.isGridErrorDisplay && !vm.autoWidth}">
<div>
<div tooltip="" tooltip-placement="bottom" tooltip-append-to-body="true" tooltip-trigger="open" class="ng-scope">
<div px-validation="loanLossAssumptionrating3InputField" ng-class="{'col-xs-12': !vm.isGridErrorDisplay && !vm.autoWidth}" min="" max="" input-type="number" maxlength="" minlength="" error-display-strategy="grid" px-tooltip="" class="ng-isolate-scope">
<div class="row">
<div ng-class="vm.inputFieldClass()" class="col-xs-12">
<div class="input-group px-input-group-overrides px-no-grid-padding" ng-hide="vm.pxInputDisabled">
<span id="loanLossAssumptionrating3Prefix" name="loanLossAssumptionrating3Prefix" class="input-group-addon input-left-addon ng-binding ng-hide" ng-show="vm.prefix" ng-class="{'px-auto-width': vm.autoWidth}">
</span>
<div class="inner-addon">
<input id="loanLossAssumptionrating3InputField" name="loanLossAssumptionrating3InputField" class="form-control ng-pristine ng-untouched ng-valid ng-valid-px-email ng-valid-required ng-valid-unique" ng-class="{'px-input-with-tooltip': !!vm.pxTooltip, 'px-auto-width': vm.autoWidth}" ng-required="vm.pxInputRequired" px-round-value="0" px-edit-rate="false" type="text" ng-model="vm.ngModel" px-email-validator="" ng-disabled="vm.pxControlDisabled" placeholder="" ng-change="vm.onChange()" ng-focus="vm.showTooltip(true)" ng-blur="vm.showTooltip(false)" px-unique-field="" autocomplete="off" required="required">
</div>
<span class="fa fa-exclamation-circle inline-error form-control-feedback" ng-style="{left:vm.feedbackIconLeft()}" style="left: 0px;">
</span>
<span class="input-group-addon input-empty-addon" ng-hide="vm.postfix" ng-class="{'px-auto-width': vm.autoWidth}">
</span>
</div>
</div>
<span ng-transclude="" class="px-vertical-middle">
</span>
</div>
</div>
</div>
</div>
<div px-tooltip-view="" px-tooltip="vm.pxTooltip" ng-click="vm.applyAdjustment()" class="ng-isolate-scope">
</div>
</div>
</div>
<span ng-hide="grid.appScope.inEditMode(row, 'rating')" ng-class="{'px-font-highlighted': grid.appScope.showHighlighted(row, 'rating'), 'px-font-enabled': grid.appScope.showEnabled(row, 'rating'), 'px-font-disabled': grid.appScope.showDisabled(row, 'rating'), 'px-cannot-edit': !grid.appScope.clickableCell(row.entity, 'rating')}" class="ng-binding px-font-enabled px-cannot-edit ng-hide">3</span>
</div>
</div>
</div>
</div>
<div ng-repeat="(colRenderIndex, col) in colContainer.renderedColumns track by col.uid"
class="ui-grid-cell ng-scope ui-grid-coluiGrid-09Y" ng-class="{ 'ui-grid-row-header-cell': col.isRowHeader }" ui-grid-cell="">
<div ng-click="grid.appScope.rowColumnSelect(row, 'isDefault')"
ng-class="{'px-grid-row-selected': grid.appScope.highlightCell(row, 'isDefault'), 'px-can-edit': grid.appScope.clickableCell(row.entity, 'isDefault'), 'px-grid-total-row': row.entity.isTotal, 'px-grid-notable-row': row.entity.isNotable}"
class="ng-scope">
<div class="ui-grid-cell-contents ng-hide" ng-hide="grid.appScope.showCell(row, 'isDefault')">
</div>
<div ng-show="grid.appScope.showCell(row, 'isDefault')" name="loanLossAssumptionfalse" class="ui-grid-cell-contents"> <!-- Changes to loanLossAssumptiontrue-->
<div ng-if="grid.appScope.haveData(row)" class="text-center ng-scope">
<input ng-disabled="!grid.appScope.inEditMode(row, 'isDefault')" type="checkbox" ng-input="row.entity.isDefault" ng-model="row.entity.isDefault"
class="ng-pristine ng-valid ng-touched">
</div>
</div>
</div>
</div>
</div>
</div>
In this scenario, I have a handle on the input field with an ID of loanLossAssumptionrating3InputField. From here, I need to get other checkboxes from this ui-grid-row. In this example, one checkbox appears near the end of the markup, before the 6 closing div tags.
Over time we've constructed a library of functions, so there is reference to "lib." in this code. That's just a working reference to our library. This piece of code is in a pageObject.
lib.getCheckboxInRow(fields.ratingFieldId, 0).then(function (checkboxElement) {
// The fields.ratingFieldId value = "loanLossAssumptionrating3InputField"
checkboxElement.click();
});
This is just a "click" attempt, but I'll also need to get that element and determine if the checkbox is clicked. And in my case the only way to do that via the markup is to read an attribute in the parent element. I know how to do that, but am providing this detail to illustrate why I'm attempting to return the element itself.
Here is my targeted function. Two attempts are shown below. Both of these failed. This is in my library.
// Attempt A
public getCheckboxInRow(siblingElementId: string, checkboxIndex: number): webdriver.promise.Promise<protractor.ElementFinder> {
var xPathToRow : string = "./ancestor::div[contains(concat(' ', @class, ' '), ' ui-grid-row ')][1]";
return element(by.id(siblingElementId)).element(by.xpath(xPathToRow)).then(function (row) {
return row.all(by.css("input[@type='checkbox']")).filter.then(function (checkboxArray) {
return checkboxArray[checkboxIndex];
});
});
}
// Attempt B
public getCheckboxInRow(siblingElementId: string, checkboxIndex: number): webdriver.promise.Promise<protractor.ElementFinder> {
var xPathToRow : string = "./ancestor::div[contains(concat(' ', @class, ' '), ' ui-grid-row ')][1]";
var cssToCheckbox : string = "input[@type='checkbox']";
return element(by.id(siblingElementId)).element(by.xpath(xPathToRow)).all(by.css(cssToCheckbox)).filter(function(checkbox) {
return true;
}).then(function (checkboxArray) {
return checkboxArray[checkboxIndex];
});
}
With "Attempt A" the following error occurs.
TypeError: element(...).element(...).then is not a function
The error was more dramatic with "Attempt B".
InvalidSelectorError: invalid selector: An invalid or illegal selector was specified (Session info: chrome=47.0.2526.111) (Driver info: chromedriver=2.19.346078 (6f1f0cde889532d48ce8242342d0b84f94b114a1),platform=Windows NT 6.3 x86_64) (WARNING: The server did not provide any stacktrace information) Command duration or timeout: 30 milliseconds For documentation on this error, please visit: http://seleniumhq.org/exceptions/invalid_selector_exception.html Build info: version: '2.47.1', revision: '411b314', time: '2015-07-30 03:03:16' System info: host: 'PSIDEVKPALM', ip: '10.226.128.27', os.name: 'Windows Server 2012 R2', os.arch: 'x86', os.version: '6.3', java.version: '1.8.0_66' Driver info: org.openqa.selenium.chrome.ChromeDriver Capabilities [{applicationCacheEnabled=false, rotatable=false, mobileEmulationEnabled=false, chrome={userDataDir=C:\Users\kpalmer\AppData\Local\Temp\2\scoped_dir7352_8503}, takesHeapSnapshot=true, databaseEnabled=false, handlesAlerts=true, hasTouchScreen=false, version=47.0.2526.111, platform=WIN8_1, browserConnectionEnabled=false, nativeEvents=true, acceptSslCerts=true, locationContextEnabled=true, webStorageEnabled=true, browserName=chrome, takesScreenshot=true, javascriptEnabled=true, cssSelectorsEnabled=true}] Session ID: 12959162beeed374185734ef29b03b57 *** Element info: {Using=css selector, value=input[@type='checkbox']}
Having spent many hours working on this, I'm at a loss as to why these fail.
Ultimately what I want is a function that will return a checkbox element that is positioned on the same row as another element inside that row, based on the checkbox index, so that we can either click the checkbox or determine if that element is checked or unchecked.
Thanks for your help.
=== Edit 1/2/2016 afternoon ===
Thank you @alecxe, you were a huge help. Here are the final implementations that I ran with. Both of these approaches work.
Either of these will work in my pageObject.
lib.getCheckboxInRow(fields.ratingFieldId, 0).click();
lib.getCheckboxArrayInRow(fields.ratingFieldId).filter(function (checkboxArray, index) {
return (index === 0);
}).first().click();
And these are the targeted functions in the library. One thing that I had to adjust was the return type, from the promise to the element finder. These work like a champ.
public getCheckboxInRow(siblingElementId: string, checkboxIndex: number): protractor.ElementFinder {
var xPathToRow : string = "./ancestor::div[contains(concat(' ', @class, ' '), ' ui-grid-row ')][1]";
return element(by.id(siblingElementId)).element(by.xpath(xPathToRow)).all(by.css("input[type=checkbox]")).filter(function (checkboxArray, index) {
return index === checkboxIndex;
}).first();
};
public getCheckboxArrayInRow(siblingElementId: string): protractor.ElementArrayFinder {
var xPathToRow : string = "./ancestor::div[contains(concat(' ', @class, ' '), ' ui-grid-row ')][1]";
return element(by.id(siblingElementId)).element(by.xpath(xPathToRow)).all(by.css("input[type=checkbox]"));
};
Upvotes: 2
Views: 1708
Reputation: 474151
Let's review your attempts.
Attempt A
You are getting the TypeError: element(...).element(...).then is not a function
error since you cannot resolve an ElementFinder
with then()
since Protractor 2.0.0 (changelog).
You can just continue chaining element
and all()
in this case:
public getCheckboxInRow(siblingElementId: string, checkboxIndex: number): webdriver.promise.Promise<protractor.ElementFinder> {
var xPathToRow : string = "./ancestor::div[contains(concat(' ', @class, ' '), ' ui-grid-row ')][1]";
return element(by.id(siblingElementId)).element(by.xpath(xPathToRow)).all(by.css("input[type=checkbox]")).filter(function (checkboxArray, index) {
return index === checkboxIndex;
}).first();
}
I've also fixed the way you apply the filter()
function. And note the first()
call - this would help us to get a single filtered element and not an array.
Attempt B
You are getting the An invalid or illegal selector was specified
error since you actually have an invalid CSS selector input[@type='checkbox']
- you don't need to prepend @
to attribute names in CSS - replace it with input[type=checkbox]
(no need for single quotes too).
And, you don't apply the filter()
function correctly in this attempt too. Fixed version:
public getCheckboxInRow(siblingElementId: string, checkboxIndex: number): webdriver.promise.Promise<protractor.ElementFinder> {
var xPathToRow : string = "./ancestor::div[contains(concat(' ', @class, ' '), ' ui-grid-row ')][1]";
var cssToCheckbox : string = "input[type=checkbox]";
return element(by.id(siblingElementId)).element(by.xpath(xPathToRow)).all(by.css(cssToCheckbox)).filter(function(checkbox, index) {
return checkboxIndex === index;
}).first();
}
which is essentially becoming the same as Attempt A.
Upvotes: 2