Cypress: Async function using for loop giving same results - cypress

I'm new to cypress framework and trying to achieve the below functionality using cypress.
I have a page with table rows and a dropdown menu on page header. On selecting the option, the dropdown menu gets closed and the body content gets changed/loaded up according to the selected menu options.
Problem: Getting the same length for the table rows for all the menu options selected sequentially, although the table rows count is different for the options.
Here is my code:
it.only('Validate table Row changed length on menu option selection', {defaultCommandTimeout: 10000}, () => {
// opening the dropdown menu
page.openDropdownMenu();
// getting the dropdown options and calculating the length
cy.get('dropdownOptions').then($options => {
// calculating the length
const menuOptionCount = $options.length;
// closing the dropdown menu
page.closeDropdownMenu();
for (let i = 0; i < menuOptionCount; i++) {
// opening the dropdown menu
page.openDropdownMenu();
// clicking the menu option
$options[i].click();
// closing the dropdown menu
page.closeDropdownMenu();
cy.get("body").then($body => {
// always getting the same length for the table rows for all selected options
const rowsLength = $body.find('.table.rows').length;
cy.log('****************Rows length************', rowsLength);
});
}
});
});
Is there any way to write the asynchronous statement to synchronous like (await async in promises) without using any external utility in cypress. As in my previous assignment using Protractor the same thing could be handled using async await as below.
const elementCount = await element(
by.css('[title="Locked By"] .med-filter-header-button div')
).count();

After click() the app rewrites the table, but Cypress does not know that happens and gets the row count before the change occurs.
TLDR - You need to give Cypress more information test correctly. Generally, your test data should be known (not "discovered" by the test code).
Problem #1
You need some way to wait for the row change to finish. Either some text element changes (maybe the first row text), or by adding a .should() on the actual row count.
Something like
const expectedRowCount = [5, 4, 3, 2]
cy.get('dropdownOptions').each(($option, index) => {
page.openDropdownMenu()
$option.click()
page.closeDropdownMenu()
cy.get('.table.rows')
.should('have.length', expectedRowCount[index]) // this will retry until rowsLength changes
.then(rowsLength => {
cy.log('****************Rows length************', rowsLength)
})
})
Problem #2
If "the body content gets changed/loaded" means that the dropdown also gets rewritten with every click, then the loop will fail because $options gets refreshed each time.
You might use the expectedRowCount to loop instead
const expectedRowCount = [5, 4, 3, 2]
expectedRowCount.forEach((expectedCount, index) => {
page.openDropdownMenu()
cy.get('dropdownOptions').eq(index).click()
page.closeDropdownMenu()
cy.get('.table.rows')
.should('have.length', expectedCount) // retries until rowsLength changes
.then(rowsLength => {
cy.log('****************Rows length************', rowsLength)
})
})
The above strategies do not really give you the most solid test.
If you can, check some text that changes upon each iteration,
page.openDropdownMenu()
cy.get('dropdownOptions').then($options => {
let firstRowText = ''; // to control the loop, waiting for this to change
const menuOptionCount = $options.length;
page.closeDropdownMenu();
for (let i = 0; i < menuOptionCount; i++) {
page.openDropdownMenu();
cy.get('dropdownOptions').eq(i).click(); // fresh query each time through the loop
page.closeDropdownMenu();
cy.get('.table.rows').first().invoke('text')
.should('not.eq', firstRowText); // retry until text has changed
.then(newText => firstRowText = newText); // save for next loop
cy.get('.table.rows').then($rows => {
const rowsLength = $rows.length;
cy.log('****************Rows length************', rowsLength);
});
}
})

You can condense your code to something like this. Instead of using a for loop, use each which is a cypress inbuilt method for looping.
it.only(
'Validate table Row changed length on menu option selection',
{defaultCommandTimeout: 10000},
() => {
page.openDropdownMenu()
cy.get('dropdownOptions').each(($options, index) => {
cy.wrap($options).eq(index).click()
page.closeDropdownMenu()
cy.get('.table.rows')
.its('length')
.then((rowsLength) => {
cy.log('****************Rows length************', rowsLength)
})
page.openDropdownMenu()
})
page.closeDropdownMenu()
}
)

Related

How to check if element is fully uncovered

This is NOT the common cypress issue where you get a test failure because a fixed element is covered by another element.
I have an expandable list toward the top of my page. When it expands, I want it to be on top of every other aspect of the page. So I'm writing a cypress test to verify that nothing else is covering it.
Unfortunately, the test isn't failing in a clear failure case.
This test is succeeding for the above list
cy.get('#list')
.should('be.visible')
.find('p').each(($listItem) => {
cy.wrap($listItem)
.should('be.visible')
.click(); // another layer of cover check
});
I imagine this is succeeding because the elements aren't hidden and thus are 'visible,' and the click is succeeding because the center of each element is uncovered. How can I test that the list body is fully uncovered/displaying on top?
In the image the list is right-ish of the dropdown, so this is one way to check for overlap in the x-dimension:
cy.get('#dropdown').then($el => {
const rhs = Math.round($el[0].getBoundingClientRect().right)
cy.get('#list').then($el => {
const lhs = Math.round($el[0].getBoundingClientRect().left)
expect(lhs).to.be.gt(rhs)
})
})
It looks like there's also a table on the page which you'd want to repeat the check for.
To generalize a little bit:
Cypress.Commands.add('hasNoOverlapWith', {prevSubject: true}, (subject, others) => {
let covered = false;
const targetRect = subject[0].getBoundingClientRect()
others.forEach(other => {
cy.get(other).then($el => {
const otherRect = $el[0].getBoundingClientRect()
// other covers from the left
const coveredLeft = otherRect.right >= targetRect.left &&
otherRect.right <= targetRect.right
// other covers from the right
const coveredRight = otherRect.left <= targetRect.right &&
otherRect.left >= targetRect.left
if (!covered) {
covered = coveredLeft || coveredRight
}
})
})
cy.then(() => {
expect(covered).to.eq(false)
})
})
cy.get('#list').hasNoOverlapWith(['#dropdown', '#table'])

Cypress: Get all elements containing a given string / regex

I'm trying to test a pagination bar with cypress.
I want to assert the number of buttons containing a number only in this bar, and ignore the other buttons (previous page, next page...)
The buttons are looking like this:
<button class="...">33</button>
I first tried this test:
cy.get('.pagination')
.find('button')
.contains(/\d+/)
.should('have.length.gte', 2)
But this gave me a warning about the fact that contains will only return one element, making the "length" test useless.
I also tried many combinations based on filter, the ":contains" jquery keyword, but none worked:
.filter(`:contains('/\d+\')`)
// >> finds nothing
.filter((elt) => { return elt.contains(rx) })
// >> throws 'elt.contains is not a function'
.filter((elt) => { return rx.test(elt.text()) })
// >> throws 'elt.text is not a function'
.filter(() => { return rx.test(Cypress.$(this).text()) })
// filter everything and return nothing, even the buttons containing the text '1'
.filter() with a callback has parameters (index, elt) => {} which means you can use it like this
cy.get('.pagination')
.find('button')
.filter((index, elt) => { return elt.innerText.match(/\d+/) })
.should('have.length.gte', 2)
nextAll() might work in this situation:
cy
.get('.pagination')
.find('button')
.contains(/\d+/)
.nextAll()
.should('have.length.gte', 2);
Another solution might be to distinguish the pagination buttons by something else, like a class, or some html attribute that is unique to them.
You can use an loop through the elements and match the element text and then increment a count variable and then later validate it, something like:
var count =0
cy.get('.pagination').find('button').each(($ele) => {
if(/\d+/.test($ele.text()){
count++
}
})
expect(count).to.be.greaterThan(2)
You can do other things as well like:
Assertions
cy.get('.pagination').find('button').each(($ele) => {
if(/\d+/.test($ele.text()){
expect(+$ele.text().trim()).to.be.a('number')
}
})
Perform Click
cy.get('.pagination').find('button').each(($ele) => {
if(/\d+/.test($ele.text()){
cy.wrap($ele).click()
}
})
Validate Inner text
cy.get('.pagination').find('button').each(($ele) => {
if(/\d+/.test($ele.text()){
cy.wrap($ele).should('have.text', 'sometext')
}
})
nextAll() fails if there's element wrapping the buttons, but you can count the wrappers.
cy.get('.pagination')
.find('button') // presume this is 'Prev' button
.parent()
.nextAll(':not(:contains(Next))')
.should('have.length.gte', 2)
or .nextUntil()
cy.get('.pagination')
.find('button') // presume this is 'Prev' button
.parent()
.nextUntil(':contains(Next)')
.should('have.length.gte', 2)
or .children()
cy.get('.pagination')
.children(':not(:contains(Prev)):not(:contains(Next))')
.should('have.length.gte', 2)
Overall, .filter() is better as it does not assume the HTML structure.

Cypress: How to scroll a dropdown to find item

I need to click a dropdown list and scroll to find an item by text.
At the moment I know that the item is at the bottom of the list so I can do:
cy.get('.ng-dropdown-panel-items').scrollTo("bottom").contains(/test/i).click()
and this works, but if the item moves and is no longer at the bottom, this will break.
I tried scrollIntoView but with no luck:
cy.get('.ng-dropdown-panel-items').contains(/test/i).scrollIntoView().click()
and
cy.get('.ng-dropdown-panel-items').scrollIntoView().contains(/test/i).click()
Does anyone know how I can do this?
Update: the list of options is dynamically generated (not all options are in the DOM initially) so scrolling to the bottom is required to get all options. Once all options are available .contains() can be used to find the element.
The Angular ng-select in virtual mode is quite tricky to handle.
It's list is virtual, which means it only has some of the items in the DOM at one time, so you can't select them all and iterate over them.
You can recursively scan the options list and use .type({downarrow}) to move new options into the DOM (which is one way a user would interact with it).
it('selects an option in a virtual-scroll ng-select', () => {
cy.visit('https://ng-select.github.io/ng-select#/virtual-scroll')
cy.get('ng-select').click(); // open the dropdown panel
cy.get('.ng-option')
.should('have.length.gt', 1); // wait for the option list to populate
function searchForOption(searchFor, level = 0) {
if (level > 100) { // max options to scan
throw 'Exceeded recursion level' // only useful for 100's
} // not 1000's of options
return cy.get('ng-select input')
.then($input => {
const activeOptionId = $input.attr('aria-activedescendant') // highlighted option
const text = Cypress.$(`#${activeOptionId}`).text() // get it's text
if (!text.includes(searchFor)) { // not the one?
cy.wrap($input).type('{downarrow}') // move the list
return searchForOption(searchFor, level + 1) // try the next
}
return cy.wrap(Cypress.$(`#${activeOptionId}`))
})
}
searchForOption('ad et natus qui').click(); // select the matching option
cy.get('.ng-value')
.should('contain', 'ad et natus qui'); // confirm the value
})
Note that recursion can be hard on memory usage, and this code could be optimized a bit.
For most cases you would need cy.get().select like for example:
cy.get('.ng-dropdown-panel-items').select(/test/i)
You can use an each() to loop through the drop down elements and when you find the desired text, make an click().
cy.get('span.ng-option-label.ng-star-inserted').each(($ele) => {
if($ele.text() == 'desired text') {
cy.wrap($ele).click({force: true})
}
})
Try something like below recursive function:
function scrollUntilElementFound(scrollIndex) {
scrollIndex = scrollIndex+10;
if(!cy.get('.ng-dropdown-panel-items').contains(/test/i)){
cy.get('.ng-dropdown-panel-items').scrollTo(scrollIndex);
scrollUntilElementFound(scrollIndex);
} else {
//element found
return;
}
}

Suggestions to fix a flaky test in cypress

I have a map-based application (like Google Maps) and I am writing tests for the zoom-in option. The test covering all the zoom levels when zooming in. My test is working but the result is not consistent and it is flaky.
My code:
static verifyAllAvailableZoomInZoomSizeInNM() {
var expectedValues = [500,200,100,50,20,10,5,2,1,0.5,0.2,0.1,0.05,0.02,0.01,0.005,0.002,0.001,0.0005,0.0002,0.0001,0.00005,0.00002];
cy.getCurrentZoomSizes({
numValues: 26,//I expect to give 23 but I just gave 26 in order to capture all sizes
waitBetween: 1000,
elementLocator: BTN_MAP_ZOOMIN,
}).should("be.eql", expectedValues);
}
Cypress Commands:
/* Get the numeric value of zoom size in nm */
Cypress.Commands.add("getCurrentZoomSize", () => {
cy.get('div[class="ol-scale-line-inner"]').then(
($el) => +$el[0].innerText.replace(" nm", "")
);
});
/* Get a sequence of zoom size values */
Cypress.Commands.add("getCurrentZoomSizes", ({ numValues, waitBetween, elementLocator }) => {
const values = [];
Cypress._.times(numValues, () => {
cy.getCurrentZoomSize()
.then((value) => values.push(value))
cy.get(elementLocator)
.click()
.wait(waitBetween);
});
return cy.wrap(values);
});
And the test result1:
test result2:
As you can see in the screenshots, a few of the zoom sizes had duplicated. I tried giving enough wait between each zoom-in click but it is not helping either. Is there any way, I can fix this flaky test?
The loop executes a lot faster than the Cypress commands or the zoom operation, you can see it if you add a console.log() just inside the loop
Cypress._.times(numValues, (index) => {
console.log(index)
That's not necessarily a problem, it just fills up the command queue really quickly and the commands then chug away.
But in between getCurrentZoomSize() calls you need to slow things down so that the zoom completes, and using .wait(waitBetween) is probably why thing get flaky.
If you apply the .should() to each zoom level, you'll get retry and wait in between each zoom action.
The problem is figuring out how to arrange things so that the proper retry occurs.
If you do
cy.getCurrentZoomSize()
.should('eq', currentZoom);
which is equivalent to
cy.get('div[class="ol-scale-line-inner"]')
.then($el => +$el[0].innerText.replace(" nm", "") )
.should('eq', currentZoom);
it doesn't work, the conversion inside the .then() gets in the way of the retry.
This works,
cy.get('div[class="ol-scale-line-inner"]')
.should($el => {
const value = +$el[0].innerText.replace(" nm", "")
expect(value).to.eq(expectedValue)
})
or this
cy.get('div[class="ol-scale-line-inner"]')
.invoke('text')
.should('eq', `${currentZoom} nm`);
So the full test might be
Cypress.Commands.add("getCurrentZoomSizes", (expectedValues, elementLocator) => {
const numValues = expectedValues.length;
Cypress._.times(numValues, (index) => {
const currentZoom = expectedValues[index];
cy.get('div[class="ol-scale-line-inner"]')
.invoke('text')
.should('eq', ${currentZoom} nm`); // repeat scale read until zoom finishes
// or fail if never gets there
cy.get(elementLocator).click(); // go to next level
});
});
const expectedValues = [...
cy.getCurrentZoomSizes(expectedValues, BTN_MAP_ZOOMIN)

cypress.io how to remove items for 'n' times, not predictable, while re-rendering list itself

I've a unpredictable list of rows to delete
I simply want to click each .fa-times icon
The problem is that, after each click, the vue.js app re-render the remaining rows.
I also tried to use .each, but in this cas I got an error because element (the parent element, I think) has been detached from DOM; cypress.io suggest to use a guard to prevent this error but I've no idea of what does it mean
How to
- get a list of icons
- click on first
- survive at app rerender
- click on next
- survive at app rerender
... etch...
?
Before showing one possible solution, I'd like to preface with a recommendation that tests should be predictable. You should create a defined number of items every time so that you don't have to do hacks like these.
You can also read more on conditional testing, here: https://docs.cypress.io/guides/core-concepts/conditional-testing.html#Definition
That being said, maybe you have a valid use case (some fuzz testing perhaps?), so let's go.
What I'm doing in the following example is (1) set up a rendering/removing behavior that does what you describe happens in your app. The actual solution (2) is this: find out how many items you need to remove by querying the DOM and checking the length, and then enqueue that same number of cypress commands that query the DOM every time so that you get a fresh reference to an element.
Caveat: After each remove, I'm waiting for the element (its remove button to be precise) to not exist in DOM before continuing. If your app re-renders the rest of the items separately, after the target item is removed from DOM, you'll need to assert on something else --- such as that a different item (not the one being removed) is removed (detached) from DOM.
describe('test', () => {
it('test', () => {
// -------------------------------------------------------------------------
// (1) Mock rendering/removing logic, just for the purpose of this
// demonstration.
// -------------------------------------------------------------------------
cy.window().then( win => {
let items = ['one', 'two', 'three'];
win.remove = item => {
items = items.filter( _item => _item !== item );
setTimeout(() => {
render();
}, 100 )
};
function render () {
win.document.body.innerHTML = items.map( item => {
return `
<div class="item">
${item}
<button class="remove" onclick="remove('${item}')">Remove</button>
</div>
`;
}).join('');
}
render();
});
// -------------------------------------------------------------------------
// (2) The actual solution
// -------------------------------------------------------------------------
cy.get('.item').then( $elems => {
// using Lodash to invoke the callback N times
Cypress._.times($elems.length, () => {
cy.get('.item:first').find('.remove').click()
// ensure we wait for the element to be actually removed from DOM
// before continuing
.should('not.exist');
});
});
});
});

Resources