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'])
Related
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()
}
)
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.
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)
Question regarding react-navigation v5.
In previous versions, we were able to specify custom transition for specific routes, not screens, by doing the following inside a StackNavigator:
transitionConfig: () => ({
screenInterpolator: sceneProps => {
const {scenes, scene} = sceneProps;
const prevRoute = scenes[0].route.routeName === 'Route A';
// If prev route is A, and then current route is B, then we do a specific transition
if (prevRoute && scene.route.routeName === 'Route B') {
return StackViewStyleInterpolator.forVertical(sceneProps);
}
// Otherwise default to normal transition
return StackViewStyleInterpolator.forHorizontal(sceneProps);
},
}),
Now, I'm trying to achieve the same for react-navigation v5. I know I'm able to specify a custom animation per screen by doing something like:
<Stack.Screen name="Route B" component={RouteB} options={{ cardStyleInterpolator: CardStyleInterpolators.forVerticalIOS }} />
The problem is that I don't want this transition applied every time it is navigated to RouteB, ONLY when the previous route is RouteA, I want this transition applied, just like the previous code block above.
Couldn't find any example in the docs so would appreciate some help in migrating the code over to v5.
Something like this should work:
options={({ navigation, route }) => {
const state = navigation.dangerouslyGetState();
const index = state.routes.indexOf(route);
const previousRoute = state.routes[index - 1];
if (previousRoute?.name === 'RouteA') {
return {
cardStyleInterpolator: CardStyleInterpolators.forVerticalIOS,
gestureDirection: 'vertical',
};
} else {
return {};
}
}}
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');
});
});
});
});