How Cypress test HTML element is assigned after Ajax call - cypress

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

Related

Cypress E2E testing: How to test the text message on toast container (or) popup on a web page?

screenshot of html code for the error toast popupI am trying to get the element of popup (or) toast-container and asserting the text, but I am getting an error that element never found. Please someone help?
describe('Wholesoft Login Page', function(){
it('Check Login popup', function(){
cy.visit('https://https://platform.wholesoftmarket.com/login')
cy.get('#email').type('kj#gmail.com')
cy.get('#password').type('hello')
cy.get('button.btn.active').click()
cy.get('div').within(($div)=>{
cy.get('div.overlay-container').should('have.text','no record found')
})
})
})
You need to set a bigger timeout for the element, waiting to be present in the DOM:
cy.get('div[aria-label="Error"]', {timeout: 10000}).should('have.text','no record found')
// maybe you can use the class selector on that div (div.toast-title.ng-star-inserted)
// default timeout is 6000
// increase it until it is caught by Cypress
You need to wait till the toast or pop-up element is visible to access it for further steps.
Please make sure element selector is right.
following cmd
cy.get('div.overlay-container').should('be.visible').should('have.text','no record found')

Cypress identify a document element inside DOM

I have an application that has a document section showing some notes and I am trying to identify and verify the text ("Release Notes") with Cypress. But all my locating strategies are failing and need some help. Please see if you can recommend some other way to locate this.
DOM:
Below is my code:
cy.get('.release-notes')
.should('be.visible')
.then((text => {
cy.log(text.text()) // blank
cy.log(text) // <span.release-notes>
}))
cy.get('[data="release-notes.html"]').should('be.visible').then((text => {
cy.log(text.text()) // blank
cy.log(text) // <object.release-notes-object>
}))
//Below lines - Fails
//cy.get('h1:contains("Release Notes")') -- Timed out retrying after 4000ms: Expected to find element: h1:contains("Release Notes"), but never found it.
//cy.get('body').should('contains', "Release Notes"); --Timed out retrying after 4000ms: object tested must be an array, a map, an object, a set, a string, or a weakset, but object given
Note: I have "includeShadowDom": true inside my cypress.json
You may be getting blocked by the #document. It does not look like an iframe, but may behave like one.
Try
cy.get('object.release-notes-object')
.its('0.contentDocument').should('exist')
.its('body').should('not.be.undefined').within(() => {
cy.get('h1').should('contain', 'Release Notes')
})
Also turn off chromeWebSecurity in case there's a cross-domain issue
cypress.json
{
"chromeWebSecurity": false
}
Debugging the inner document
Since you can get into the inner document body, in theory you should now be able to query it.
Take a look at the DOM inside body from Cypress' perspective, experiment with timing - the release notes likely to be fetched from a server
cy.get('object.release-notes-object')
.its('0.contentDocument').should('exist')
.its('body').should('not.be.undefined')
.then($body => {
cy.wait(10000) // just for debugging, explicit wait here
.then(() => console.log($body[0].innerHTML)) // inspect the elements inside after wait
})
Another way, add a .should() to retry
cy.wait(10000) // for debugging only, waiting for fetch
cy.get('object.release-notes-object')
.its('0.contentDocument').should('exist')
.its('body', {timeout: 10000}) //vary timeout here
.should('not.be.undefined')
.should('have.descendants', 'h1') // retries the body fetch until true
The difference between the last and adding a timeout to cy.get('h1') is that .should('have.descendants', 'h1') will re-query the body during each retry.
It's possible that there is an empty body element before the release notes are fetched, and it is replaced when the release notes are added.
Before the "includeShadowDom": true global flag we have to do something like this:
cy.get('selector').shadow().find('selector')
Now may be you can omit the .shadow() part and try
cy.get('[app-name="Voyage Planning"]').find('h1:contains("Release Notes")')
You have attributes data-gr-ext-installed and data-new-gr-c-s-check-loaded on the body. These look like they mark the end of something loading.
Add a check for that in #Mihi's solution (which must be nearly correct).
cy.get('object.release-notes-object')
.its('0.contentDocument').should('exist')
.its('body')
.should('not.be.undefined')
.should('have.attr', 'data-gr-ext-installed') // or data-new-gr-c-s-check-loaded
.within(() => {
cy.get('h1').should('contain', 'Release Notes')
})

Cypress. cy.type({enter} fails intermittently

I am running e2e tests on a web app using Cypress.
This section fails intermittently.
Here, search text is entered in the appropriate field. When {enter} is clicked, the search happens & the dialog only shows the search results.
Here is the code:
function selectDesign(designName: string, designCode: string) {
// enter search text & click enter
cy.get(singlePickerSearch).type(designName + '{enter}', { force: true });
// select your design from search results
cy.get(designPickerDesign(designCode))
.scrollIntoView()
.should('have.length', 1)
.click({ force: true });
}
When it fails, it is like '{enter}' was not executed. I can click enter manually, and it works.
Because it is an intermittent failure, it is difficult to obtain logs.
Every single test uses the same design. In a single run there are 19 tests. The test runner can find the file 18 times & then fail on 1 test.
Also tried using backticks in the .type() command:
cy.get(singlePickerSearch)
.clear()
.type(`${designName}{enter}`);
There was no appreciable improvement.
I finally got this working by using the following:
function selectDesign(e2eDesign: any) {
// open dodi picker
cy.get(createCardQueryOpenDodiPicker)
.should('be.visible')
.click()
.then(() => {
// wait for loader
cy.get(loader).should('not.be.visible');
// enter search text
cy.get(dodiPickerSearchText)
.clear()
.type(`${e2eDesign.design.name}{enter}`)
.then(() => {
// wait for loader to go
cy.get(loader).should('not.be.visible');
// short wait to avoid intermittent failure here
cy.wait(400);
// select required design
cy.get(dodiPickerItems)
.should('have.length', 1) // should only be one match
.click();
});
});
}
I think the last .should('have.length' 1) causes it to wait until that is true before trying to execute the next statement.

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

how does Jasmine 'expect' waits for a protractor promise to resolve

I'm working on e2e testing.
I have a confirm pop which doesnt exist on the page till I click a button.
Once the confirm popup is create I get the text from an element there.
After that I click on OK button which causes the confirm popup to be delete from the DOM and also add a new element to the DOM with the value i got the text earlier.
the problem is, because getText() returns a promise, by the time I do the comparison the first element is not present on the screen and the test fails.
if I do expect while the confirm popup on the screen I can see the text of the confirm popup element.
how does Jasmine expect() resolve the promise?
thanks in advance
Something like this?
element(by.id('dangerous-activity')).click().then(function () {
element(by.id('confirmation-text')).getText().then(function (textToConfirm) {
element(by.id('confirm-button')).click().then(function () {
element(by.id('new-element')).getText().then(function (newText)) {
expect(newText).toBe(textToConfirm);
});
});
});
});
Here all promises are explicitly resolved, so Jasmine does not need to resolve any promise anymore.
You can let expect resolve the new-element promise, replacing the last two lines by:
....
expect(element(by.id('new-element')).getText()).toBe(textToConfirm);
....
But you cannot get the textToConfirm in the same expectation, since it is gone by then as you indicated.
This should be the simplest way to do what you want:
$('#open-popup').click();
var textToConfirm = $('#popup-text').getText();
$('#confirm-button').click();
var newText = $('#new-element').getText();
expect(newText).toBe(textToConfirm);
Note this will not work:
$('#open-popup').click();
var textToConfirmElement = $('#popup-text');
$('#confirm-button').click();
var newText = $('#new-element').getText();
expect(newText).toBe(textToConfirmElement.getText());
because here you get the text after the popup is already closed.

Resources