After reading a lot of docs and try to find a solution to my problem I didn't find anything, so here we go.
I have the following problem testing my end to end flow, the flow I'm testing does launch requests continuously but in one case I expect these requests to stop. In other words, I want to throw an error if the request is made and continue with errors when hits the timeout without any request.
cy.wait('#my-request', { timeout: 20000 })
I expect this to timeout if the app works fine so I tried to do this.
cy.wait('#my-request', { timeout: 20000 })
.its('status').should('not.eq', 404)
.its('status').should('not.eq', 200);
I expected to execute the chained tasks but this only happens when the request is made, as well as tried to use a .then but I have the same problem.
Adding a global on fail event can help us but also limits to not execute additional code when this test is failing and we force it to be marked as done.
In the test definition we can add the done callback like in the example.
it('Description', (done) => {
// other test stuff
cy.on('fail', (err) => {
if (err.name === 'CypressError' && err.message.includes('routeAlias') && err.message.includes('Timed out')) {
done();
return true;
}
throw err;
});
cy.wait('#routeAlias', { timeout: 20000 })
.then(() => {
throw new Error('Error request found.');
});
});
// Any remaining code won't be executed if you need to reset something you need to create a new step, like in my case I did a new step to click a cancel button and prepare the app for the next test.
Now our test passes when this specific error is caught but any other error will lead to a test error.
This kind of workarounds are not recommended by cypress but unless cypress adds a catch to manage some errors it's the only way to fix my problem.
Related
I am using the method described in the answer of this question Log network failures in Cypress to log network failures. I basically intercept every failing request and its response and log it in some array as follows:
cy.intercept('*', (request) => {
request.continue(response => {
if(response.statusMessage !== "OK") {
networkFails.push({request, response})
}
})
})
The tests run perfectly fine, the problem is at the end of the tests I get this error
How do I solve this problem?
I ran into the same issue, and it turns out using request.continue is what was causing it.
request.continue is expecting a response, but when you get a socket hangup (ConnResetException), or in my case the socket being closed before it was finished writing, it triggers a cypress error.
Instead of request.continue, you'll want to use request.on( 'response', response => {
Your full code snippet should look like this:
cy.intercept('*', (request) => {
request.on( 'response', response => {
if(response.statusMessage !== "OK") {
networkFails.push({request, response})
}
})
})
The cypress documentation does a great job of explaining all of the different options here: https://docs.cypress.io/api/commands/intercept#Request-events
For reference, this was the specific cypress error I was getting while using .continue(). The error message was a bit different from yours but essentially the same problem requiring the same solution.
A callback was provided to intercept the upstream response, but a network error occurred while making the request:
Error: Socket closed before finished writing response
We recently installed datadogRUM in our application and now so many DD events kick off in my cypress test that they cause a timeout and failure
I have tried cy.intercept in multiple ways:
cy.intercept('POST', 'https://rum.browser-intake-datadoghq.com/*', {
statusCode: 202,
body: {
},
}).as('datadogRUM');
cy.intercept('POST', 'https://rum-http-intake.logs.datadoghq.com/*', {});
cy.intercept(/\.*datadog.*$/, (req) => {
console.log('DATADOG INTERCEPTED');
req.reply("console.log('datadog intercept');");
});
cy.intercept({
method: 'POST',
url: '/\.*datadog.*$/'
}, req => {
req.destroy();
});
cy.intercept('POST', 'https://rum-http-intake.logs.datadoghq.com/*', { forceNetworkError: true });
just to start. I feel like I've tried every possible variation. I also created a cypress.json file in my /cypress folder
{
"blockHosts": "*datadoghq.com/*"
}
I get hundreds of calls back in my network tab to https://rum.browser-intake-datadoghq.com/api/v2/rum with the preview of console.log('datadog intercept') as I've intercepted them. They all display the solid blue line as if they are being intercepted and blocked. When I set the intercept to an alias I see the alias in my cypress runner window. But there are no 503s or 404s anywhere. The page still fills up with events, cypress gets overloaded, and my test times out.
I even tried copying the data-dog-rum.ts from the src/utils folder to cypress/utils and either commenting out everything or setting the sampleRate to 0, no dice.
EDIT: I am able to get the test passing by adding
Cypress.on('uncaught:exception', () => {
// returning false here prevents Cypress from
// failing the test
return false;
});
to my support/index.js but now whether I add a cy.intercept in my test makes absolutely no difference. The page still fills up with datadog requests regardless, and whether they come back as 200/pending/cancelled, they still delay a single it block in a spec to where it takes 60 seconds to run instead of approx 10 seconds
You can use javascript to perform the stub inside the routeHandler
cy.intercept('*', (req) => { // look at everything
if (req.url.includes('datadoghq')) { // add more conditions if needed
req.reply({}) // prevent request reaching the server
}
})
blockhosts should work with
Pass only the host
{
"blockHosts": "*datadoghq.com"
}
I'm using Cypress 6.0.0 new way of interception.
Waiting on a request
I need to wait for the "templatecontract" response in order to click the #template-button-next because otherwise is disabled. But is trying to click it before getting the response from the API. The documentation seems pretty straight forward.
Am I wrong here?
I have also tried just like:
cy.wait('#templatecontract')
cy.get('#template-button-next').click()
it("Test", function() {
cy.intercept(Cypress.env("baseUrl")+`/api/v1/contract-type/templatecontract`).as('templatecontract')
cy.login(Cypress.env('testUserInviteEmail'), Cypress.env('testUserInvitePassword')).then((token) => {
cy.visit(Cypress.env('baseUrl')+"/templates", {headers: {
Authorization: token,
},
});
cy.get('a[href="/create-template"]').click();
cy.get('.template-usecasetitle').contains('UBO-Formular')
cy.get('button[cy-data="Formular"]').click();
cy.get('#title').type("Title for testing");
cy.get('#usecasetitle').type("Usecasetitle for testing")
cy.get('#description').type("Description just for testing")
cy.wait('#templatecontract').then(interceptions => {
cy.get('#template-button-next').click()
});
});
});
I'm not sure why but just setting the method type (POST in this case) have solved the problem.
cy.intercept('POST', Cypress.env("baseUrl")+`/api/v1/contract-type/templatecontract`).as('templatecontract')
I had a similar problem, and the issue was that the first request my application sent was an OPTIONS request.
If you do not include the method as the first argument, all methods (including OPTIONS) are now matched. This can be puzzling as your .wait will be satisfied by the OPTIONS request, not by your second POST request.
Reference: https://docs.cypress.io/api/commands/intercept.html#Comparison-to-cy-route
Reference: https://docs.cypress.io/api/commands/intercept.html#Matching-URL
Interestingly I am getting different results for
cy.intercept("POST", "https://backend.rocketgraph.app/api/signup").as("doSignup")
and
cy.intercept("POST", `${BACKEND_URL}/signup`).as("doSignup")
Not sure what is the issue. Also have to set POST as one of the users have mentioned
The first one is actually intercepted. Weird but happened.
If you spy a route at the top of your test, as you do here, cy.wait() will return immediately if there have already been responses to that route by the time it's called.
As an example, say you notice this in your network tab:
GET some-route: 200
GET some-route: 200
GET some-route: 200
GET some-route: 200
POST something-unique: 200
GET some-route: 500
^ some-route is 500ing at some clearly-identifiable point. Should be easy to catch in a test, right? Well:
it('Should fail on this 500, but doesn't???', () => {
// start spying our indicator; seems good:
cy.intercept('GET', 'something-unique').as('indicator')
// but if we start spying the route here:
cy.intercept('POST', 'some-route').as('route')
// then hit some-route a bunch of times & return:
foo()
// then expect to catch the failure after something-unique fires:
cy.wait('#indicator').its('response.statusCode').should('eq', 200).then( () => {
// then expect the test to fail on the 500:
cy.wait('#route').its('response.statusCode).should('not.eq', 500)
// ...this won't work! this test will pass because cy.wait() will
// succeed and return the *first* GET some-route: 200 !
})
I personally find this to be pretty counter-intuitive - it feels reasonable that a wait() on a spied route would always actually wait for the next request! - but that's apparently not how it works.
One way around this is to not start spying until you're actually ready:
it('Fails on a 500', () => {
cy.intercept('GET', 'something-unique').as('indicator')
foo()
cy.wait('#indicator').its('response.statusCode').should('eq', 200).then( () => {
// don't start spying until ready:
cy.intercept('POST', 'some-route').as('route')
cy.wait('#route').its('response.statusCode).should('not.eq', 500)
// and the test fails correctly; response.statusCode 500 should not equal 500
})
Background
I use 3 back-end servers to provide fault tolerance for one of my online SaaS application. All important API calls, such as getting user data, contact all 3 servers and use value of first successfully resolved response, if any.
export function getSuccessValueOrThrow$<T>(
observables$: Observable<T>[],
tryUntilMillies = 30000,
): Observable<T> {
return race(
...observables$.map(observable$ => {
return observable$.pipe(
timeout(tryUntilMillies),
catchError(err => {
return of(err).pipe(delay(5000), mergeMap(_err => throwError(_err)));
}),
);
})
);
}
getSuccessValueOrThrow$ get called as following:
const shuffledApiDomainList = ['server1-domain', 'server2-domain', 'server3-domain';
const sessionInfo = await RequestUtils.getSuccessValueOrThrow(
...(shuffledApiDomainList.map(shuffledDomain => this.http.get<SessionDetails>(`${shuffledDomain}/file/converter/comm/session/info`))),
).toPromise();
Note: if one request resolve faster than others, usually the case, race rxjs function will cancel the other two requests. On Chrome dev network tab it will look like bellow where first request sent out was cancelled due to being too slow.
Question:
I use /file/converter/comm/session/info (lets call it Endpoint 1) to get some data related to a user. This request dispatched to all 3 back-end servers. If one resolve, then remaining 2 request will be cancelled, i.e. they will return null.
On my Cypress E2E test I have
cy.route('GET', '/file/converter/comm/session/info').as('getSessionInfo');
cy.visit('https://www.ps2pdf.com/compress-mp4');
cy.wait('#getSessionInfo').its('status').should('eq', 200)
This sometimes fails if the since getSessionInfo alias was hooked on to a request that ultimately get cancelled by getSuccessValueOrThrow$ because it wasn't the request that succeeded.Bellow image shows how 1 out of 3 request aliased with getSessionInfo succeeded but the test failed since the first request failed.
In Cypress, how do I wait for a successful i.e. status = 200 request?
Approach 1
Use .should() callback and repeat the cy.wait call if status was not 200:
function waitFor200(routeAlias, retries = 2) {
cy.wait(routeAlias).then(xhr => {
if (xhr.status === 200) return // OK
else if (retries > 0) waitFor200(routeAlias, retries - 1); // wait for the next response
else throw "All requests returned non-200 response";
})
}
// Usage example.
// Note that no assertions are chained here,
// the check has been performed inside this function already.
waitFor200('#getSessionInfo');
// Proceed with your test
cy.get('button').click(); // ...
Approach 2
Revise what it is that you want to test in the first place.
Chances are - there is something on the page that tells the user about a successful operation. E.g. show/hide a spinner or a progress bar, or just that the page content is updated to show new data fetched from the backend.
So in this approach you would remove cy.wait() altogether, and focus on what the user sees on the page - do some assertions on the actual page content.
cy.wait() yields an object containing the HTTP request and response properties of the XHR. The error you're getting is because you're looking for property status in the XHR object, but it is a property of the Response Object. You first have to get to the Response Object:
cy.wait('#getSessionInfo').should(xhr => {
expect(xhr.response).to.have.property('status', 200);
});
Edit: Since our backend uses graphql, all calls use the single /graphql endpoint. So I had to come up with a solution to differentiate each call.
I did that by using the onResponse() method of cy.route() and accumulating the data in Cypress environment object:
cy.route({
method: 'GET',
url: '/file/converter/comm/session/info',
onResponse(xhr) {
if (xhr.status === 200) {
Cypress.env('sessionInfo200') = xhr;
}
}
})
You can then use it like this:
cy.wrap(Cypress.env()).should('have.property', 'sessionInfo200');
I wait like this:
const isOk = cy.wait("#getSessionInfo").then((xhr) => {
return (xhr.status === 200);
});
Here's the current test:
describe('/POST Register Page', function() {
it('it should register new user', function(/*done*/) {
chai.request(server)
.post('/auth/register')
.send(new_user_data)
.end(function(res) {
expect(res).to.have.status(2017);
// done();
})
})
})
The last I checked, there's no http code as 2017, however, it still passes:
Registration
Get register page
GET /auth/register 200 6.989 ms - 27
✓ it should get register page
/POST Register Page
✓ it should register new user
2 passing (147ms)
I want to simply post something, then get a response back, and play with the response.
If I include the done(), I get the timeout error:
1) Registration /POST Register Page it should register new user:
Error: Timeout of 3000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.
I can't get anything to work, as for whether wrong or right, the tests pass.
Although this get request is passing as expected:
describe('Get register page', function() {
it('it should get register page', function(done) {
chai.request(server)
.get('/auth/register')
.end(function(err, res) {
expect(err).to.be.null;
expect(res).to.have.status(200);
done();
})
})
})
I'm new at this mocha-cum-chai-chai-http thing, and the experience so far is weird.
Thanks.
Your POST request is probably taking longer than 3 seconds to complete, therefore mocha throws the timeout error.
You can try setting the timeout to a larger value like:
describe('/POST Register Page', function() {
// timeout in milliseconds
this.timeout(15000);
// test case
it('it should register new user', function(done) {
chai.request(server)
.post('/auth/register')
.send(new_user_data)
.end(function(res) {
expect(res).to.have.status(200);
done();
})
})
})
With some trial, you can figure out an optimum value of timeout to set in your tests.
When you don't use the done() callback, mocha simply skips the assertions without waiting for the actual response to arrive. Since the assertions in .end() block never get executed, mocha passes the test as it faces no assertions. I had faced something similar when I first started out with TDD, which I learned about the hard way.
Reference:
Because the end function is passed a callback, assertions are run
asynchronously. Therefore, a mechanism must be used to notify the
testing framework that the callback has completed. Otherwise, the test
will pass before the assertions are checked.