Cypress - wait for the API response and verify UI changes - javascript

I have a component that I want to cover with some e2e tests. This component takes the URL provided by the user in the input, calls the API after the button click and then returns the shortened version of that URL. After that, shortened url is added to the list below the input on the UI and makes some localStorage assertion. I want Cypress to wait for the API response and only then check the UI if the list item was added. I made this working but I hardcoded the wait time in the wait() method. How Can I achieve that programatically ?
describe("Shortener component", () => {
it("Should add the list item and data to localStorage", () => {
cy.visit("http://127.0.0.1:5500"); //Live server extension address
cy.get("#url-input").type("https://facebook.com");
cy.get("#form-submit-button").click();
// wait for the api response and make sure that the value has been added to the localStorage
cy.wait(40000); //todo - wait for the api response instead of hardcoding the wait time
const localStorageData = localStorage.getItem("linksData");
if (JSON.parse(localStorageData)) {
expect(JSON.parse(localStorageData)[0].inputValue).to.eq(
"https://facebook.com"
);
}
// check if the new list item with the corrct value has been addded
cy.get(".shortener-component__list-item")
.contains("https://facebook.com")
.should("be.visible");
//validation mesasge should not be visible
cy.get("#validationMesage")
.contains("Please add a valid link")
.should("not.be.visible");
});
});
I tried with intercept() however I failed. Not sure how to make it working. I also saw some similar SE topics on that but it did not help me.
Any ideas / examples apreciated :)
Thx !

From the order of events you've given
short URL returned
added to localStorage
added to list
just change the order of feature testing
test list - it is last event, but has retriable commands (you can increase the timeout)
now test localStorage, if UI has the short URL so will localStorage
cy.contains('.shortener-component__list-item', 'https://facebook.com', { timeout: 40000 })
.then(() => {
// nested inside .then() so that the above passes first
const localStorageData = localStorage.getItem("linksData");
const linksData = JSON.parse(localStorageData);
expect(linksData).not.to.eq(undefined);
expect(linksData[0].inputValue).to.eq("https://facebook.com");
})
Alternatively, to make use of retry and timeout on the localStorage check,
cy.wrap(localStorage.getItem("linksData"))
.should('not.eq', undefined, { timeout: 40000 }) // will retry the above until true
.then(data => JSON.parse(data))
.should(parsedData => {
expect(parsedData.inputValue).to.eq("https://facebook.com");
});
I guess you should also start the test with
cy.clearLocalStorage("linksData")

There're examples in the documentation, it only takes some reading and experimentation.
In general, you need three commands: cy.intercept(), .as(), and cy.wait():
cy.intercept(your_url).as('getShortenedUrl');
cy.wait('#getShortenedUrl');
you can also use .then() to access the interception object, e.g. a response:
cy.wait('#getShortenedUrl').then(interception => { });
or you can check something in the response using .its():
cy.wait('#getShortenedUrl').its('response.statusCode').should('eq', 200);
The point is that after cy.wait('#getShortenedUrl'), the response has been received.

Related

How to capture the 'latest' instance of a request if it is called multiple times during a Cypress test?

In my Cypress Cucumber test, here is my current feature file scenario (this scenario fails on the final step):
Given I log in
Then I am on the dashboard
And the dashboard displays 0 peers (#Peers is called here, & 0 is the correct value here)
When I go to User 1 page
And I click the Add Peer button (#AddPeer request is successful here)
And I go to the dashboard
Then the dashboard displays 1 peers (#Peers is called here. The actual value is 0, but I am expecting 1. It looks like the code is using the response body from the 1st `Peers` intercept)
Here are my step definitions:
Given('I log in', () => {
cy.intercept('GET', `**/user/peers`).as('Peers')
cy.intercept('POST', `**/Peers/add`).as('AddPeer')
});
Then('I am on the dashboard', () => {
cy.url().should('include', `dashboard`)
})
Then('the dashboard displays {int} peers', expectedPeersCount => {
cy.wait('#Peers').then(xhr => {
const peers = xhr.response.body
expect(peers.length).to.eq(expectedPeersCount)
});
});
When('I click the Add Peer button', () => {
dashboard.btnAddPeer().click()
cy.wait('#AddPeer').then(xhr => {
expect(xhr.response.statusCode).to.eq(200)
})
})
When('I go to the dashboard', () => {
cy.visit('/dashboard');
});
In the backend, #AddPeers() adds a peer to a list, while #Peers() returns a list of my peers.
When I go to the dashboard, #Peers() returns the latest list of peers.
But for some reason, the above code is still using the 'old' response that has an empty response body.
Can someone please point out how I can get the 'latest' #Peers response?
Here is the 1st Peers response that is empty:
And here is the 2nd Peers response that contains 1 array item:
Attempted fix:
Given('I log in', () => {
cy.intercept('GET', `**/user/peers`).as('Peers0')
cy.intercept('GET', `**/user/peers`).as('Peers1')
});
Then('the dashboard displays {int} peers', expectedPeersCount => {
cy.wait(`#Peers${expectedPeersCount }`).then(xhr => {
const peers = xhr.response.body
expect(peers.length).to.eq(expectedPeersCount)
});
});
Cypress logs:
Peers1 looks like it's empty below, but it shouldnt' be:
And then it looks below like Peers0 has the populated array. Note the Matched cy.intercepts()
I can't see why the original code doesn't work, it looks perfectly good (without a running system to play with).
But the "fix" variation is backwards - the last intercept that is set up is the first to match.
From the docs:
This diagram shows route5 being the first (non-middleware) route checked, followed by route3, then route1.
Also, since each intercept is intended to catch only one call, add { times: 1 }, to make it so.
Given('I log in', () => {
cy.intercept('GET', `**/user/peers`, {times: 1})
.as('Peers1') // catches 2nd call
cy.intercept('GET', `**/user/peers`, {times: 1})
.as('Peers0') // catches 1st call
})
You can see it in this screenshot
where Peers1 is matched first then Peers0, but the following block is expecting the reverse order
Then('the dashboard displays {int} peers', expectedPeersCount => {
cy.wait(`#Peers${expectedPeersCount }`).then(xhr => {
const peers = xhr.response.body
expect(peers.length).to.eq(expectedPeersCount)
});
})
Called with 0 by And the dashboard displays 0 peers and then called with 1 by Then the dashboard displays 1 peers
Dynamic aliases
You might be able to set the alias dynamically depending on how many peers are in the response.
Given('I log in', () => {
cy.intercept('GET', `**/user/peers`, (req) => {
req.continue().then(res => {
if (re.body.length === 0) {
req.alias = 'Peers0'
}
if (re.body.length === 1) {
req.alias = 'Peers1'
}
})
})
})
If this works, then you don't need to worry about the order of setup.
A bit of misunderstanding here. cy.wait() for an alias intercept will only wait for the first requesting matching the given params.
cy.intercept('request').as('call')
// some actions to trigger request
cy.wait('#call')
// later in the test trigger same request
// this will refer to the first request made
// by the app earlier in the test
cy.wait('#call')
You can either make another intercept with a unique alias and then use .wait() on the new unique alias.
cy.intercept('request').as('call')
// some actions to trigger request
cy.wait('#call')
// later in the test trigger same request
cy.intercept('request').as('newCall')
// some actions to trigger same request
cy.wait('#newCall')
or you can use your same approach and use cy.get() to get the list of matching requests, however, this may be a good choice as cy.get() will not wait for your new request to complete.
cy.intercept('request').as('call')
// some actions to trigger request
cy.wait('#call')
// later in the test trigger same request
// have to ensure the request is completed by this point
// to get the list of all matching and hopefully
// the last request will be the one you seek
cy.get('#call.all')
// get last matching request
.invoke('at', -1)

Download two URLs at once but process as soon as one completes

I have two API urls to hit. One known to be fast (~50-100ms). One known to be slow (~1s). I use the results of these to display product choices to the user. Currently I await-download one, then do the second. Pretty synchronous and because of that it's adding 50-100ms to the already-slow second hit.
I would like to:
Send both requests at once
Start processing data as soon as one request comes back
Wait for both requests before moving on from there.
I've seen the example Axios give...
axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread(function (acct, perms) {
// Both requests are now complete
}));
But this appears to wait for both URLs to commit. This would still be marginally faster but I want the data from my 50ms API hit to start showing as soon as it's ready.
For sure you can chain additional .thens to the promises returned by axios:
Promise.all([
getUserAccount()
.then(processAccount),
getUserPermissions()
.then(processPermissions)
]).then(([userAccount, permissions]) => {
//...
});
wereas processAccount and processPermissions are functions that take the axios response object as an argument and return the wanted results.
For sure you can also add multiple .thens to the same promise:
const account = getUserAccount();
const permissions = getUserPermissions();
// Show permissions when ready
permissions.then(processPermissions);
Promise.all([account, permissions])
.then(([account, permissions]) => {
// Do stuff when both are ready
});
I replaced axios.all with Promise.all - I don't know why axios provides that helper as JS has a native implementation for that. I tried consulting the docs ... but they are not even documenting that API.

Cypress wait for API after button click

I've made a React app, which all works perfectly and I'm now writing some end to end tests using Cypress.
The React app all works on the same url, it's not got any routes, and api calls from inside the app are handled through button clicks.
The basis of the app is the end user selects some options, then presses filter to view some graphs that are dependant on the selected options.
cy.get('button').contains('Filter').click()
When the button is pressed in cypress, it runs the 3 api calls which return as expected, but looking over the cypress docs there is no easy way unless I use inline cy.wait(15000) which isn't ideal, as sometimes they return a lot faster, and sometimes they return slower, depending on the selected options.
Edit 1
I've tried using server and route:
cy.server({ method: 'GET' });
cy.route('/endpoint1*').as('one')
cy.route('/endpoint2*').as('two')
cy.route('/endpoint3*').as('three')
cy.get('button').contains('Filter').click()
cy.wait(['#one', '#two', '#three'], { responseTimeout: 15000 })
Which gives me the error:
CypressError: Timed out retrying: cy.wait() timed out waiting 5000ms for the 1st request to the route: 'one'. No request ever occurred.
After further investigation
Changing from responseTimeout to just timeout fixed the error.
cy.server({ method: 'GET' });
cy.route('/endpoint1*').as('one')
cy.route('/endpoint2*').as('two')
cy.route('/endpoint3*').as('three')
cy.get('button').contains('Filter').click()
cy.wait(['#one', '#two', '#three'], { timeout: 15000 }).then(xhr => {
// Do what you want with the xhr object
})
Sounds like you'll want to wait for the routes. Something like this:
cy.server();
cy.route('GET', '/api/route1').as('route1');
cy.route('GET', '/api/route2').as('route2');
cy.route('GET', '/api/route3').as('route3');
cy.get('button').contains('Filter').click();
// setting timeout because you mentioned it can take up to 15 seconds.
cy.wait(['#route1', '#route2', 'route3'], { responseTimeout: 15000 });
// This won't execute until all three API calls have returned
cy.get('#something').click();
Rather than using a .wait you can use a timeout parameter. That way if it finished faster, you don't have to wait.
cy.get('button').contains('Filter', {timeout: 15000}).click()
This is mentioned as one of the options parameters in the official docs here.
cy.server() and cy.route() are deprecated in Cypress 6.0.0 In a future release, support for cy.server() and cy.route() will be removed. Consider using cy.intercept() instead.
this worked for me...
ex:-
see the screen shot
cy.intercept('GET', 'http://localhost:4001/meta').as('route');
cy.get(':nth-child(2) > .nav-link').click();
cy.contains('Filter');
cy.wait(['#route'], { responseTimeout: 15000 });
Try this:
cy.contains('button', 'Save').click();
cy.get('[data-ng-show="user.manage"]', { timeout: 10000 }).should('be.visible').then(() => {
`cy.get('[data-ng-show="user.manage"]').click();
})

How to add recursive function checking xhr response in testcafe script?

I'm trying to write a test download works, which requires to check if xhr response has status READY. I created a client function in TestCafe, using promises, but it's failing in case of recursion.
How should I fix my code to handle this situation?
P.S. many apologies for newbies questions, I have just started my journey in automation testing.
fixture`Download report works`
test
.requestHooks(logger)//connected a request hook, will wait for logger request
('I should be able to download PDF report from header of the page', async t => {
//recursively check if response status is READY, and then go to assertions
const waitForDownloadResponseStatus = ClientFunction((log) => {
return new Promise((resolve,rejects)=>{
const waitForStatus=()=>{
const arrayFromResponse = JSON.parse(log.response.body);
const responseStatus = arrayFromResponse.status;
if (responseStatus == 'READY')
{
resolve(responseStatus);
}
else {
waitForStatus();
}
}
waitForStatus();
})
});
//page objects
const reportTableRaw = Selector('div.contentcontainer').find('a').withText('April 2019').nth(0);
const downloadPdfButton = Selector('a.sr-button.sr-methodbutton.btn-export').withText('PDF');
//actions.
await t
.navigateTo(url)
.useRole(admin)
.click(reportTableRaw)//went to customise your report layout
.click(downloadPdfButton)
.expect(logger.contains(record => record.response.statusCode === 200))
.ok();//checked if there is something in logger
const logResponse = logger.requests[0];
// const arrayFromResponse = JSON.parse(logResponse.response.body);
// const responseStatus = arrayFromResponse.status;
console.log(logger.requests);
await waitForDownloadResponseStatus(logResponse).then((resp)=>{
console.log(resp);
t.expect(resp).eql('READY');
});
});
When you pass an object as an argument or a dependency to a client function, it will receive a copy of the passed object. Thus it won't be able to detect any changes made by external code. In this particular case, the waitForStatus function won't reach its termination condition because it can't detect changes in the log object made by an external request hook. It means that this function will run indefinitely until it consumes the all available stack memory. After that, it will fail with a stack overflow error.
To avoid this situation, you can check out that response has status READY if you change the predicate argument of the contains function.
Take a look at the following code:
.expect(logger.contains(record => record.response.statusCode === 200 &&
JSON.parse(record.response.body).status === 'READY'))
.ok({ timeout: 5000 });
Also, you can use the timeout option. It's the time (in milliseconds) an assertion can take to pass before the test fails.

Typescript Angular HTTP request get only last result

What is the "best" (common) way to make sure that my Angular HTTP request only returns the newest response data. (I am using Angulars HttpClient)
Lets say the user submits a new request before the response of the previous request is returned. My backend needs more time to process the first request so the second request returns before the first one -> the view get's updated and later the first request returns. Now the view gets updated with old data. Thats obviously not what I want.
I found this solution to cancel the request but is that really the only way? Are there any build in functionalities to achive that?
if ( this.subscription ) {
this.subscription.unsubscribe();
}
this.subscription = this.http.get( 'myAPI' )
.subscribe(resp => {
// handle result ...
});
I know there is the switchMap() operator which only returns the latest data but as far as I know you can only use it inside of an observable. In my case I am not observing any input changes. I simply call a function on form submit via (ngSubmit)="doRequest()" directive in HTML-Template.
// get called on submit form
doRequest() {
this.http.get('myAPI')
.subscribe(resp => {
// handle result ...
});
}
Is there a way to use the switchMap operator or do you have any other solutions? Thanks for taking your time :)
PS: Of course in my real project I have an api service class with different functions doing the requests and just returning http response observables. So I subscribe to such an observable in doRequest() function.
you just make the clicks / requests a stream that you can switch off of... the user's requests / clicks are a stream of events that you can observe and react to.
private requestSource = new Subject();
request() {
this.requestSource.next();
}
ngOnInit() {
this.requestSource.switchMap(req => this.http.get('myApi'))
.subscribe(response => console.log(response, "latest"));
}
modern client programming rule of thumb: everything is a stream, even if it doesn't look like one right away.
Edit: In the case of rxjs 6 or latest version, add pipe operator with the above code.
this.requestSource.pipe(switchMap(...));

Categories

Resources