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.
Related
I'm fetching all the user's transactions within useEffect() but when I create a new transaction the useEffect doesn't reload then I need to refresh the page in order to see the changes. I've searched a lot and I've tried somethings such as useCallback() and useRef() with useEffect() but still doesn't work, probably because I'm not quite understand how to use them properly. When I pass the data that I want to be watched, in my case [transactions] I get an infinite loop because setState will mudate my state and then the component will reload hence useEffect will call my function and setState will be trigger and this will happen all over again.
const [transactions, setTransactions] = useState([]);
useEffect(() => {
getUserTransactions();
}, []);
const getUserTransactions = async () => {
if (currentUser) {
const token = await firebase.auth().currentUser.getIdToken();
axios
.get("http://localhost:8080/transactions", {
headers: {
"Content-Type": "application/json",
Authorization: token,
},
})
.then((res) => {
setTransactions(res.data);
})
.catch((err) => console.log(err));
}
};
I was wondering if the async operation can cause this issue because in another project, I haven't had any problem.
const createTransaction = async (e) => {
e.preventDefault();
if (currentUser) {
const token = await firebase.auth().currentUser.getIdToken();
const data = {
title: textRef.current.value,
price: priceRef.current.value,
category: categoryRef.current.value,
};
axios
.post("http://localhost:8080/transactions", data, {
headers: {
"Content-Type": "application/json",
Authorization: token,
},
})
.then((res) => {
setTransactions(prevTransactions => [...prevTransactions, res.data.rows])
})
.catch((err) => console.log(err));
setIsOpen(false);
}
};
That because the dependency array of your useEffect is empty, meaning it happens only once, on the initial render of the page.
The purpose of useEffect is to react to state changes, it's unaware of your backend. So when adding a new transaction, you should change the state accordingly. I'd suggest something along those lines:
When you add a transaction, the server's response will include all the details you need to display it on your frontend, so when you get the response object, just change the state by appending it. Something like:
const addTransaction = async () => {
const newTransactionData = (await axios.post('http://localhost:8000/transactions/add' /*or whatever your request looks like*/)).data
setTransactions(prevTransactions => [...prevTransactions, newTransactionData])
}
I am trying to unit test a function which makes an async call using an Axios helper instance. I have attempted multiple ways of trying to unit test this but I can not seem to find any material online which has helped. I've been stuck on this problem for a few days which is frustrating so any help would be appreciated! Below are the Axios Helper file (api.js)
api.js
import axios from 'axios'
const API = (token = null) => {
let headers = {
'Content-Type': 'application/json',
'Ocp-Apim-Subscription-key': process.env.NEXT_PUBLIC_API_HEADER_SUBSCRIPTION_KEY
}
if (token) {
const tokenHeader = { Authorization: 'Bearer ' + token }
headers = { ...headers, ...tokenHeader }
}
const url = process.env.NEXT_PUBLIC_API_BASE_URL
const API = axios.create({
baseURL: url,
headers
})
return API
}
export default API
mocked API
export default {
post: jest.fn(() =>
Promise.resolve({
data: {}
})
),
get: jest.fn(() =>
Promise.resolve({
data: {}
})
)
}
action file
export const initiate2FA = (destinationValue) => async () => {
const twoFactorAuth = destinationValue
const res = await API().post('/foo', {
Destination: twoFactorAuth
})
return res
}
Action.test.js
import API from 'api/api'
import { initiate2FA } from 'actions/userActions'
jest.mock('api/api')
const mockedAxios = API
const dispatch = jest.fn()
describe('Initiate2FA function', () => {
it('bar', async () => {
mockedAxios.get.mockImplementationOnce(() => Promise.resolve({ status: 200 }))
const t = await dispatch(initiate2FA('test#test.com'))
console.log(t)
})
})
My issue with the above test file is that it returns an anonymous function and I do not know how to handle this to pass the unit test. The goal of the test is to make sure the function is called. I am not sure if I am approaching this the correct way or should change my approach.
Again, any suggestions would be great!
Mocking an API call is something you can mock on your own React component, instead of a function, and the best option would be to not mock anything on your component. Here you can read all about why you should not mock your API functions. At the end of the article, you're going to find a library called Mock Service Worker which you can use for your purpose.
The way you declare you have an actual HTTP called that needs to be mocked would be something like this:
rest.get('/foo', async (req, res, ctx) => {
const mockedResponse = {bar: ''};
return res(ctx.json(mockedResponse))
}),
If you just need to unit test a function, you can still use Mock Service Worker to resolve the HTTP request, and then test what happens after that. This would still be your first choice. And the test would look like:
// this could be in another file or on top of your tests.
rest.get('/foo', async (req, res, ctx) => {
const mockedResponse = {bar: ''};
return res(ctx.json(mockedResponse))
}),
// and this would be your test
describe('Initiate2FA function', () => {
it('bar', async () => {
const res = await initiate2FA('test#test.com');
expect(res).toBe({bar: '');
})
})
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;
};
EDIT
I boiled down the problem. The following code yields an error in tests, but works as expected in the browser (see https://github.com/prumand/jest-marbles-merge-map and https://github.com/ReactiveX/rxjs/issues/4837)
tests: returns a WE_FINISH
browser (expected): MY_NEW_ERROR
// code
export default function basicMergeMapObs(
action$: Observable<Action>
) : Observable<any> {
return action$.pipe(
filter((val: Action) => {
throw new Error('We stop here')
}),
map((val: Action) => ({
type: 'WE_FINISH',
})),
catchError(() => of({
type: 'MY_NEW_ERROR',
}))
)
}
// test
it('should yield an MY_ERROR', () => {
const source = of({
type: 'TEST',
status: 'NEW'
})
getScheduler().run(helpers => {
const { expectObservable, cold } = helpers
expectObservable(
basicMergeMapObs(
source
)
).toBe(
'(t|)',
{
t: { type: 'MY_NEW_ERROR' }
}
)
})
})
function getScheduler() {
return new TestScheduler((actual, expected) => {
expect(actual).toMatchObject(expected);
});
}
UPDATE 19.06.2019
I added cartants example from the given github issue, which works fine. Still my example fails. No idea why. IMO it should always throw an error.
And yet another update, the tests don't fail on linux, but only on my windows machine
UPDATE 02.07.2019
:O seemed to be a issue with endpoint-security solution we use ...
Learning VueJS and trying to do a simple API call on component load to put a list of repos onto my page. When I call and set the this.repos from the created() method, no problem. But if I set it as a method and then call it from this.getRepos nothing happens. No error, nothing. What am I missing about VueJS?
This works:
data: () => ({
msg: 'Github Repos',
ok: 'Im practically giving away these repos',
repos: [],
}),
methods: {
},
async created() {
const repos = await axios.get('https://api.github.com/orgs/octokit/repos');
this.repos = repos.data.map(repo =>
`<div class="box"><a href="${repo.html_url}">
${repo.name}
</div>`,
);
},
This DOES NOT work:
data: () => ({
msg: 'Github Repos',
ok: 'Im practically giving away these repos',
repos: [],
}),
methods: {
getRepos: async () => {
const repos = await axios.get('https://api.github.com/orgs/octokit/repos');
this.repos = repos.data.map(repo =>
`<div class="box"><a href="${repo.html_url}">
${repo.name}
</div>`,
);
},
},
created() {
this.getRepos();
},
Any ideas? Thanks!
It's simply because you used arrow functions here so that this.repos's this is bound to window object. Changing async () => {} to async function() {} will help you overcome it.
See demo
Note that you should not use an arrow function to define a method (e.g. plus: () => this.a++). The reason is arrow functions bind the parent context, so this will not be the Vue instance as you expect and this.a will be undefined.
reference
Another way to do an Axios call with Vue using then() method:
demo
created() {
axios.get('https://api.github.com/orgs/octokit/repos', {
params: {
type: 'all',
},
})
.then((res) => {
console.log('Success Response', res.data);
res.data.forEach((repo) => {
this.repos.push({ name: repo.name, url: repo.html_url, language: repo.language });
});
})
.catch((err) => {
console.log('Error', err);
});
},