Cypress wait for API after button click - javascript

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

Related

Cypress - wait for the API response and verify UI changes

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.

Cannot get HTTP call response while debugging but it exists on the browser

I simply send an HTTP request to create a record and then get the added record id on the Network section of browsers that I tried Chrome, Firefox, but interestingly, I cannot get the same id value while debugging. Here is the code and the breakpoint that I am trying to get the response. I also tried to use promise, etc, but while it is working on other PC, I cannot get it on my PC (I also close antivirus app, etc. and clean browser histor, restart PC, restart the app, etc. But none of them make any sense.
this.myservice.create(<Employee>{ name: data.name, surname: data.surname })
.pipe(
tap((result) => {
// --> I am expecting to get result as id value as it
// is seen on the Network tab of Google Developer tool
...
Note that my service returns an observable and there is no problem at that sied. The problem is most probably related to browser or pc settings e.g. firewall or etc.
It might depend on the implementation of this.myservice.create. If that is implemented eager instead of lazy that will mean that your http request is already invoked before you subscribe, effectively already giving you the http response in the network inspector. But it will only get into the tap() as an emission once you subscribe() to your Observable which makes the whole stream active.
Example 'lazy' implementation:
create() {
return Observable.Create((obs) => {
fetch('http://example.com/movies.json')
.then(response => {
obs.onNext(response.json());
obs.onCompleted();
});
});
}
This will invoke the fetch only once the observable is subscribed to.
Example of an eager implementation:
create(){
const obs = new Rx.Subject();
fetch('http://example.com/movies.json')
.then(response => {
obs.next(response.json());
obs.complete();
});
return obs;
}
This will start the fetch even though the observable is not subscribed to already. It might even lead to a race condition (the value is already pushed before being subscribed to).
Having 'eager' observables is in my opinion often a mistake // anti-pattern because it can lead to very confusing results unless the engineers know intimately how the system works and it is documented throughly. And then still....

Testing Stripe Checkout w/ Cypress

I have to call stripe.redirectToCheckout (https://stripe.com/docs/js/checkout/redirect_to_checkout) in js to take a customer to their stripe checkout page.
I want to use cypress to test the checkout process, but it is not able to handle the stripe redirect as the cypress frame is lost when stripe.redirectToCheckout navigates to the page on Stripe's domain.
I also want to test that Stripe redirects us back to the success or error URL.
Is there any way to force cypress to "reattach" to the page once we've navigated to the Stripe checkout
-or-
Is there any way to get the URL for the Stripe checkout page so we can redirect manually or just know that it was at least called with the right parameters.
I know that testing external sites is considered an "anti-pattern" by the people at cypress (https://github.com/cypress-io/cypress/issues/1496). But how can a very standard web process, checkout, be tested (with a very popular and standard payment service, I will add) in that case? I don't buy that this is an "anti-pattern". This is an important step of the end-to-end test, and Stripe specifically gives us a testing sandbox for this kind of thing.
One common way to e2e test application with external dependencies like stripe is to make a simple mock version of it, that is then applied in e2e testing. The mock can also be applied during development, to speed things up.
Would this help?
it(`check stripe redirection`, () => {
cy.get('#payButton').click();
cy.location('host', { timeout: 20 * 1000 }).should('eq', STRIPE_REDIRECT_URL);
// do some payment stuff here
// ...
// after paying return back to local
cy.location({ timeout: 20 * 1000 }).should((location) => {
expect(location.host).to.eq('localhost:8080')
expect(location.pathname).to.eq('/')
})
})
I've used this method to test keyCloak login. This is the actual code that worked.
describe('authentication', () => {
beforeEach(() => cy.kcLogout());
it('should login with correct credentials', () => {
cy.visit('/');
cy.location('host', { timeout: 20 * 1000 }).should('eq', 'keycloak.dev.mysite.com:8443');
// This happens on https://keycloak.dev.mysite.com:8443/auth/dealm/...
cy.fixture('userCredentials').then((user) => {
cy.get('#username').type(user.email);
cy.get('#password').type(user.password);
cy.get('#kc-login').click();
cy.location({ timeout: 20 * 1000 }).should((location) => {
expect(location.host).to.eq('localhost:8080')
expect(location.pathname).to.eq('/')
})
})
});
I'm giving it a try to use stripe elements instead since it does not redirect and gives me much more control over it.

Second test doesnt change URL

I have two test. The first test passes successfully. Then there is an url method call in the second test, but it doesn't change the url in the browser.
The baseUrl in wdio.conf.js is set to http://localhost/web/es/index.html#
Tests:
var assert = require('assert');
describe('user login ', function(){
it('user login', function(){
browser
.url('/system/login')
.setValue('[name="username"]','test')
.setValue('[name="password"]','test')
.click('=Potvrď');
assert(browser.waitUntil('=test test'));
});
it('user form', function(){
browser
.url('/user/form');
});
});
In the first test /system/login is opened correctly. But in the second test the url never changes to /user/form
I'm just starting with webdriverio so am i missing something ?
A lot of time passed since #user49126 asked the question, but since no one actually saw that this is in fact a routing issue, here is my answer:
It looks like you are using a bad baseUrl value. You have to go back and re-read it's description in the wdio.config.js file. Any value you pass to the browser.url() function will be postpended to your baseURL.
In your case, firstly you are going to http://localhost/web/es/index.html# + /system/login, which due to the # and probably your routing logic behind, evaluates correctly to a valid route. The second test most surely takes you to an invalid route.
In order to completely isolate your issue we will need the console output.
Here is the setup I used to debug this:
wdio.config.js:
// Set a base URL in order to shorten url command calls. If your url parameter starts
// with "/", then the base url gets PREPENDED.
baseUrl: ' http://127.0.0.1:8303',
Code:
describe("\nDebug routing issue", function() {
it("\nShould verify the URL, click something, move on\n", function() {
return browser
.url('/product-page.html')
.getUrl()
.then( function(retURL) {
assert.include(retURL, "product-page", "Yeap! You're good!");
})
.waitUntil(function() {
return browser.isVisible("div.top-bar-right a");
}, 3000, "\nLogin failed...")
.click('div.top-bar-right a')
// Casual 'pause' to witness the click
.pause('1000');
});
it("\nShould reload different URL and validate site routing\n", function() {
return browser
.url('/index.html')
.getUrl()
.then( function(retURL) {
assert.include(retURL, "index", "Yeap! You're good!");
})
// Casual 'pause' to witness the new route
.pause('1000')
});
TL;DR: Just make sure you are have the correct value in the baseUrl variable. In your case, change the baseUrl value to http://localhost/.
My suggestion is to also use the port, e.g. http://localhost:8303/, or the IP of your localhost server altogether, as I also experienced some issues using it as simply localhost.
PS: None of the other answers touch upon the real issue the user is experiencing. Unfortunately I have no way to comment/signal this. ('Not enough minera... errrg, rep points,' #feelsbadman)
Check the condition on browser.waitUntil(...). The first parameter is a function according to http://webdriver.io/api/utility/waitUntil.html.
Also, adding a timeout (3seconds) will help to see if the wait has been successful.
browser.waitUntil(function() {
return browser.getText('#some_id') === 'test test';
}, 3000, "Timeout!!");
That is because it's not really a test. What are you testing, where is your acceptance criteria? You told the browser to do something and didn't wait to check up on it. It's basically like you said: Selenium, do this and I don't care I am done, bye.
What you can use?
Try giving it a pause:
.pause(ms);
give it 10000 (10s) and visually see if it changes.
you can also add an additional:
.getUrl() - to get the URL after the time stated under ms.
Alternatively (instead of the things stated above):
On your new URL, is there an element in DOM that is not present in the DOM on old URL? If so, add to the end:
.waitForExist(selector, ms);
give it 1000 under ms, and make sure you got the right selector for that element
This way browser will wait for that element to appear.

Backbone fetch async timeout not taking effect

I am trying to do a fetch on a large backbone collection that involves some server side processing.
I tried setting timeout to 0 (for a infinite timeout) or to a large value:
aCollection.fetch({
// ...
timeout: 500000
});
// or:
aCollection.fetch({
// ...
timeout: 0
});
…but neither of them are taking effect; the GET query involved during the fetch operation times-out every 2 minutes.
Is this a browser timeout over-riding the fetch async options? is there any work around for this?
I got this problem too and it was due to node default timeout (I was using expressjs).
It's probably not the best solution but I changed the server timeout.
this.server.timeout = 240000; (4minutes)

Categories

Resources