testing redux-saga inside if statement and using real values - javascript

How do I test a function inside an if statement or try/catch? For instance,
export function* onFetchMessages(channel) {
yield put(requestMessages())
const channel_name = channel.payload
try {
const response = yield call(fetch,'/api/messages/'+channel_name)
if(response.ok){
const res = yield response.json();
const date = moment().format('lll');
yield put(receiveMessages(res,channel.payload,date))
}
} catch (error){
yield put(rejectMessages(error))
}
}
I need to input a real channel name that actually exist in the database for it to return a valid response for the yields that follow to execute, otherwise it will throw an error. In addition, I will get an error message, cannot read property json of undefined, so the yield after that cannot be reached due to this error message.
So my first problem is 'if(response.ok)' but even if I remove it, yield response.json() would return an error and in addition the yield after that wont be executed.
If anyone can show me how to test these, would be much appreciated.

Pass the response object to the previous execution and test conditional, I would do it like this, hope this helps:
export function* onFetchMessages(channel) {
try {
yield put(requestMessages())
const channel_name = channel.payload
const response = yield call(fetch,'/api/messages/'+channel_name)
if(response.ok){
const res = yield response.json();
const date = moment().format('lll');
yield put(receiveMessages(res,channel.payload,date))
}
} catch (error){
yield put(rejectMessages(error))
}
}
describe('onFetchMessages Saga', () => {
let output = null;
const saga = onFetchMessages(channel); //mock channel somewhere...
it('should put request messages', () => {
output = saga.next().value;
let expected = put(requestMessages()); //make sure you import this dependency
expect(output).toEqual(expected);
});
it('should call fetch...blabla', ()=> {
output = saga.next(channel_name).value; //include channel_name so it is avaiable on the next iteration
let expected = call(fetch,'/api/messages/'+channel_name); //do all the mock you ned for this
expect(output).toEqual(expected);
});
/*here comes you answer*/
it('should take response.ok into the if statemenet', ()=> {
//your json yield is out the redux-saga context so I dont assert it
saga.next(response).value; //same as before, mock it with a ok property, so it is available
output = saga.next(res).value; //assert the put effect
let expected = put(receiveMessages(res,channel.payload,date)); //channel should be mock from previous test
expect(output).toEqual(expected);
});
});
Notice your code probably does more stuff I'm not aware of, but this at least should put u in some line to solve your problem.

You might want to use an helper library for that, such as redux-saga-testing.
Disclaimer: I wrote this library to solve that exact same problem
For your specific example, using Jest (but works the same for Mocha), I would do two things:
First, I would separate the API call to a different function
Then I would use redux-saga-testing to test your logic in a synchronous way:
Here is the code:
import sagaHelper from 'redux-saga-testing';
import { call, put } from 'redux-saga/effects';
import { requestMessages, receiveMessages, rejectMessages } from './my-actions';
const api = url => fetch(url).then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error(response.status); // for example
}
});
function* onFetchMessages(channel) {
try {
yield put(requestMessages())
const channel_name = channel.payload
const res = yield call(api, '/api/messages/'+channel_name)
const date = moment().format('lll');
yield put(receiveMessages(res,channel.payload,date))
} catch (error){
yield put(rejectMessages(error))
}
}
describe('When testing a Saga that throws an error', () => {
const it = sagaHelper(onFetchMessages({ type: 'foo', payload: 'chan1'}));
it('should have called the API first, which will throw an exception', result => {
expect(result).toEqual(call(api, '/api/messages/chan1'));
return new Error('Something went wrong');
});
it('and then trigger an error action with the error message', result => {
expect(result).toEqual(put(rejectMessages('Something went wrong')));
});
});
describe('When testing a Saga and it works fine', () => {
const it = sagaHelper(onFetchMessages({ type: 'foo', payload: 'chan2'}));
it('should have called the API first, which will return some data', result => {
expect(result).toEqual(call(api, '/api/messages/chan2'));
return 'some data';
});
it('and then call the success action with the data returned by the API', result => {
expect(result).toEqual(put(receiveMessages('some data', 'chan2', 'some date')));
// you'll have to find a way to mock the date here'
});
});
You'll find plenty of other examples (more complex ones) on the project's GitHub.

Here's a related question: in the redux-saga docs, they have examples where take is listening for multiple actions. Based on this, I wrote an auth saga that looks more or less like this (you may recognize that this is a modified version of an example from the redux-saga docs:
function* mySaga() {
while (true) {
const initialAction = yield take (['AUTH__LOGIN','AUTH__LOGOUT']);
if (initialAction.type === 'AUTH__LOGIN') {
const authTask = yield fork(doLogin);
const action = yield take(['AUTH__LOGOUT', 'AUTH__LOGIN_FAIL']);
if (action.type === 'AUTH__LOGOUT') {
yield cancel(authTask);
yield call (unauthorizeWithRemoteServer)
}
} else {
yield call (unauthorizeWithRemoteServer)
}
}
}
I don't think this is an anti-pattern when dealing with Sagas, and the code certainly runs as expected outside the test environment (Jest). However, I see no way to handle the if statements in this context. How is this supposed to work?

Related

How to extract data from api function with a promise in react

Im using react-redux, and in my saga file where I have implemented logic for new/edit page, I need to implement an API for getting some codes for customer.
const getCodesById = (Id) => get(`${BASE_URL}/${companyId}/codes`);
export function* getTableById(action) {
const Id = yield select(getCurrentCustomeId);
getEarningCodesForCompany(companyId).then((response) => {
console.log(response) //It shows correct array of objects from api
return response;
});
}
in console.log(response) I can see the data properly.
However, I dont know how can I extract that response in some variable outside that function to be able to use it along in the function getTableById.
I tried with const request = yield call(getCodesById(Id)); but with yield my program is crashing.
How can I do this to, get response and use it elsewhere?
export function* getTableById(action) {
const Id = yield select(getCurrentCustomeId);
const codes = yield call(getEarningCodesForCompany, companyId);
console.log(codes)
}
Did you try adding a return statement before getEarningCodesForCompany:
const getCodesById = (Id) => get(`${BASE_URL}/${companyId}/codes`);
export function* getTableById(action) {
const Id = yield select(getCurrentCustomeId);
return getEarningCodesForCompany(companyId).then((response) => {
console.log(response) //It shows correct array of objects from api
return response;
});
}

How to make other function gets called after first is executed?

I am working on a react app where I am using redux for state management and I have 2 functions to call and I want them to run only after 1st function is executed.
Here's a snippet of whats I am doing:
if (this.props.page_num < this.props.numPages) {
this.props.fetchCode(params, isFiltered, isSearched).then(() => {
this.props.setPageNumber(this.props.page_num + 1);
});
}
Here I am getting a error stating:
CodeTable.jsx?2468:132 Uncaught TypeError: this.props.fetchCode(...).then is not a function
fetchCode function:
export function* fetchCode(action) {
try {
const response = yield call(Services.fetchCode, action.params);
const { dtoList } = response.data.pagedList;
const num_pages = response.data.pagedList.numPages;
const total_records = response.data.pagedList.totalRecords;
const page_number = response.data.pagedList.pageNumber;
const postCodeSetsData = dtoList.map(({
}) => ({
}));
yield put(ActionCreator.setCodes(dtoList, num_pages, total_records, postCodeData, page_number, action.isFiltered, action.isSearched));
} catch (error) {
sagaException(error);
}
}
Since you are using redux saga, I believe the most appropriate thing to do is compose another saga.
export function* fetchCodeSetsAndSetPage(action) {
try {
yield put (ActionCreator.fetchCodes(...));
yield put (ActionCreator.setPageNumber(...));
} catch (error) {
sagaException(error);
}
}
And then call that one instead in your component.
Some docs.
make fetchCodeSets function async. You can write promise like then only with an async functions. also use a return statement inside the function. The declaration of the fetchCodeSets function should be like this
const fetchCodeSets = async (params) => {
//codes here
return;
}

Problem with select in redux-saga. Error: call: argument of type {context, fn} has undefined or null `fn`

After looking through some answers to similar questions here, I just can't get my selector to work. Here's my selector.js:
export const getButtonStatus = state => state.buttonStatus;
(That's the entirety of the file. I don't know if I have to import anything into it. Didn't seem like it from looking at other answers I've seen here.)
and here's what I'm where I'm trying to access the selector in my saga:
import { takeLatest, call, put, select } from "redux-saga/effects";
import { getButtonStatus } from "./selector.js";
...
export function* watcherSaga() {
yield takeLatest("get-tweets", workerSaga);
}
function* workerSaga() {
try {
const buttonStatus = yield select(getButtonStatus);
const response = yield call(getTweets(buttonStatus));
const tweets = response.tweets;
yield put({
type: "tweets-received-async",
tweets: tweets,
nextTweeter: response.nextTweeter
});
} catch (error) {
console.log("error = ", error);
yield put({ type: "error", error });
}
}
...
Here's the error I'm receiving:
Error: call: argument of type {context, fn} has undefined or null `fn`
I'm new to Saga. Can anyone tell me what I'm doing wrong?
The error is not with your selector but with your yield call - it takes the function as an arg followed by the arguments to pass to the function: https://redux-saga.js.org/docs/api/#callfn-args. So it should be:
const response = yield call(getTweets, buttonStatus);
Otherwise looks good!
The Problem
You are probably doing this:
const foo = yield call(bar())
So you don't pass the function itself, but rather the function call.
The Fix
Try to only send the function, not its call.
const foo = yield call(bar)
Notice that we have bar only, not bar().

Javascript yield inside object

So I am using redux-saga and I have this generator function:
export function * getData(api, action) {
const response = yield call(api.getData)
if (response.ok) {
let { data } = response;
yield put(MyActions.responseSuccess(data))
}
}
Everything works as expected, there is a request and if the response is ok I get change of state in my component.
Now I wanted to have a websocket connection, WAMP to be precise. I wrote the wamp function that looks like this:
const default_callbacks = {
onopen: () => console.log('Default callback - onopen.')
}
const openConnection = (data, callbacks = default_callbacks) => {
const wsuri = "wss://wssuri";
let connection = new wamp.Connection({
url: wsuri,
realm: "realm1"
});
connection.onopen = () => {
console.log('Connection opened')
callbacks.onopen();
}
// Some code managing the data
connection.open()
}
export default { openConnection }
Now what I wanted to do is to use this openConnection function inside my generator function, and provide callbacks that will change the state of my application (as there will be a subscription to a channel inside this connection). So I modified my generator functions to look like this:
export function * getData(api, action) {
const response = yield call(api.getData)
if (response.ok) {
let { data } = response;
yield put(MyActions.responseSuccess(data))
const callbacks = {
onopen: yield put(MyActions.wampConnectionOpened())
}
WAMP.openConnection(data, callbacks);
}
}
Now in my console I see the log saying that connection was opened, but there is no change in the state.
What am I doing wrong? Maybe my whole approach to this problem is wrong?

How to migrate api call wrapper from a redux thunk to redux saga

I have recently started using redux-saga and I'm really liking it.
I have the following wrapper which I was using for my api calls which would take a promise (my api call), and display a preloader and handle errors.
export const callApi = (promise: Promise<any>, errorMsg: string = 'Api error') => (dispatch: Dispatch) => {
dispatch(setLoading(true));
return promise.then(
(response) => {
dispatch(setLoading(false));
return response.body;
},
(error) => {
dispatch(setLoading(false));
dispatch(apiError(errorMsg, error));
return error;
});
};
I'm unsure how I would replicate behaviour like this in redux saga. I couldnt find any example of doing anything like this?
So far I've come up with
const camelizeKeysPromise = (obj) => Promise.resolve(camelizeKeys(obj));
export function* sagaCallApi(promise: Promise<any>, errorMsg: string = 'Api error') {
yield put(setLoading(true));
try {
const response = yield call(promise);
try {
const result = yield call(camelizeKeysPromise(response.body));
return result;
} catch (e) {
return response.body;
}
} catch (exception) {
yield put(setLoading(false));
yield put(apiError(errorMsg, error));
};
}
Yielding a call to promise will not return the desired response. You can use eventChannel from redux-saga to create a channel that emits the response on success or the error object on failure and then subscribe to the channel in your saga.
const promiseEmitter = promise => {
return eventChannel(emit => {
promise.then(
response => emit({response}),
error => emit({error})
);
});
};
Modify your new saga by replacing the call to the promise with this:
const channel = yield call(promiseEmitter, promise);
const {response, error} = yield take(channel);
if(response){
// handle success
return response;
}else if(error){
// handle failure
yield put(setLoading(false));
yield put(apiError(errorMsg, error));
}
Be aware that there might be syntactical errors in my code as I wrote this without an editor, but you can get the general approach.

Categories

Resources