I have 15 buttons on a page. I need to test each button.
I tried a simple for loop, like
for (var i = 1; i < 15; i++) {
cy.get("[=buttonid=" + i + "]").click()
}
But Cypress didn't like this. How would I write for loops in Cypress?
To force an arbitrary loop, I create an array with the indices I want, and then call cy.wrap
var genArr = Array.from({length:15},(v,k)=>k+1)
cy.wrap(genArr).each((index) => {
cy.get("#button-" + index).click()
})
Lodash is bundled with Cypress and methods are used with Cypress._ prefix.
For this instance, you'll be using the _.times. So your code will look something like this:
Cypress._.times(15, (k) => {
cy.get("[=buttonid=" + k + "]").click()
})
You can achieve something similar to a "for loop" by using recursion.
I just posted a solution here: How to use a while loop in cypress? The control of is NOT entering the loop when running this spec file? The way I am polling the task is correct?
Add this to your custom commands:
Cypress.Commands.add('recursionLoop', {times: 'optional'}, function (fn, times) {
if (typeof times === 'undefined') {
times = 0;
}
cy.then(() => {
const result = fn(++times);
if (result !== false) {
cy.recursionLoop(fn, times);
}
});
});
Then you can use it by creating a function that returns false when you want to stop iterating.
cy.recursionLoop(times => {
cy.wait(1000);
console.log(`Iteration: ${times}`);
console.log('Here goes your code.');
return times < 5;
});
While cy.wrap().each() will work (one of the answers given for this question), I wanted to give an alternate way that worked for me. cy.wrap().each() will work, but regular while/for loops will not work with cypress because of the async nature of cypress. Cypress doesn't wait for everything to complete in the loop before starting the loop again. You can however do recursive functions instead and that waits for everything to complete before it hits the method/function again.
Here is a simple example to explain this. You could check to see if a button is visible, if it is visible you click it, then check again to see if it is still visible, and if it is visible you click it again, but if it isn't visible it won't click it. This will repeat, the button will continue to be clicked until the button is no longer visible. Basically the method/function is called over and over until the conditional is no longer met, which accomplishes the same thing as a for/while loop, but actually works with cypress.
clickVisibleButton = () => {
cy.get( 'body' ).then( $mainContainer => {
const isVisible = $mainContainer.find( '#idOfElement' ).is( ':visible' );
if ( isVisible ) {
cy.get( '#idOfElement' ).click();
this.clickVisibleButton();
}
} );
}
Then obviously call the this.clickVisibleButton() in your test. I'm using typescript and this method is setup in a class, but you could do this as a regular function as well.
// waits 2 seconds for each attempt
refreshQuote(attempts) {
let arry = []
for (let i = 0; i < attempts; i++) { arry.push(i) }
cy.wrap(arry).each(() => {
cy.get('.quote-wrapper').then(function($quoteBlock) {
if($quoteBlock.text().includes('Here is your quote')) {
}
else {
cy.get('#refreshQuoteButton').click()
cy.wait(2000)
}
})
})
}
Try template literals using backticks:
for(let i = 0; i < 3; i++){
cy.get(`ul li:nth-child(`${i}`)).click();
}
Related
I'm manipulating some angular services/functions via Chrome console. (I have to specifically do this for a task I'm working on).
What I want to do is wait for the AddBagIfLimitNotReached() function to execute and finish running. And only then access the variable this.option.Quantity.
angular.element(document.querySelector(".quantity-button")).controller()._registeredControls[1].Scope.AddBagIfLimitNotReached = async function(n) {
console.log("tthis", this)
if (this.HasReachedMaximumBaggageAllowance()) {
angular.element(document.querySelector(".quantity-button")).controller()._registeredControls[1].LuggageDrawersService.OpenLuggageLimitReachedDrawer();
return;
}
this.AddBag(n);
console.log("Quantity", this.option.Quantity);
};
With this function, I'm adding a product to my basket. And this.option.Quantity should console.log 1. But it actually consoles.log 0.
However, if I check the object itself, it shows 1.
So I think what is happening, is I'm console.logging my bag quantity, before the bag has actually finished being added to the basket.
For example, if I added a settimeout of 2 seconds, the correct bag value = 1 is console.logged.
angular.element(document.querySelector(".quantity-button")).controller()._registeredControls[1].Scope.AddBagIfLimitNotReached = async function(n) {
console.log("tthis", this)
if (this.HasReachedMaximumBaggageAllowance()) {
angular.element(document.querySelector(".quantity-button")).controller()._registeredControls[1].LuggageDrawersService.OpenLuggageLimitReachedDrawer();
return;
}
this.AddBag(n);
// Returns 1
setTimeout(function(){ console.log("Quantity", this.option.Quantity); }, 2000);
};
Is there a better way I can achieve this, without using settimeout? I have tried async/await/promises, but I still can't seem to find a way to wait for the function to finish loading.
Async/await returns an error - it doesn't like the function this.HasReachedMaximumBaggageAllowance() and throws an error stating this.HasReachedMaximumBaggageAllowance is not a function.
Any tips/ideas would be much appreciated.
I found a solution, I'm using $watch, to watch a key/value, in the this object. And this seems to work:
angular.element(document.querySelector(".quantity-button.plus-button")).controller()._registeredControls[1].Scope.AddBagIfLimitNotReached = function(n) {
let bagCount = this.option.Quantity;
console.log("bagCount", bagCount);
if (this.HasReachedMaximumBaggageAllowance()) {
angular.element(document.querySelector(".quantity-button.plus-button")).controller()._registeredControls[1].LuggageDrawersService.OpenLuggageLimitReachedDrawer();
return;
};
this.AddBag(n);
this.$watch("this.option.Quantity", function (newValue) {
console.log(`Value of foo changed ${newValue}`);
if (newValue > 0) {
document.querySelector(`.luggage-tile-weight-${n.Weight} .tile-title .tick-box`).classList.add("green-tick");
displayGreenTickNoBagSelected();
};
if (newValue === 0) {
document.querySelector(`.luggage-tile-weight-${n.Weight} .tile-title .tick-box`).classList.remove("green-tick");
displayGreenTickNoBagSelected();
};
});
};
I am writing an E2E test with protractor. I had to fetch information from the browser and execute a step multiple times.
I am testing one screen which will start when a
User clicks 'Start'
lands on a new page
The workflow below is invoked with count being passed as argument
id the html id does not change. the value changes when queried again after submitting the current form.
for(i = 0 ; i < count ; i++){
console.log("counter is "+i);
element(by('id')).evaluate('value').then(function(v) {
// do some action on UI based on v
element(by('id1')).sendKeys(v+v);
// submit etc.,
// some angular code runs in the frontend.
}
// need to wait since webdriver jumps to the next one without this completing
}
Many blog posts/documentations suggests you cannot use it in a loop, but does not suggest any alternative way to do this.
Any suggestions appreciated.
Never use protractor element statements inside loop: The simple reason is that the webdriverJS (protractor) API is asynchronous. Element statements returns a promise and that promise is in unresolved state while the code below the statements continues to execute. This leads to unpredictable results. Hence, it is advisable to use recursive functions instead of loops.
source: http://engineering.wingify.com/posts/angularapp-e2e-testing-with-protractor/
Edit: updated question with details of workflow.
It is usually not recommended to use a loop when an iteration has an asynchronous call.
The reason is that the first asynchronous calls is executed after the last iteration of the loop when i is already equal to count.
Thus, it makes it difficult to break the loop and to keep track of the value of i.
On way to tackle the issue is to use a recursive function :
var count = 3;
var results = [];
function iterate(i, n) {
if(i < n) {
console.log(`counter is ${i}`);
browser.refresh();
return element(by.id('h-top-questions')).getText().then(function(text) {
results.push(`${i}:${text}`);
return iterate(i + 1, n);
});
}
}
iterate(0, count).then(function(){
console.log("done!", results);
});
But a better way would be to iterate with promise.map on an array sized to the number of iterations:
var count = 3;
protractor.promise.map(Array(count).fill(0), function(v, i) {
console.log(`counter is ${i}`);
browser.refresh();
return element(by.id('h-top-questions')).getText().then(function(text) {
return `${i}:${text}`;
});
}).then(function(results){
console.log("done!", results);
});
You could also keep using a loop. First you'll have to use the let statement to get the value of i in an asynchronous function (ES6).
Then call all the synchronous code with browser.call to synchronize the execution:
var count = 3;
var results = [];
for(let i = 0 ; i < count ; i++){
browser.call(function(){
console.log(`counter is ${i}`);
browser.refresh();
element(by.id('h-top-questions')).getText().then(function(text) {
results.push(`${i}:${text}`);
});
});
}
browser.call(function() {
console.log("done!", results);
});
Looping in protractor works like this
describe('Describe something', function() {
var testParams = [1,2,3,4,5,6,7,8,9,10];
beforeEach( function() {
// ...
});
for (var i = 0; i < testParams.length; i++) {
(function (testSpec) {
it('should do something', function() {
// inside loop
});
})(testParams[i]);
};
});
Edit : I might be mis-understanding your question, but it seems to me you want to complete all(dynamic count) actions on the page, before going to the next one ?
it('should clear old inspections', function() {
inspectieModuleInspectieFixture.getRemoveInspectionButton().count().then(function (value) {
if(value == 0){
console.log('--- no inspections to remove ---');
}
for(var i = 0; i < value; i++){
//global.waitForClickable(inspectieModuleInspectieFixture.getRemoveInspectionButtonList(i+1));
inspectieModuleInspectieFixture.getRemoveInspectionButtonList(i+1).click();
console.log('iteration '+i + 'count '+value )
};
});
global.wait(5000);
}); */
this counts elements on the page and then it performs an action for the ammount of elements it found
In the above example I use containers to hold my elements, so my code remains readable (i.e. inspectieModuleInspectieFixture.getRemoveInspectionButton() holds $(".elementSelectorExample")
There is also a 'global.waitForClickable' commented, that is reffering to a 'time module' I've created that extends the functionality of 'wait', in this case it waits till the element is vissible/clickable.
This is easily mirrored perhaps something like this :
waitForElementNoDisplay: function(element){
return browser.wait(function() {
return element.isDisplayed().then(function(present) {
return !present;
})
});
},
this will make protractor WAIT untill an element is no longer displayed.(Display:none)
If you need to perform some action on every element, it is true, that better to not use loops. Use .map() or .each() or .filter() instead
Still not quite sure what you what to do, but here is example how i am doing similar tasks, when you need to make number of actions depending on data from the page:
class SomePage {
typeValueForEachElement(elements) {
elements.each((elem, index)=> {
elem.getAttribute('value').then(value=> {
elem.sendKeys(value + value)
elem.submit()
})
})
}
}
new SomePage().typeValueForEachElement($$('your locator here'))
Here is api reference that might help
http://www.protractortest.org/#/api?view=ElementArrayFinder.prototype.map
http://www.protractortest.org/#/api?view=ElementArrayFinder.prototype.reduce
http://www.protractortest.org/#/api?view=ElementArrayFinder.prototype.each
http://www.protractortest.org/#/api?view=ElementArrayFinder.prototype.filter
I have the following test (for the sake of brievity I've removed the page object):
element(by.model("elc.search.placeOfBirth")) //this is a select
element(by.model("elc.search.placeOfBirth")).all(by.tagName("option")).then(function(options) {
for(var i = 0; i < options.length; i++) {
options[i].getText().then(function(text) {
if(text !== "---") {
element(by.model("elc.search.placeOfBirth")).sendKeys(text);
var firstRow = element.all(by.repeater("employee in elc.filtered")).first();
firstRow.all(by.tagName("td")).then(function(cells) {
expect(cells[4].getText()).toBe(text);
});
var lastRow = element.all(by.repeater("employee in elc.filtered")).last();
lastRow.all(by.tagName("td")).then(function(cells) {
expect(cells[4].getText()).toBe(text);
});
}
});
}
});
Let me explain what's happening here. I have a table and a select box above it. The table's 5th column is related to the select combobox and the array I use in ng-repeat for the table is filtered by the value in the combobox. What I wanted to do here is to go over the values in the combobox, select a particular value and make sure the table has that value in the first and last row.
If I but browser.debugger() in the loop this works and the test passes, however if I don't debug the testing seems to go too fast and my table doesn't get updated quickly enough and the tests fail. I'm guessing this is due to the fact that a promise isn't resolved and the code keeps running, but I'm not sure which promise I'm waiting for, as I've also tried to put a .then(function() {...} right after I send the keys to the combobox.
I guess your for loop executes quickly and so the code inside it waiting for promises gets skipped. You can avoid it by executing a function inside the for loop. Update your code to do it -
var someFunction = function(options, i){
//Write your code that was inside your for loop
};
element(by.model("elc.search.placeOfBirth")).all(by.tagName("option")).then(function(options) {
for(var i = 0; i < options.length; i++) {
someFunction(options, i);
}
});
However there's a better solution for this problem. Use the inbuilt loops that protractor has .each() or .map() to get your job done. Here's how -
element(by.model("elc.search.placeOfBirth")).all(by.tagName("option")).each(function(option) {
option.getText().then(function(text) {
if(text !== "---") {
element(by.model("elc.search.placeOfBirth")).sendKeys(text);
var firstRow = element.all(by.repeater("employee in elc.filtered")).first();
firstRow.all(by.tagName("td")).then(function(cells) {
expect(cells[4].getText()).toBe(text);
});
var lastRow = element.all(by.repeater("employee in elc.filtered")).last();
lastRow.all(by.tagName("td")).then(function(cells) {
expect(cells[4].getText()).toBe(text);
});
}
});
});
You may use wait's in between to make sure your DOM is updated before you perform any operation. Hope it solves your issue.
I'm trying to test if an element is visible using protractor. Here's what the element looks like:
<i class="icon-spinner icon-spin ng-hide" ng-show="saving"></i>
When in the chrome console, I can use this jQuery selector to test if the element is visible:
$('[ng-show=saving].icon-spin')
[
<i class="icon-spinner icon-spin ng-hide" ng-show="saving"></i>
]
> $('[ng-show=saving].icon-spin:visible')
[]
However, when I try to do the same in protractor, I get this error at runtime:
InvalidElementStateError:
invalid element state: Failed to execute 'querySelectorAll' on 'Document':
'[ng-show=saving].icon-spin:visible' is not a valid selector.
Why is this not valid? How can I check for visibility using protractor?
This should do it:
expect($('[ng-show=saving].icon-spin').isDisplayed()).toBe(true);
Remember protractor's $ isn't jQuery and :visible is not yet a part of available CSS selectors + pseudo-selectors
More info at https://stackoverflow.com/a/13388700/511069
The correct way for checking the visibility of an element with Protractor is to call the isDisplayed method. You should be careful though since isDisplayed does not return a boolean, but rather a promise providing the evaluated visibility. I've seen lots of code examples that use this method wrongly and therefore don't evaluate its actual visibility.
Example for getting the visibility of an element:
element(by.className('your-class-name')).isDisplayed().then(function (isVisible) {
if (isVisible) {
// element is visible
} else {
// element is not visible
}
});
However, you don't need this if you are just checking the visibility of the element (as opposed to getting it) because protractor patches Jasmine expect() so it always waits for promises to be resolved. See github.com/angular/jasminewd
So you can just do:
expect(element(by.className('your-class-name')).isDisplayed()).toBeTruthy();
Since you're using AngularJS to control the visibility of that element, you could also check its class attribute for ng-hide like this:
var spinner = element.by.css('i.icon-spin');
expect(spinner.getAttribute('class')).not.toMatch('ng-hide'); // expect element to be visible
I had a similar issue, in that I only wanted return elements that were visible in a page object. I found that I'm able to use the css :not. In the case of this issue, this should do you...
expect($('i.icon-spinner:not(.ng-hide)').isDisplayed()).toBeTruthy();
In the context of a page object, you can get ONLY those elements that are visible in this way as well. Eg. given a page with multiple items, where only some are visible, you can use:
this.visibileIcons = $$('i.icon:not(.ng-hide)');
This will return you all visible i.icons
If there are multiple elements in DOM with same class name. But only one of element is visible.
element.all(by.css('.text-input-input')).filter(function(ele){
return ele.isDisplayed();
}).then(function(filteredElement){
filteredElement[0].click();
});
In this example filter takes a collection of elements and returns a single visible element using isDisplayed().
This answer will be robust enough to work for elements that aren't on the page, therefore failing gracefully (not throwing an exception) if the selector failed to find the element.
const nameSelector = '[data-automation="name-input"]';
const nameInputIsDisplayed = () => {
return $$(nameSelector).count()
.then(count => count !== 0)
}
it('should be displayed', () => {
nameInputIsDisplayed().then(isDisplayed => {
expect(isDisplayed).toBeTruthy()
})
})
To wait for visibility
const EC = protractor.ExpectedConditions;
browser.wait(EC.visibilityOf(element(by.css('.icon-spinner icon-spin ng-hide')))).then(function() {
//do stuff
})
Xpath trick to only find visible elements
element(by.xpath('//i[not(contains(#style,"display:none")) and #class="icon-spinner icon-spin ng-hide"]))
element(by.className('your-class-name'))
.isDisplayed()
.then(function (isVisible) {
if (isVisible) { // element is visible
} else { // element is not visible
}
})
.catch(function(err){
console.error("Element is not found! ", err);
})
Here are the few code snippet which can be used for framework which use Typescript, protractor, jasmine
browser.wait(until.visibilityOf(OversightAutomationOR.lblContentModal), 3000, "Modal text is present");
// Asserting a text
OversightAutomationOR.lblContentModal.getText().then(text => {
this.assertEquals(text.toString().trim(), AdminPanelData.lblContentModal);
});
// Asserting an element
expect(OnboardingFormsOR.masterFormActionCloneBtn.isDisplayed()).to.eventually.equal(true
);
OnboardingFormsOR.customFormActionViewBtn.isDisplayed().then((isDisplayed) => {
expect(isDisplayed).to.equal(true);
});
// Asserting a form
formInfoSection.getText().then((text) => {
const vendorInformationCount = text[0].split("\n");
let found = false;
for (let i = 0; i < vendorInformationCount.length; i++) {
if (vendorInformationCount[i] === customLabel) {
found = true;
};
};
expect(found).to.equal(true);
});
Something to consider
.isDisplayed() assumes the element is present (exists in the DOM)
so if you do
expect($('[ng-show=saving]').isDisplayed()).toBe(true);
but the element is not present, then instead of graceful failed expectation, $('[ng-show=saving]').isDisplayed() will throw an error causing the rest of it block not executed
Solution
If you assume, the element you're checking may not be present for any reason on the page, then go with a safe way below
/**
* element is Present and is Displayed
* #param {ElementFinder} $element Locator of element
* #return {boolean}
*/
let isDisplayed = function ($element) {
return (await $element.isPresent()) && (await $element.isDisplayed())
}
and use
expect(await isDisplayed( $('[ng-show=saving]') )).toBe(true);
waitTillElementIsPresent(locator: Locator): promise.Promise<boolean>
{
const EC = protractor.ExpectedConditions;
return browser.wait(EC.visibilityOf(element(by.id('xyz')), browser.params.explicitWaitTime, 'Element taking too long to appear in the DOM');
}
const isDisplayed = await $('div').isDisplayed().then(null, err => false)
I'm trying to loop through a list on a timer (currently 1 second but I want it faster). The problem is that the current value isn't being updated visually & I can't see why.
When I loop through with Firebug it works as intended but without firebug it's not showing the text change...is it skipping the text update somehow?
I set the timer to 1 second; surely a .html() call wouldn't take longer? Thanks for any info.
My HTML is simply:
<div id="winner2"><h1></h1></div>
<div id="button"">
<img id="start" src="start.png" />
<img id="stop" src="stop.png" />
</div>
My JS is:
var people = ["a", "b", "c"];
var counter = 1;
var run;
$(document).ready(function(){
$("#winner2 h1").html("a");
$("#stop").hide();
$( "#start" ).click(function() {
$("#start").hide();
$("#stop").show();
run = setTimeout (nameIncrement(),1000);
});
$( "#stop" ).click(function() {
$("#stop").hide();
$("#start").show();
clearTimeout(run);
});
function nameIncrement() {
if(counter == people.length) {
counter=0;
}
$("#winner2 h1").html(people[counter]);
counter++;
run = setTimeout (nameIncrement(),1000);
}
});
You are calling nameIncrement() and passing its return value to setTimeout(), rather than passing nameIncrement.
Remove the parentheses:
run = setTimeout(nameIncrement, 1000);
jsfiddle
It looks like you are running a recursive function without defining exit condition. this probably causes overload to the browser which decided not to run the function.
Try:
function nameIncrement() {
if(counter == people.length) {
counter=0;
return;
}
$("#winner2 h1").html(people[counter]);
counter++;
run = setTimeout (nameIncrement(),1000);
}
});
on debug mode, however, the browser is less defensive, so you can see your erors yourself.