Cypress loop only visits the first item in the list - cypress

I'm having trouble recreating this test.
Problem:
It seems that Cypress is only visiting one link from this list whilst looping over each item.
Notes:
I added the length check to make sure that the array of nodes is the correct size
The code within each seems to work fine, since the test runner navigates to the first link
I was looking into the .next() methods, but that returns the next DOM nodes. Still not clear if that might be the issues here
seems like there's no iterator within the each() method
Test Case
GIVEN a personal website
WHEN when I navigate to the /blog page
THEN Cypress find the list of blog posts
AND checks the number of total posts
THEN Cypress loops over those list items
THEN Cypress collects the href
THEN Cypress visits that page
THEN Cypress checks that href includes 'posts'
AND wait 1s
Test Code
describe("Visual regression on /posts/{id}", () => {
sizes.forEach((size) => {
it(`Should match screenshot, when '${size}' resolution'`, () => {
cy.visit("/blog")
cy.get("ul > li > a")
.should("have.length", 9)
.each((element) => {
cy.wrap(element)
.invoke("attr", "href")
.then((href) => {
cy.visit(href);
});
cy.wrap(element)
.should("have.attr", "href")
.and("include", "posts"
cy.wait(1000);
});
});
});
});

Solution
cy.visit("/blog");
cy.get("ul > li > a").should("have.length", 9);
cy.get("ul > li > a").each(($element) => {
cy.wrap($element)
.invoke("attr", "href")
.then((href) => {
cy.wrap(href).should("exist")
cy.visit(href);
cy.wait(2000);
});
});
I seems like the block below is letting Cypress loose track of the current element.
.should("have.length", 9)
Once I split out the rules of the test, I'm no longer seeing any issues and Cypress is correctly navigating through all the pages I need.

Related

How Cypress test HTML element is assigned after Ajax call

I have a search input and result display area that are being handled by Ajax call. When user enter a keyword, Ajax call the backend and return HTML string. The JS handler then appends the response HTML into result display area.
Here my steps:
Search input: Vancouver (auto input by browser location)
Result: Welcome to Vancouver
Type into search input: Calgary
Expected: Welcome to Calgary
// Test case
Cypress.Commands.add('assessHeadingInfo', (options) => {
cy.fixture('selectors/index-page').then((selectors) => {
cy.xpath(selectors.head_info).then((heading) => {
cy.searchForArea('Calgary'); // Steps to input keyword and click search
cy.get(heading)
.children('h1')
.should('be.visible');
cy.get(heading)
.children('h1')
.invoke('text')
.should('equals', 'Welcome to Calgary');
});
});
});
Test error:
AssertionError
Timed out retrying after 4000ms: expected 'Welcome to Vancouver' to equal 'Welcome to Calgary'
However, the visual screenshot showed that the 'Welcome to Calgary' text was displayed while the test could not see.
I did follow the guide from Cypress app
Timed out retrying after 4000ms: cy.should() failed because this element is detached from the DOM.
...
You typically need to re-query for the element or add 'guards' which delay Cypress from running new commands. Learn more
I added 'a guard', cy.wait()...but nothing works.
Would you please teach me how to handle this issue?
Thank you.
UPDATE SOLUTION:
Thanks for #jhelguero helped me to find out.
Solution 1:
I added this and it worked well.
cy.contains('h1', 'Welcome to Calgary')
.should('be.visible');
Because I have put all the tests in block of
cy.xpath(selectors.head_info).then((heading) => {
...
});
So, when I do cy.get(heading) it caught the element before Ajax done. It was 'Welcome to Vancouver' instead.
Solution 2:
Then, I separated test steps like this.
cy.searchForArea('Calgary');
cy.xpath(selectors.head_info)
.children('h1')
.should('have.text', 'Welcome to Calgary');
and it was successful.
Summary:
Do this:
cy.xpath({selector}).should(have.text,'ABC');
cy.xpath({selector}).find('h1').should('have.text, '123');
DON'T do this:
cy.xpath({selector}).then((ele) => {
cy.get(ele).should(have.text, 'ABC');
cy.get(ele).find('h1').should('have.text, '123');
});
It looks like your DOM rerenders after the ajax call and thus the element('Welcome to') you were previously reference has been detached.
To fix this you will need to re query using the same selector after the ajax call is completed. You will not need cy.xpath(selectors.head_info).then().
Cypress.Commands.add('assessHeadingInfo', (options) => {
cy.fixture('selectors/index-page').then((selectors) => {
cy.searchForArea('Calgary'); // Steps to input keyword and click search
// use .contains() to search a selector with regex text
cy.contains(selectors.head_info, /Welcome to Calgary/)
.should('be.visible');
});
});
If you visually confirmed the text "Welcome to Calgary", the problem might be that .should('equals', 'Welcome to Calgary') is not retrying enough of preceding steps.
Try shortening the query.
You can also increase the timeout if you notice a lag before the text appears.
cy.get(`${heading} > h1`, {timeout:10_000})
.should('have.text', 'Welcome to Calgary')

How to Skip a Test if an element is not present in Cypress

I am writing a test in which if I land on a page and if any records are available, I need to click on three dots buttons near the record. But I should skip the test if no records are available on the page.
cy.get('body')
.then(($body) => {
if ($body.find('.ant-empty-description').length) {
cy.log('Element not found. Skip the Test')
}
else {
cy.xpath("//tbody[#class='ant-table-tbody']//tr[" + rowNumber + "]//td[4]//button//em").click()
}
})
I am using an approach in which if 'No Record found' message is present, I need to skip the test else click on the button present near the record.
Sometimes it's necessary to test conditionally, but using <body> as a base element is a mistake IMO.
<body> is always on the page, but the table data may not be if fetched from an API.
The test will always run faster than the API, always see the empty row placeholder.
Pattern to use
Add an intercept() and wait for the API to respond.
Use the table rows as base element for conditional check - there will always be at least one row (but it may be the "No records" row).
cy.intercept(...).as('tableData')
...
cy.wait('#tableData')
cy.get('tbody tr').then($rows => { // may be only one "No record" row
const noData = $rows.text().includes('No Record found')
if (!noData) {
$rows.eq(rowNumber).find('td').eq(4]).find('button').click()
}
})
You can use mocha's .skip() functionality. Please note that you'll have to use function() instead of arrow functions.
it('test', function() {
cy.get('body')
.then(function($body) {
if ($body.find('.ant-empty-description').length) {
cy.log('Element not found. Skip the Test')
this.skip()
} else {
cy.xpath("//tbody[#class='ant-table-tbody']//tr[" + rowNumber + "]//td[4]//button//em").click()
}
})
})
That being said, I agree with #jjhelguero -- using skips in this way is an anti-pattern for testing. Ideally, you should control whether or not an element will appear on the webpage, and use your test setup to manipulate the page into having/not having the element.

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

Iterate through elements

I have a page with a few Results panels, each panel has its own delete button.
I wrote a Cypress test to test the delete process, the test works as expected, the panel gets deleted:
cy.get('div[data-test="Results"]')
.first()
.within(() => {
cy.get('p[data-test="Contact ID"]').then($match => {
contactID = $match.html();
cy.get('button[data-test="Delete Contact"]')
.click()
.get('div[data-test="Delete Record Modal"]')
.should('be.visible')
.get('button[data-test="Confirm Deletion"]')
.click();
});
});
Next I'm trying to detect if the correct panel got deleted.
How can I iterate through all the <p />s of all panels and make sure none of them has a contactID equal to the one that was deleted?
I tried this:
cy.get('p[data-test="ContactID"]').then($match2 => {
expect($match2.text()).not.to.eq(contactID);
});
But in $match2 I get all contacts ids all together for example: 12345678 instead of 1234 and 5678
You can use each:
cy.get('p[data-test="ContactID"]').each(($match) => {
cy.wrap($match).invoke('text').should('not.eq', contactID)
})
invoke calls a function on the subject, in this case, .text()
the chained .should makes an assertion on that text
this will retry the assertion until it passes or times out (see retry-ability) due to the cy.wrap

cypress.io and hidden element not present i DOM

I need to check if a text ("Skadesaken min") is present on the next page I am navigating to using this code:
describe('Folg skade Test', function() {
it('Enter the app', function() {
cy.visit('http://localhost:3000/')
})
it('Select claim', () => {
cy.get('#app > section > article:nth-child(3) > a:nth-child(2)').click()
.next().should('contain', 'Skadesaken min>')
})
})
Using the selector when inspecting the element in cypress developer tool I get this:
#app > section.col-md-9 > article > h1.hidden-xs
However the error when replaying the script says that the element is
"cy.next() failed because this element is detached from the DOM."
any idea how to solve this?
Sounds like the original element got removed from the dom and a new one went in its place
You can get around this easily.
const selector = '#app > section > article:nth-child(3) > a:nth-child(2)';
cy.get(selector).click();
cy.get(selector).should('contain, 'Skadesaken min>')

Resources