Suggestions to fix a flaky test in cypress - 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)

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: Async function using for loop giving same results

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()
}
)

Spying on canvas context methods with Cypress

I am spying on an element that has a canvas, which draws a canvas and clears it every second with context.clearRect(...), and I'm trying to spy on that with cypress:
describe('canvas clears', () => {
let context;
let spy;
beforeEach(() => {
cy.visitHome();
cy.sendStudyUpdateWithSingleSeries(seriesWithNoFrames); // this places the canvas on the screen
cy.get('[data-testid=data]').then(([canvas]) => {
context = (canvas as HTMLCanvasElement).getContext('2d');
spy = cy.spy(context, 'clearRect');
});
});
it('works', () => {
expect(spy).to.be.called;
});
});
Cypress makes its way throught he beforeEach section and immediately goes on to the test which immediately fails. However, after the failure is printed, the Spy shows up as called, and indeed updates its call count every second. But the test isn't looking for it anymore, I guess?
How do I make this spy work?
(note: I get the same results with expect(spy).to.have.been.called)

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');
});
});
});
});

Protractor does not perceive a quick change

This is my protractor test:
it("should check email validity", function(){
var resetButton = element(by.id('reset-button'));
element(by.model('Contact.email')).sendKeys('nick');
element.all(by.css('.form-control-error')).each(function (elem, index) {
if (index===1) {
expect(elem.isPresent()).toBe(true);
element(by.model('Contact.email')).sendKeys('#gmail.com').then(
function(){
expect(elem.isPresent()).toBe(false);
}
)
}
});
});
Behind that code there is a form with some input texts. The second one includes the email.form-control-erroris an error message which appears whenever the email format is not correct. The first time expect(elem.isPresent()).toBe(true);passes the test, the second time it does not, even if the error message disappears from the UI. It seems that Protractor does not perceive the fast change; however, it should because it is inside a promise. Do you have any explanation for that?
You should make things more reliable by adding a wait for the element to become not present ("stale") after sending the keys:
element(by.model('Contact.email')).sendKeys('#gmail.com');
var EC = protractor.ExpectedConditions;
browser.wait(EC.stalenessOf(elem), 5000);
expect(elem.isPresent()).toBe(false);

Resources