Cypress: Test fails if request from "cy.route()" is not found - cypress

I have a question about cypress testing.
I'm doing the following:
cy.route() to an url with alias
then cy.wait(#alias)
I know that the default action that cypress does it to fail the test if the there wasn't any request made to that url.
My problem is that I have multiple requests and one of them may not reach the request url. But I don't want that to fail my test, just to skip over it. How can I do this?
Basically, I'm asking how do you make your tests NOT to fail when you get this:
CypressError: Timed out retrying: cy.wait() timed out waiting 30000ms for the 221st response to the route: 'productRequest'. No response ever occurred.

If your usecase is to wait for requests and then continue with more commands, this solution might help you:
describe("route", () => {
it("hiting route", () => {
let req1 = false;
let req2 = false;
cy.server()
cy.route({
methdod: "GET",
onRequest: () => {
req1 = true;
},
url: "/will/eventually/called"
});
cy.route({
methdod: "GET",
onRequest: () => {
req2 = true;
},
url: "/will/eventually/called2"
});
setTimeout(() => {
req2 = true
}, 2000)
cy.visit("https://biehler-josef.de")
cy.get("body").should(() => {
if (req1) {
expect(req1).to.eq(true);
}
if (req2) {
expect(req2).to.eq(true);
}
if (!req1 && !req2) {
expect(false).to.eq(true)
}
});
cy.get("body").should("exist");
});
})
You define the routes and pass a onRequest function that sets a variable. This can be done with multiple routes. Then you use should with callback function. Within that you can check both variables and force to fail only if no request occurred. The setTimeoutin this example demonstrates a request that takes 2 seconds to finish.
If you want to check if a request is not hit, it is much easier. But this solution is not usable if you want to execute additional commands after the cy.wait(#alias):
describe("route", () => {
it("hiting route", (done) => {
cy.server()
cy.route("GET", "will/never/be/hit").as("requestalias");
cy.visit("https://biehler-josef.de")
cy.on("fail", (error) => {
if (error.name === "CypressError"
&& error.message.match(/.*Timed out retrying: cy.wait().*requestalias.*/)) {
// calling done forces cypress to turn test to green
done()
}
});
cy.wait("#requestalias")
});
})
With cy.on("fail") you can listen to the event that is thrown when a test fails. Caling done() within this will force the test to be green. But you can not continue with subsequent commands in your test. So the wait() must be the last command in your test

Related

How to make cypress wait for a response that depends on another response?

From response A (/list.json) my app receives a list of items. Based on the output of A, my app makes another set of requests B for individual items (/one.txt, /two.txt, ...).
Now in my test I want to make sure that all responses B return HTTP 200.
Waiting (cy.wait) for response A is fine. However, waiting for responses B is more difficult, because I have to start waiting just upon receiving response A where I learn about responses B.
I tried 2 options:
start waiting inside of cy.wait of response A - code,
start waiting outside of cy.wait of response A - code
Neither of those work. With option 1 I get
`cy.wait()` timed out waiting `5000ms` for the 1st request to the route: `one.txt`. No request ever occurred
And with option 2 I get a pass, even though /two.txt doesn't exist. Looks like cy.wait for responses B is added after the responses were received
Since all requests are triggered off the visit, and are dynamic, you need a single intercept that handles all requests.
To me that means adding some javascript and dynamic aliases.
// avoid status code 304, disable browser cache
Cypress.automation('remote:debugger:protocol', {
command: 'Network.clearBrowserCache'
})
describe('spec', () => {
it('test', () => {
let items = [];
cy.intercept('GET', '*', (req) => {
const slug = req.url.substring(req.url.lastIndexOf('/') + 1)
if (slug === 'list.json') {
req.alias = 'list'
}
if (items.includes(slug)) {
req.alias = 'item'
}
req.continue((res) => {
if (slug === 'list.json')) {
items = res.body;
}
})
})
cy.visit('https://demo-cypress.netlify.app');
cy.wait('#list') // wait for list
.then(() => { // now items is populated
for (let item of items) { // really just need the count
cy.wait('#item').then(interception => { // wait n-times
expect(interception.response.statusCode).to.eq(200);
})
}
})
})
})

Cypress MOCK api response for different status

I am testing my login component with Cypress (just started with it) and I want to handle three different cases where the API returns status 200, 400 or 500. I want to mock these responses to see how the frontend responds to that.
I want to mock the response for three different cases (200, 400 and 500) when sending a request to my API endpoint http://localhost:9999/api/login
I have written some code based on the docs but I still am not where I want to be.
describe('Login Approach', () => {
it('login', () => {
cy.visit('/login')
// these values email and pw shouldn't matter if mocking is done right
cy.get('#email')
.type('test')
.should('have.value', 'test')
cy.get('#password')
.type('123456')
.should('have.value', '123456')
cy.server()
cy.route({
method: 'POST',
url: 'http://localhost:9999/api/login', // this is the api that I send the request to
})
cy.location('pathname', { timeout: 10000 }).should('eq', '/login');
cy.title().should('include', 'Condeo')
cy.get('#notification').should('exist')
})
})
I am not getting status in the details of the test:
Method Url Stubbed Alias #
POST http://localhost:9999/api/login Yes -
You should use the wait method of cypress.
You can find the cypress documentation here.
For your use case, make sure you start the server and define the route before you visit the link. Just after visiting the link, use the cy.wait() method which will wait for that API call to finish.
Eg.
describe('Login Approach', () => {
it('login', () => {
cy.visit('/login')
// these values email and pw shouldn't matter if mocking is done right
cy.get('#email')
.type('test')
.should('have.value', 'test')
cy.get('#password')
.type('123456')
.should('have.value', '123456')
cy.server()
cy.route({
method: 'POST',
url: 'http://localhost:9999/api/login', // this is the api that I send the request to
}).as('login')
cy.location('pathname', { timeout: 10000 }).should('eq', '/login');
cy.title().should('include', 'Condeo')
cy.get('#notification').should('exist')
// Code which will try to visit the login API.
cy.wait('#login').then((xhr)=> {
if(xhr.status === 200) {
// Code to test when status is 200
} else if(xhr.status === 400) {
// Code to test when status is 400
} else {
// Code to test when status is none of the above.
}
})
})
})

Unit testing NestJS Observable Http Retry

I'm making a request to a 3rd party API via NestJS's built in HttpService. I'm trying to simulate a scenario where the initial call to one of this api's endpoints might return an empty array on the first try. I'd like to use RxJS's retryWhen to hit the api again after a delay of 1 second. I'm currently unable to get the unit test to mock the second response however:
it('Retries view account status if needed', (done) => {
jest.spyOn(httpService, 'post')
.mockReturnValueOnce(of(failView)) // mock gets stuck on returning this value
.mockReturnValueOnce(of(successfulView));
const accountId = '0812081208';
const batchNo = '39cba402-bfa9-424c-b265-1c98204df7ea';
const response =client.viewAccountStatus(accountId, batchNo);
response.subscribe(
data => {
expect(data[0].accountNo)
.toBe('0812081208');
expect(data[0].companyName)
.toBe('Some company name');
done();
},
)
});
My implementation is:
viewAccountStatus(accountId: string, batchNo: string): Observable<any> {
const verificationRequest = new VerificationRequest();
verificationRequest.accountNo = accountId;
verificationRequest.batchNo = batchNo;
this.logger.debug(`Calling 3rd party service with batchNo: ${batchNo}`);
const config = {
headers: {
'Content-Type': 'application/json',
},
};
const response = this.httpService.post(url, verificationRequest, config)
.pipe(
map(res => {
console.log(res.data); // always empty
if (res.status >= 400) {
throw new HttpException(res.statusText, res.status);
}
if (!res.data.length) {
this.logger.debug('Response was empty');
throw new HttpException('Account not found', 404);
}
return res.data;
}),
retryWhen(errors => {
this.logger.debug(`Retrying accountId: ${accountId}`);
// It's entirely possible the first call will return an empty array
// So we retry with a backoff
return errors.pipe(
delayWhen(() => timer(1000)),
take(1),
);
}),
);
return response;
}
When logging from inside the initial map, I can see that the array is always empty. It's as if the second mocked value never happens. Perhaps I also have a solid misunderstanding of how observables work and I should somehow be trying to assert against the SECOND value that gets emitted? Regardless, when the observable retries, we should be seeing that second mocked value, right?
I'm also getting
: Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error:
On each run... so I'm guessing I'm not calling done() in the right place.
I think the problem is that retryWhen(notifier) will resubscribe to the same source when its notifier emits.
Meaning that if you have
new Observable(s => {
s.next(1);
s.next(2);
s.error(new Error('err!'));
}).pipe(
retryWhen(/* ... */)
)
The callback will be invoked every time the source is re-subscribed. In your example, it will call the logic which is responsible for sending the request, but it won't call the post method again.
The source could be thought of as the Observable's callback: s => { ... }.
What I think you'll have to do is to conditionally choose the source, based on whether the error took place or not.
Maybe you could use mockImplementation:
let hasErr = false;
jest.spyOn(httpService, 'post')
.mockImplementation(
() => hasErr ? of(successView) : (hasErr = true, of(failView))
)
Edit
I think the above does not do anything different, where's what I think mockImplementation should look like:
let err = false;
mockImplementation(
() => new Observable(s => {
if (err) {
s.next(success)
}
else {
err = true;
s.next(fail)
}
})
)

How to repeat, circle XHR requests, handle multiple XHR requests in Cypress

How to make through an interval requests before tests?
I tried 2 ways to retry requests but either was failing;
I need to upload a file, waiting till one got imported successfully
On the first step i upload a file to my server in cypress
before( ()=> {
//my custom POST command
cy.form_request(url, data)
.then(({id}) => {
Then i wait for id of the uploaded file
check_It_Till_Success_It(id);
})
})
Then the received id i pass into a new request to verified its status on the server and need to repeat the request till the file processing is finished.
At the solution below it says
CypressError: cy.wait() only accepts aliases for routes.
The alias: 'check_it_request' did not match a route.
function check_It_Till_Success_It(id) {
function checkRequest() {
cy.request("GET", "http://localhost:28080/admin/api/catalog/import/status/" + id)
.then(({status}) => {
if (status === "FINISHED" || status === "FAILED") {
clearInterval(check_It);
} else {
console.log('retry one more time');
}
}).as('check_it_request');
cy.wait("#check_it_request");
}
checkRequest();
const check_It = setInterval(checkRequest, 1000);
}
or here is another my solution through a recursive requesting:
function check_It_Till_Success_It(id) {
return (
cy.request("GET", BASE_URL + "/admin/api/catalog/import/status/" + id)
.then(({status}) => {
if (status === "FINISHED" || status === "FAILED") {
console.log('success');
} else {
console.log('retry one more time');
setTimeout(() => check_It_Till_Success_It(id), 1000)
}
})
)
}
but it throws an error:
Uncaught CypressError: Cypress detected that you returned a promise from a command while also invoking one or more cy commands in that promise.
What am i doing wrong?
I found my mystake because of the use of native JS tools as setTimeout, setInterval.
Cypress doesnt allow to use them and replaces with controllable methods: cy.clock and cy.tick
So i took the recursion implementation and replaced with the methods above and my code became:
function check_It_Till_Success_It(id) {
cy.request("GET", BASE_URL + "/admin/api/catalog/import/status/" + id)
.then(resp => {
const status = resp.body.status;
if (status === "FINISHED" || status === "FAILED") {
console.log('success');
} else {
console.log('retry one more time');
cy.clock();
cy.tick(1000);
check_It_Till_Success_It(id)
}
})
}
Offtopic note: I'm new with Cypress and as i understood it replaces the most of the js native features so it's needed to look the docs much closer (BINGO!) or look at issue section because some JS specific feature just crash Cypress without any notification or catched error, for example: FormData object crashed Cypress'es request method.

How can I alias specific GraphQL requests in Cypress?

In Cypress, it is well-documented that you can alias specific network requests, which you can then "wait" on. This is especially helpful if you want to do something in Cypress after a specific network request has fired and finished.
Example below from Cypress documentation:
cy.server()
cy.route('POST', '**/users').as('postUser') // ALIASING OCCURS HERE
cy.visit('/users')
cy.get('#first-name').type('Julius{enter}')
cy.wait('#postUser')
However, since I'm using GraphQL in my app, aliasing no longer becomes a straightforward affair. This is because all GraphQL queries share one endpoint /graphql.
Despite it not being possible to differentiate between different graphQL queries using the url endpoint alone, it is possible to differentiate graphQL queries using operationName (refer to following image).
Having dug through the documentation, there doesn't appear to be a way to alias graphQL endpoints using operationName from the request body. I'm also returning the operationName (yellow arrow) as a custom property in my response header; however, I haven't managed to find a way to use it to alias specific graphQL queries either.
FAILED METHOD 1: This method attempts to use the purple arrow shown in image.
cy.server();
cy.route({
method: 'POST',
url: '/graphql',
onResponse(reqObj) {
if (reqObj.request.body.operationName === 'editIpo') {
cy.wrap('editIpo').as('graphqlEditIpo');
}
},
});
cy.wait('#graphqlEditIpo');
This method doesn't work since the graphqlEditIpo alias is registered at runtime and as such, the error I receive is as follows.
CypressError: cy.wait() could not find a registered alias for: '#graphqlEditIpo'. Available aliases are: 'ipoInitial, graphql'.
FAILED METHOD 2: This method attempts to use the yellow arrow shown in image.
cy.server();
cy.route({
method: 'POST',
url: '/graphql',
headers: {
'operation-name': 'editIpo',
},
}).as('graphql');
cy.wait('graphql');
This method doesn't work because the headers property in the options object for cy.route is actually meant to accept response headers for stubbed routes per the docs. Here, I'm trying to use it to identify my specific graphQL query, which obviously won't work.
Which leads me to my question: How can I alias specific graphQL queries/mutations in Cypress? Have I missed something?
The intercept API introduced in 6.0.0 supports this via the request handler function. I used it in my code like so:
cy.intercept('POST', '/graphql', req => {
if (req.body.operationName === 'queryName') {
req.alias = 'queryName';
} else if (req.body.operationName === 'mutationName') {
req.alias = 'mutationName';
} else if (...) {
...
}
});
Where queryName and mutationName are the names of your GQL operations. You can add an additional condition for each request that you would like to alias. You can then wait for them like so:
// Wait on single request
cy.wait('#mutationName');
// Wait on multiple requests.
// Useful if several requests are fired at once, for example on page load.
cy.wait(['#queryName, #mutationName',...]);
The docs have a similar example here: https://docs.cypress.io/api/commands/intercept.html#Aliasing-individual-requests.
This works for me!
Cypress.Commands.add('waitForGraph', operationName => {
const GRAPH_URL = '/api/v2/graph/';
cy.route('POST', GRAPH_URL).as("graphqlRequest");
//This will capture every request
cy.wait('#graphqlRequest').then(({ request }) => {
// If the captured request doesn't match the operation name of your query
// it will wait again for the next one until it gets matched.
if (request.body.operationName !== operationName) {
return cy.waitForGraph(operationName)
}
})
})
Just remember to write your queries with unique names as posible, because the operation name relies on it.
If 'waiting' and not 'aliasing' in itself is the main purpose, the easiest way to do this, as I've encountered thus far, is by aliasing the general graphql requests and then making a recursive function call to 'wait' targeting the newly created alias until you find the specific graphql operation you were looking for.
e.g.
Cypress.Commands.add('waitFor', operationName => {
cy.wait('#graphqlRequest').then(({ request }) => {
if (request.body.operationName !== operationName) {
return cy.waitFor(operationName)
}
})
})
This of course have its caveats and may or may not work in your context. But it works for us.
I hope Cypress enables this in a less hacky way in the future.
PS. I want to give credit to where I got the inspiration to this from, but it seemt to be lost in cyberspace.
Since I was having the same issue and I did not find a real solution for this problem I combined different options and created a workaround that solves my problem. Hopefully this can help someone else too.
I do not really 'wait' for the request to be happen but I catch them all, based on **/graphql url and match the operationName in the request. On a match a function will be executed with the data as parameter. In this function the tests can be defined.
graphQLResponse.js
export const onGraphQLResponse = (resolvers, args) => {
resolvers.forEach((n) => {
const operationName = Object.keys(n).shift();
const nextFn = n[operationName];
if (args.request.body.operationName === operationName) {
handleGraphQLResponse(nextFn)(args.response)(operationName);
}
});
};
const handleGraphQLResponse = (next) => {
return (response) => {
const responseBody = Cypress._.get(response, "body");
return async (alias) => {
await Cypress.Blob.blobToBase64String(responseBody)
.then((blobResponse) => atob(blobResponse))
.then((jsonString) => JSON.parse(jsonString))
.then((jsonResponse) => {
Cypress.log({
name: "wait blob",
displayName: `Wait ${alias}`,
consoleProps: () => {
return jsonResponse.data;
}
}).end();
return jsonResponse.data;
})
.then((data) => {
next(data);
});
};
};
};
In a test file
Bind an array with objects where the key is the operationName and the value is the resolve function.
import { onGraphQLResponse } from "./util/graphQLResponse";
describe("Foo and Bar", function() {
it("Should be able to test GraphQL response data", () => {
cy.server();
cy.route({
method: "POST",
url: "**/graphql",
onResponse: onGraphQLResponse.bind(null, [
{"some operationName": testResponse},
{"some other operationName": testOtherResponse}
])
}).as("graphql");
cy.visit("");
function testResponse(result) {
const foo = result.foo;
expect(foo.label).to.equal("Foo label");
}
function testOtherResponse(result) {
const bar = result.bar;
expect(bar.label).to.equal("Bar label");
}
});
}
Credits
Used the blob command from glebbahmutov.com
This is what you're looking for (New in Cypress 5.6.0):
cy.route2('POST', '/graphql', (req) => {
if (req.body.includes('operationName')) {
req.alias = 'gqlMutation'
}
})
// assert that a matching request has been made
cy.wait('#gqlMutation')
Documentation:
https://docs.cypress.io/api/commands/route2.html#Waiting-on-a-request
I hope that this helps!
I used some of these code examples but had to change it slightly to add the onRequest param to the cy.route and also add the date.Now (could add any auto incrementer, open to other solutions on this) to allow multiple calls to the same GraphQL operation name in the same test. Thanks for pointing me in the right direction!
Cypress.Commands.add('waitForGraph', (operationName) => {
const now = Date.now()
let operationNameFromRequest
cy.route({
method: 'POST',
url: '**graphql',
onRequest: (xhr) => {
operationNameFromRequest = xhr.request.body.operationName
},
}).as(`graphqlRequest${now}`)
//This will capture every request
cy.wait(`#graphqlRequest${now}`).then(({ xhr }) => {
// If the captured request doesn't match the operation name of your query
// it will wait again for the next one until it gets matched.
if (operationNameFromRequest !== operationName) {
return cy.waitForGraph(operationName)
}
})
})
to use:
cy.waitForGraph('QueryAllOrganizations').then((xhr) => { ...
This is how I managed to differentiate each GraphQL request. We use cypress-cucumber-preprocessor so we have a common.js file in /cypress/integration/common/ where we can call a before and beforeEach hook which are called before any feature file.
I tried the solutions here, but couldn't come up with something stable since, in our application, many GraphQL requests are triggered at the same time for some actions.
I ended up storing every GraphQL requests in a global object called graphql_accumulator with a timestamp for each occurence.
It was then easier to manage individual request with cypress command should.
common.js:
beforeEach(() => {
for (const query in graphql_accumulator) {
delete graphql_accumulator[query];
}
cy.server();
cy.route({
method: 'POST',
url: '**/graphql',
onResponse(xhr) {
const queryName = xhr.requestBody.get('query').trim().split(/[({ ]/)[1];
if (!(queryName in graphql_accumulator)) graphql_accumulator[queryName] = [];
graphql_accumulator[queryName].push({timeStamp: nowStamp('HHmmssSS'), data: xhr.responseBody.data})
}
});
});
I have to extract the queryName from the FormData since we don't have (yet) the key operationName in the request header, but this would be where you would use this key.
commands.js
Cypress.Commands.add('waitGraphQL', {prevSubject:false}, (queryName) => {
Cypress.log({
displayName: 'wait gql',
consoleProps() {
return {
'graphQL Accumulator': graphql_accumulator
}
}
});
const timeMark = nowStamp('HHmmssSS');
cy.wrap(graphql_accumulator, {log:false}).should('have.property', queryName)
.and("satisfy", responses => responses.some(response => response['timeStamp'] >= timeMark));
});
It's also important to allow cypress to manage GraphQL requests by adding these settings in /cypress/support/index.js:
Cypress.on('window:before:load', win => {
// unfilters incoming GraphQL requests in cypress so we can see them in the UI
// and track them with cy.server; cy.route
win.fetch = null;
win.Blob = null; // Avoid Blob format for GraphQL responses
});
I use it like this:
cy.waitGraphQL('QueryChannelConfigs');
cy.get(button_edit_market).click();
cy.waitGraphQL will wait for the latest target request, the one that will be stored after the call.
Hope this helps.
Somewhere else this method was suggested.
Btw it all becomes a bit easier once you migrate to Cypress v5.x and make use of the new route (route2) method.
Our use case involved multiple GraphQL calls on one page. We had to use a modified version of the responses from above:
Cypress.Commands.add('createGql', operation => {
cy.route({
method: 'POST',
url: '**/graphql',
}).as(operation);
});
Cypress.Commands.add('waitForGql', (operation, nextOperation) => {
cy.wait(`#${operation}`).then(({ request }) => {
if (request.body.operationName !== operation) {
return cy.waitForGql(operation);
}
cy.route({
method: 'POST',
url: '**/graphql',
}).as(nextOperation || 'gqlRequest');
});
});
The issue is that ALL GraphQL requests share the same URL, so once you create a cy.route() for one GraphQL query, Cypress will match all the following GraphQL queries to that. After it matches, we set cy.route() to just a default label of gqlRequest or the next query.
Our test:
cy.get(someSelector)
.should('be.visible')
.type(someText)
.createGql('gqlOperation1')
.waitForGql('gqlOperation1', 'gqlOperation2') // Create next cy.route() for the next query, or it won't match
.get(someSelector2)
.should('be.visible')
.click();
cy.waitForGql('gqlOperation2')
.get(someSelector3)
.should('be.visible')
.click();

Resources