Cypress cy.request .then chaining returning undefined - cypress

I'm upgrading Cypress from 10.2.0 to 10.11.0 and I'm encountering some behaviour I'm trying to understand.
In the second .then, the response is undefined. This had previously worked on 10.2.0.
public makeRequest(params) {
return cy.request({
...params
})
.then((response) => {
// do something with response
});
}
this.makeRequest(params)
.then((response) => {
// response is undefined
});
Can anyone point me in the right direction, I have checked the changelogs for every version since 10.3.0 and cannot find anything to explain this behaviour.
Thanks!

Cypress (version 10.11.0 and previous versions) returns the last command result taken within the cy.request().then() chain, when there is no explicit return value given.
For example, if // do some async tasks is a non-Cypress asynchronous query, the response is returned:
cy.visit('http://example.com');
function makeRequest(params) {
return cy.request(params)
.then((response) => {
// do some async tasks
setTimeout(() => {
console.log(response.title)
expect(response.body.title).to.eq('delectus aut autem') // passes
}, 1000)
})
}
makeRequest({url: 'https://jsonplaceholder.typicode.com/todos/1'})
.then(response => {
expect(response.body.title).to.eq('delectus aut autem') // passes
})
If however you issue more Cypress commands inside the // do some async tasks block, the last chained "subject" changes, and you get a different return value
cy.visit('http://example.com');
function makeRequest(params) {
return cy.request(params)
.then((response) => {
// do some async tasks
cy.get('h1') // changes "subject" from response to <h1> element
})
}
makeRequest({url: 'https://jsonplaceholder.typicode.com/todos/1'})
.then(response => {
console.log(response) // not your response, but the last "subject" found above
expect(response.text()).to.eq('Example Domain') // passes
})

Adding a return returns the response
public makeRequest(params) {
return cy.request({
...params
})
.then((response) => {
// do something with response
return response;
});
}
If you are doing some async tasks in makeRequest.then(), do your async tasks and return response in another .then. E.g:
public makeRequest(params) {
return cy.request({
...params
})
.then((response) => {
// do some async tasks
})
.then((response) => {
return response;
});
}

Related

How to fail cypress test from inside the Promise.prototype.catch() block?

I'm using a node library to execute api calls for test data setup and teardown. The library works as follows:
someApiServiceObject
.executeApiCall({... parameters})
.then(doSomethingWithResults())
.catch(() => {
// Here I would like to fail the test as something has gone wrong
})
If the request fails for some reason, I only learn about it by the Promise returning from executeApiCall function being rejected - hence the catch block.
But if I put throw new Error(); into the catch block or remove the catch block, I can see the (uncaught exception) Error: in the cypress console, but the test still passes.
Can someone advise me on how this case should be handled correctly?
The test:
it('List projects', () => {
projectsApi.projectsList({})
.then(() => {
cy.log('Success');
}).catch(() => {
throw new Error();
});
});
If you call someApiServiceObject.executeApiCall({...parameters}) in a task (since it's a node library), you should just be able to return the promise and Cypress handles failing the test. Don't catch() within the task.
module.exports = (on, config) => {
on('task', {
api(parameters) {
return someApiServiceObject
.executeApiCall({... parameters})
.then(doSomethingWithResults())
},
})
}
If that fails, follow this pattern Return number of files in the folder
module.exports = (on, config) => {
on('task', {
countFiles(folderName) {
return new Promise((resolve, reject) => {
someApiServiceObject
.executeApiCall({... parameters})
.then(doSomethingWithResults())
.then((results) => resolve(results))
.catch((err) => reject(err))
})
})
},
})
}
From comments, I think there's a assumption being made that .executeApiCall() must be returning a promise, but that may not be the case.
For example cy.get(...) has a .then(...) method, but it does not return a promise, it just has a .then() method.
If .executeApiCall() does actually return a promise, the first example is all you need. If it does not, you need to wrap the code.
Cypress will recognise a promise returned from a task, and use resolve or reject accordingly.

Cypress: Returning value in promise within custom command?

So I am trying to use a custom command to reduce the need to write the same thing in multiple files. Specifically this is for logging in and setting a token via JWT.
Here is the current working code (borrowed from JWT login example from cypress examples):
let user;
before(function() {
cy.request("POST", Cypress.env("auth_url"), {
username: Cypress.env("auth_username"),
password: Cypress.env("auth_password")
})
.its("body")
.then(res => {
user = res;
});
});
beforeEach(function() {
console.log(cy.get_auth_token)
cy.visit("/", {
onBeforeLoad(win) {
// set the user object in local storage
win.localStorage.setItem("token", user.token);
}
});
});
So i tried to do something similar via:
Cypress.Commands.add("get_auth_token", () => {
let user;
cy.request("POST", Cypress.env("auth_url"), {
username: Cypress.env("auth_username"),
password: Cypress.env("auth_password")
})
.its("body")
.then(res => {
user = res;
});
return user;
})
However when I try to call it within my beforeEach function as let user = cy.get_auth_token I get errors about user being undefined. Am I doing something wrong with returning the value? Im not an expert at promises...but this feels like it should be working?
Thanks!
Commands are not like functions, the return value is not assignable to a local variable. Instead they 'yield' it to the next command in the chain, which can be a then(). Also, the value is a 'subject' which is a jquery-wrapped version of the return value.
In short, this should be how you use your custom command:
beforeEach(function() {
cy.get_auth_token().then($user => {
console.log($user[0]);
cy.visit("/", {
onBeforeLoad(win) {
// set the user object in local storage
win.localStorage.setItem("token", $user[0].token);
}
});
});
});
Try to put your code inside a Promise and resolve 'user'. Using Cypress.Promise, it will wait until user is returned:
Cypress.Commands.add("get_auth_token", () => {
return new Cypress.Promise((resolve, reject) => {
cy.request("POST", Cypress.env("auth_url"), {
username: Cypress.env("auth_username"),
password: Cypress.env("auth_password")
})
.its("body")
.then(user => {
resolve(user);
});
})
})

Cypress sharing variables/alias between Hooks?

So I have a pretty `before` and `beforeEach` function that runs before all tests. It looks something like this:
describe("JWT Authentication", function() {
before(function() {
// custom command runs once to get token for JWT auth
// alias token as 'user' for further use
cy.get_auth_token().as('user')
})
beforeEach(function() {
// before each page load, set the JWT to the aliased 'user' token
cy.visit("/", {
onBeforeLoad(win) {
// set the user object in local storage
win.localStorage.setItem("token", this.user.token);
}
})
})
it("a single test...", function() {
//do stuff
});
The custom command is also pretty simple:
Cypress.Commands.add("get_auth_token", () => {
cy.request("POST", Cypress.env("auth_url"), {
username: Cypress.env("auth_username"),
password: Cypress.env("auth_password")
})
.its("body")
.then(res => {
return res;
});
})
The custom command itself works and retrieves the token as expected. However when it comes to the beforeEach it has no idea what this.user.token is. Specifically not knowing what user is.
One option is of course calling the command in every beforeEach which is what the JWT Cypress recipe/example spec does. However this feels excessive because in my case I do not NEED to grab the token every test. I only need to grab it once for this set of tests.
So how can I share the token to the beforeEach hook with a Cypress custom command.
I ran a few tests, all the bits seem to work!
The following does not give you an answer, but may help you debug.
Passing token between before() and beforeEach()
Assume we have a user in before(), does it get to the onBeforeLoad() callback?
describe("JWT Authentication", function() {
before(function() {
const mockUser = { token: 'xyz' };
cy.wrap(mockUser).as('user');
})
beforeEach(function() {
cy.visit("http://example.com", {
onBeforeLoad(win) {
console.log(this.user.title); // prints 'xyz'
}
})
})
it("a single test...", function() {
//do stuff
})
});
Is the custom command working
I can't find a generic mock for an Auth check, but any cy.request() that gets an object should be equivalent.
I'm hitting typicode.com and looking for the title property
describe("JWT Authentication", function() {
Cypress.Commands.add("get_auth_token", () => {
cy.request("GET", 'https://jsonplaceholder.typicode.com/todos/1')
.its("body")
.then(body => {
console.log('body', body); // prints {userId: 1, id: 1, title: "delectus aut autem", completed: false}
return body;
});
})
before(function() {
cy.get_auth_token()
.then(user => console.log('user', user)) // prints {userId: 1, id: 1, title: "delectus aut autem", completed: false}
.as('user')
})
beforeEach(function() {
cy.visit("http://example.com", {
onBeforeLoad(win) {
console.log(this.user.title); // prints 'delectus aut autem'
}
})
})
it("a single test...", function() {
//do stuff
})
});
Custom command
This shorter version also seems to work
Cypress.Commands.add("get_auth_token", () => {
cy.request("GET", 'https://jsonplaceholder.typicode.com/todos/1')
.its("body");
})

Can the completion of one async call be sequenced before the start of another using useEffect?

I'm trying to use useEffect in my React app but also refactor things more modularly. Shown below is the heart of actual working code. It resides in a Context Provider file and does the following:
1. Calls AWS Amplify to get the latest Auth Access Token.
2. Uses this token, in the form of an Authorization header, when an Axios GET call is made to an API Endpoint.
This works fine but I thought it would make more sense to move Step #1 into its own useEffect construct above. Furthermore, in doing so, I could then also store the header object as its own Context property, which the GET call could then reference.
Unfortunately, I can now see from console log statements that when the GET call starts, the Auth Access Token has not yet been retrieved. So the refactoring attempt fails.
useEffect(() => {
const fetchData = async () => {
const config = {
headers: { "Authorization":
await Auth.currentSession()
.then(data => {
return data.getAccessToken().getJwtToken();
})
.catch(error => {
alert('Error getting authorization token: '.concat(error))
})
}};
await axios.get('http://127.0.0.1:5000/some_path', config)
.then(response => {
// Process the retrieved data and populate in a Context property
})
.catch(error => {
alert('Error getting data from endpoint: '.concat(error));
});
};
fetchData();
}, [myContextObject.some_data]);
Is there a way of refactoring my code into two useEffect instances such that the first one will complete before the second one starts?
You could hold the config object in a state. This way you can separate both fetch calls and trigger the second one once the first one finished:
const MyComponent = props => {
const myContextObject = useContext(myContext);
const [config, setConfig] = useState(null);
useEffect(() => {
const fetchData = async () => {
const config = {
headers: {
Authorization: await Auth.currentSession()
.then(data => {
return data.getAccessToken().getJwtToken();
})
.catch(error => {
alert("Error getting authorization token: ".concat(error));
})
}
};
setConfig(config);
};
fetchData();
}, [myContextObject.some_data]);
useEffect(() => {
if (!config) {
return;
}
const fetchData = async () => {
await axios
.get("http://127.0.0.1:5000/some_path", config)
.then(response => {
// Process the retrieved data and populate in a Context property
})
.catch(error => {
alert("Error getting data from endpoint: ".concat(error));
});
};
fetchData();
// This should work for the first call (not tested) as it goes from null to object.
// If you need subsequent changes then youll have to track some property
// of the object or similar
}, [config]);
return null;
};

Test for react async action creator - undefined data

I have the following action and test case - when I run this test(jest) - I am seeing TypeError: Cannot read property 'data' of undefined in action creator, not sure what is missing here? I am providing mockData that is expected. is it because there is an async nested here? but i am using `.then but it still fails.
Action creator:
export const getUser = ({
uname,
apiendpoint,
}) => {
const arguments = {};
return async (dispatch) => {
await axiosHelper({ ---> this will return axios.get
arguments,
path: `${apiendpoint}/${uname}`,
dispatch,
}).then(async ({ data, headers }) => { -- getting error at this line.
dispatch({ type: GET_USER, payload: data });
dispatch({ type: GET_NUMBEROFUSERS, payload: headers });
});
};
};
Test:
describe('Get User Action', () => {
let store;
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
beforeEach(() => {
store = mockStore({
data: [],
});
});
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
})
const arguments = {
uname: 'user123',
apiendpoint: 'test',
};
const url = 'https://www.localhost.com/blah/blah';
it('should get a User', () => {
fetchMock
.getOnce(url, {
data: mockData, -->external mock js file with user data {}
headers: {
'content-type': 'application/json'
}
});
const expectedActions = [
{
type: 'GET_USER',
data: mockData
},
{ type: 'GET_NUMBEROFUSERS' }
];
return store.dispatch(actions.getUser(arguments)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
You are using await AND then on the same function (axiosHelper for example).
This is wrong usage and will lead to many errors of undefined.
You either use a callback-function or a .then() or an await but not 2 or all of them.
I recommend to watch some tutorials/explanations about async/await because it's really important to understand what a Promise is.
What's happening in your cas is that axiosHelper is executed 2 times, because if it's finished the then-part will fire up but at the exactly same time (because it's async) the await finishes and code-execution continues the parent flow. This brings up race-conditions and, as i said, will lead to undefined because you are executing the same logic twice or more.

Resources