how to receive a take with runSaga / redux-saga - testing

I created a recordSaga function, its target is to record what actions have been dispatched during the saga.
export const recordSaga = async (saga, initialAction, state) => {
const dispatched = [];
const done = await runSaga(
{
dispatch: action => dispatched.push(action),
getState: () => state,
},
saga,
initialAction,
).done;
return {
dispatched,
done,
};
};
so let's say my saga is this one
export function* mySaga() {
const needToSave = yield select(needToSaveDocument);
if (needToSave) {
yield put(saveDocument());
yield take(SAVE_DOCUMENT_SUCCESS);
}
yield put(doSomethingElse())
}
I want to write two tests, which I expect to be the following
describe('mySaga', async () => {
it('test 1: no need to save', async () => {
const state = { needToSave: false }
const { dispatched } = await recordSaga(mySaga, {}, state);
expect(dispatched).toEqual([
doSomethingElse()
])
})
it('test 2: need to save', async () => {
const state = { needToSave: true }
const { dispatched } = await recordSaga(mySaga, {}, state);
expect(dispatched).toEqual([
saveDocument(),
doSomethingElse()
])
})
})
However, for the test 2 where there is a take in between, and of course jest (or its girlfriend jasmine) is yelling at me: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
I know it is because runSaga is waiting for the take(SAVE_DOCUMENT_SUCCESS), but how can I mock that up ?

The answer stdChannel().put({type, payload})
Why ?
Using stdChannel you can dispatch after the first run.
How ?
import stdChannel;
add to the first param in runSaga;
call stdChannel().put(SAVE_DOCUMENT_SUCCESS);
Example
what worked for me
I left the first test as it is the expected final result, but the solution comes on the last 2.
import { runSaga, stdchannel } from 'redux-saga'
let dispatchedActions = [];
let channel;
let fakeStore;
beforeEach(() => {
channel = stdChannel(); // you have to declare the channel to have access to it later
fakeStore = {
channel, // add it to the store in runSaga
getState: () => "initial",
dispatch: (action) => dispatchedActions.push(action),
};
});
afterEach(() => {
global.fetch.mockClear();
});
it("executes getData correctly", async () => {
await runSaga(fakeStore, getData, getAsyncData("test")).toPromise();
expect(global.fetch.mock.calls.length).toEqual(1);
expect(dispatchedActions[0]).toEqual(setData(set_value));
});
it("triggers takeLatest and call getData(), but unfortunately doesn't resolve promise", async () => {
await runSaga(fakeStore, rootSaga)// .toPromise() cannot be used here, as will throw Timeout error
channel.put(getAsyncData("test")); // if remove this line, the next 2 expects() will fail
expect(global.fetch.mock.calls.length).toEqual(1);
// expect(dispatchedActions[1]).toEqual(setData(set_value)); // will fail here, but pass on the next it()
});
it("takes the promised data from test above", () => {
expect(dispatchedActions[1]).toEqual(setData(set_value));
});
this answer (about true code, not tests) helped me

By looking at recordSaga:
export const recordSaga = async (saga, initialAction, state) => {
It seems that you should pass {type: SAVE_DOCUMENT_SUCCESS} as a second argument (i.e initialAction). That should trigger the take effect.

Related

Testing custom hook - not wrapped in act warning

I' trying to test a custom hook but I receive this warning message
console.error node_modules/#testing-library/react-hooks/lib/core/console.js:19
Warning: An update to TestComponent inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser.
This is my custom hook
import { useState, useEffect } from 'react'
import io from 'socket.io-client'
import config from './../../../../config'
const useNotificationsSocket = (user) => {
const [socket, setSocket] = useState(null)
const [numUnreadMessages, setNumUnreadMessages] = useState(0)
const configureSocket = socket => {
socket.on('connect', () => {
const data = {
user: user,
}
socket.emit('user joined', data)
})
socket && socket.on('messages updated', (data) => {
//console.log(data)
setNumUnreadMessages(data.numUnreadMessages)
})
}
useEffect(() => {
const fetchSocket = async () => {
const s = await io(config.nSocket.url, {transports: ['websocket']})
configureSocket(s)
setSocket(s)
}
// Check that user is not an empty object as this causes a crash.
user && user.Id && fetchSocket()
}, [user])
return [socket, numUnreadMessages]
}
export { useNotificationsSocket }
and this is the test
import { renderHook, act } from '#testing-library/react-hooks'
import { useNotificationsSocket } from './../hooks/useNotificationsSocket'
jest.mock('socket.io-client')
describe('useNotificationsSocket', () => {
it('returns a socket and numUnreadMessages', async () => {
const user = { Id: '1' }
const { result } = renderHook(() => useNotificationsSocket(user))
expect(result).not.toBeNull()
})
})
I've tried importing act and wrapping the code in a call to act but however I try to wrap the code I still get a warning and can't figure out how I should use act in this case.
Your hook is asynchronous, so you need to await its response:
describe('useNotificationsSocket', () => {
it('returns a socket and numUnreadMessages', async () => {
const user = { Id: '1' }
const { result } = renderHook(() => useNotificationsSocket(user))
await waitFor(() => expect(result).not.toBeNull())
})
})
Additionally, if you define multiple tests, you may encounter your original error if you fail to unmount the hook. At least this appears to be the behaviour in #testing-library/react v13.3.0. You can solve this by unmounting the hook when your test completes:
describe('useNotificationsSocket', () => {
it('returns a socket and numUnreadMessages', async () => {
const user = { Id: '1' }
const { result, unmount } = renderHook(() => useNotificationsSocket(user))
await waitFor(() => expect(result).not.toBeNull())
unmount()
})
})

Async custom hook from within useEffect

When kept in the component body, the following code works fine. Inside useEffect, it checks the asyncstorage and dispatches an action (the function is longer but other checks/dispatches in the function are of the same kind - check asyncstorage and if value exists, dispatch an action)
useEffect(() => {
const getSettings = async () => {
const aSet = await AsyncStorage.getItem('aSet');
if (aSet) {
dispatch(setASet(true));
}
};
getSettings();
}, [dispatch]);
I'm trying to move it to a custom hook but am having problems. The custom hook is:
const useGetUserSettings = () => {
const dispatch = useDispatch();
useEffect(() => {
const getSettings = async () => {
const aSet = await AsyncStorage.getItem('aSet');
if (aSet) {
dispatch(setASet(true));
}
};
getSettings();
}, [dispatch]);
};
export default useGetUserSettings;
Then in the component where I want to call the above, I do:
import useGetUserSettings from './hooks/useGetUserSettings';
...
const getUserSettings = useGetUserSettings();
...
useEffect(() => {
getUserSettings();
}, [getUserSettings])
It returns an error:
getUserSettings is not a function. (In 'getUserSettings()', 'getUserSettings' is undefined
I've been reading rules of hooks and browsing examples on the internet but I can get it working. I've got ESlint set up so it'd show if there were an invalid path to the hook.
Try the following.
useEffect(() => {
if (!getUserSettings) return;
getUserSettings();
}, [getUserSettings]);
The hook doesn't return anything, so it's not surprising that the return value is undefined ;)

How to use debounce with Vuex?

I am trying to debounce a method within a Vuex action that requires an external API.
// Vuex action:
async load ({ state, commit, dispatch }) {
const params = {
period: state.option.period,
from: state.option.from,
to: state.option.to
}
commit('SET_EVENTS_LOADING', true)
const res = loadDebounced.bind(this)
const data = await res(params)
console.log(data)
commit('SET_EVENTS', data.collection)
commit('SET_PAGINATION', data.pagination)
commit('SET_EVENTS_LOADING', false)
return data
}
// Debounced method
const loadDebounced = () => {
return debounce(async (params) => {
const { data } = await this.$axios.get('events', { params })
return data
}, 3000)
}
The output of the log is:
[Function] {
cancel: [Function]
}
It is not actually executing the debounced function, but returning to me another function.
I would like to present a custom debounce method which you can use in your vuex store as
let ongoingRequest = undefined;
const loadDebounced = () => {
clearTimeout(ongoingRequest);
ongoingRequest = setTimeout(_ => {
axios.get(<<your URL>>).then(({ data }) => data);
}, 3000);
}
This method first ensures to cancel any ongoing setTimeout in the pipeline and then executes it again.
This can be seen in action HERE

redux-thunk: actions are not dispatching

I am trying to build an app in react native that is suppose to take take two inputs by a user and then make a query to an api and get information about the two inputs. I have been having trouble with redux and redux-thunk and specifically with async actions.
This is the code in my app that i am specifically having trouble with
export const fetchData = url => {
console.log("start Fetching");
return async dispatch => { // this is where the problem is
dispatch(fetchingRequest());
try {
const response = await fetch("https://randomuser.me/api/?results=10");
const json = await response.text();
if (response.ok) {
dispatch(fetchingSuccess(json));
console.log("JSON", json);
} else {
console.log("fetch did not resolve");
}
} catch (error) {
dispatch(fetchingFailure(error));
}
};
console.log("Fetched data");
};
Upon debugging the function, I have ended with finding that when the fetchData function is called the function will execute but the async dispatch that is being returned has undefined behavior.
The output in the debugger when the function is called should be
start Fetching
JSON file information/Error
but the output in the debugger is actually
start Fetching
This is the function in which fetchData is called in
_onPress = () => {
let url = "https://randomuser.me/api/?results=10";
fetchData(url);
console.log("should have fetched");
};
this is the mapDispatchToProps function that I have added. The problem is i do not know what to add inside the function.
const mapStatetoDispatch = (url, dispatch) => {
return {dispatch(fetchData(url))}; // do not know what to place in body of function
};
i have connected it in the component with
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
these are the action creators that I import, if needed
import {
fetchingSuccess,
fetchingRequest,
fetchingFailure,
fetchData
} from "../data/redux/actions/appActions.js";
Assuming you have added redux-thunk as a middleware, it looks like the errors are here:
_onPress = () => {
const { fetchData } = this.props;
let url = "https://randomuser.me/api/?results=10";
fetchData(url);
console.log("should have fetched");
};
and
const mapStatetoDispatch = dispatch => ({
fetchData: url => dispatch(fetchData(url)),
}};

Can't use Vue.js Data in Created ()

I'm wondering if is it possible, how can I use Vue.js data in my Created() function.
I'll show some code so you can see why I say.
data (){
return {
model: {},
foo: 'boo'
}
},
created (){
const getModel = () => {
const modelId = this.$route.params.id
axios.get('/api/model', { params: {modelId: modelId}})
.then(res => {
this.model = res.data
this.boo = 'hello'
console.log(this.model)
console.log(this.foo)
})
.catch(err => console.log(err))
}
getModel()
const init = () =>{
console.log(this.model)
console.log(this.foo)
}
init()
The first console.log(foo) returns 'hello'.
The second one (init) returns 'boo'.
Also the first console.log(this.model) is what I expect to get but once is out of the axios method it's like empty again all over the mounted function.
I've tried a lot of things but none of them worked, hope I get a solution... Thanks in advance!
As soon as JS functions are non-blocking - your axios call isn't done (model is still empty) when you call for init
Define init as components method
Call this.init() in axios.get callback
It might have to do with the fact that in your created hook you're creating a function using the function keyword, which means your init function will have its own context(its own this).
A solution to this problem would be to use an arrow function.
data () { return { foo: 'bar' } }
created () {
const init = () => {
console.log(this.foo);
}
init(); // bar
}
More about arrow functions
UPDATE
Actually, the issue stems from not awaiting for getModel. Because you are making a request, you first need to wait for the promise to resolve, and then use its resolved data in the code that depends on it.
The async/await version would be:
async created () {
const getModel = async () => {
const modelId = this.$route.params.id
try {
const res = await axios.get('/api/model', { params: {modelId: modelId}})
this.model = res.data
this.boo = 'hello'
console.log(this.model)
console.log(this.foo)
} catch (err) {
console.error(err)
}
}
const init = () =>{
console.log(this.model)
console.log(this.foo)
}
// An async function always returns a promise
await getModel();
init();
}