Test an Action with multiple dispatches (Loading, error and success) - reactjs

How could I create a test for an Action with multiple dispatches(Loading, error & success), let see one of my actions:
import axios from 'axios';
import { CompaniesActionTypes } from './types';
export const getCompanies = () => async (dispatch: any) => {
dispatch({
type: CompaniesActionTypes.LOADING
})
try {
const response = await axios.get('app/admin/companies');
dispatch({
type: CompaniesActionTypes.GET_COMPANIES,
payload: response.data
})
} catch (error) {
console.log(error.message);
dispatch({
type: CompaniesActionTypes.ERROR,
payload: 'There was an error while requesting list of companies, please try again later.'
})
}
}
To have more information, below is my reducer for this scenario:
import { CompaniesActionTypes, CompaniesState } from './types';
import { Reducer } from 'redux';
const INITIAL_STATE: CompaniesState = {
data: [],
loading: false,
error: ''
}
export const reducer: Reducer<CompaniesState> = (state = INITIAL_STATE, action) => {
switch (action.type) {
case CompaniesActionTypes.GET_COMPANIES:
return {...state, data: action.payload, loading: false, error: ''}
case CompaniesActionTypes.LOADING:
return {...state, loading: true};
case CompaniesActionTypes.ERROR:
return {...state, error: action.payload, loading: false};
default:
return state
}
}
Note: As you can see I'm using typescript, should not be a problem.
so what I'm trying is:
// Actions
describe('creating actions', () => {
it('should create an action to get companies', () => {
const expectedAction = {
type: CompaniesActionTypes.GET_COMPANIES,
payload: Promise.resolve()
}
expect(actions.getCompanies()).toEqual(expectedAction)
})
})
For the first action test Im getting this error:
Expected: {"payload": {}, "type": "##companies/GET_COMPANIES"}
Received: [Function anonymous]
const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)
describe('actions', () => {
afterEach(() => {
fetchMock.restore()
})
it('creates GET_COMPANIES when fetching companies', () => {
fetchMock.getOnce('app/admin/client/companies', {
body: mock,
headers: { 'content-type': 'application/json' }
})
const expectedActions = [{ type: CompaniesActionTypes.GET_COMPANIES, payload: mock }]
const store = mockStore({})
return store.dispatch(actions.getCompanies()).then(() => {
expect(store.getActions()).toEqual(expectedActions)
})
})
})
For this example I'm having problems with the dispatches:
- Expected
+ Received
Array [
Object {
- "payload": Array [
- Object {
- "id": 1,
- "name": "Company Test 1",
+ "type": "##companies/LOADING_COMPANIES",
},
Object {
- "id": 2,
- "name": "Company Test 2",
- },
- Object {
- "id": 3,
- "name": "Company Test 3",
- },
- ],
- "type": "##companies/GET_COMPANIES",
+ "payload": "There was an error while requesting list of companies, please try again later.",
+ "type": "##companies/ERROR_COMPANIES",
},
]
whats going on with:
"type": "##companies/LOADING_COMPANIES",
"type": "##companies/GET_COMPANIES",
"type": "##companies/ERROR_COMPANIES",
Any idea how to manage this scenario for testing? I guess it's because timing but I have no idea how to implement all the steps

Related

I can't output data from json

I am developing a website on the stack: React, redux, typescript.
I can't output a nested array with data from a data object in JSON
My code:
app.tsx
const App: React.FC = () => {
const {tasks, loading, error} = useTypedSelector(state => state.task)
const dispatch: Dispatch<any> = useDispatch()
useEffect(() => {
dispatch(fetchTasks())
}, [])
if (loading) {
return <h1>Идет загрузка...</h1>
}
if (error) {
return <h1>{error}</h1>
}
return (
<div className="Gant_Container">
<div>
<p className="Project_Period">{Object.values(tasks)[0]} / {Object.values(tasks)[1]}</p>
</div>
<div>
{Object.values(tasks).map((task, id) => {
return (<div key={id}>
{task.id}
{task.title}
{chart.start}
{chart.end}
</div>)
})}
</div>
</div>
);
};
export default Gantt_Container;
store/index.ts
export const store = createStore(rootReducer, applyMiddleware(thunk))
reducers/index.ts
export const rootReducer = combineReducers({
task: taskReducer,
})
export type RootState = ReturnType<typeof rootReducer>
reducers/taskReducer.tsx
const initialState: TaskState = {
tasks: [],
loading: false,
error: null
}
export const taskReducer = (state = initialState, action: TaskAction): TaskState => {
switch (action.type) {
case TaskActionTypes.FETCH_TASKS:
return {loading: true, error: null, tasks: []}
case TaskActionTypes.FETCH_TASKS_SUCCESS:
return {loading: false, error: null, tasks: action.payload}
case TaskActionTypes.FETCH_TASKS_ERROR:
return {loading: false, error: action.payload, tasks: []}
default:
return state
}
}
action-creators/task.ts
export const fetchTasks = () => {
return async (dispatch: Dispatch<TaskAction>) => {
try {
dispatch({type: TaskActionTypes.FETCH_TASKS})
const response = await axios.get("") // The data is coming from the backend, I have hidden the data
dispatch({type: TaskActionTypes.FETCH_TASKS_SUCCESS, payload: response.data})
} catch (e) {
dispatch({
type: TaskActionTypes.FETCH_TASKS_ERROR,
payload: 'Произошла ошибка при загрузке данных'
})
}
}
}
types/task.ts
export interface TaskState {
tasks: any[];
loading: boolean;
error: null | string;
}
export enum TaskActionTypes {
FETCH_TASKS = 'FETCH_TASKS',
FETCH_TASKS_SUCCESS = 'FETCH_TASKS_SUCCESS',
FETCH_TASKS_ERROR = 'FETCH_TASKS_ERROR'
}
interface FetchTasksAction {
type: TaskActionTypes.FETCH_TASKS;
}
interface FetchTasksSuccessAction {
type: TaskActionTypes.FETCH_TASKS_SUCCESS;
payload: any[]
}
interface FetchTasksErrorAction {
type: TaskActionTypes.FETCH_TASKS_ERROR;
payload: string;
}
export type TaskAction = FetchTasksAction | FetchTasksSuccessAction | FetchTasksErrorAction
useTypedSelector.ts
export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector
.json
{
"name": "Project",
"data": "2022",
"task": {
"id": 1,
"title": "Apple",
"start": "2021",
"end": "2022",
"sub": [
{
"id": 2,
"title": "tomato",
"start": "2021",
"end": "2022",
"sub": [
{
"id": 3,
"title": "Orange",
"start": "2019",
"end": "2020",
"sub": [
{
"id": 4,
"title": "Banana",
"start": "2022",
"end": "2022",
"sub": [
{
"id": 5,
"title": "Strawberry",
"start": "2015",
"end": "2018"
},
{
"id": 6,
"title": "cherry",
"period_start": "2001,
"period_end": "2003"
}
]
}
]
}
]
}
]
}
}
Unfortunately I am not able to edit this json file.
I can output all the data before sub, and after I can't output them. I need to output absolutely all the data from json.
I have tried many ways from the internet, but I have not succeeded
This is solution for how you convert nested object into single array, Please use in your code like this:
It will work like recursion function.
const obj = {
name: 'Project',
data: '2022',
task: {
id: 1,
title: 'Apple',
start: '2021',
end: '2022',
sub: [{
id: 2,
title: 'tomato',
start: '2021',
end: '2022',
sub: [{
id: 3,
title: 'Orange',
start: '2019',
end: '2020',
sub: [{
id: 4,
title: 'Banana',
start: '2022',
end: '2022',
sub: [{
id: 5,
title: 'Strawberry',
start: '2015',
end: '2018',
},
{
id: 6,
title: 'cherry',
start: '2001',
end: '2003',
},
],
}, ],
}, ],
}, ],
},
};
const arr = [];
const foo = (task) => {
if (!task.id) return;
arr.push({
id: task.id,
title: task.title,
start: task.start,
end: task.end,
});
if (task.sub && task.sub.length > 0) task.sub.forEach(item => foo(item));
};
foo(obj.task);
console.log('>>>>> arr : ', arr);
I think what you are missing is the user of object.keys:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
It allow you to return a array of keys of your object that you can map on.
What I suggest is doing something like this :
const GanttContainer: React.FC = () => {
const {tasks, loading, error} = useTypedSelector(state => state.task)
const dispatch: Dispatch<any> = useDispatch()
useEffect(() => {
dispatch(fetchTasks())
}, [])
if (loading) {
return <h1>Идет загрузка...</h1>
}
if (error) {
return <h1>{error}</h1>
}
return (
....
{Object.keys(tasks).map((taskKeys, id) => {
return (
<div key={id}>
{tasks[taskKeys]}
</div>)
....
);
};
export default GanttContainer;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Sort data in Axios response and set as useReducer payload

I'm calling data from an api into my react app using axios, like so:
const adapter = axios.create({
baseURL: "http://localhost:4000",
});
const getData = async () => {
const response = await adapter.get("/test-api");
return response.data;
};
This runs in a context, and I have a basic reducer function that I pass to the context:
const initialState = {
loading: true,
error: false,
data: [],
errorMessage: "",
};
const reducer = (state, action) => {
switch (action.type) {
case ACTIONS.FETCH_SUCCESS:
return {
...state,
loading: false,
data: action.payload,
};
case ACTIONS.FETCH_ERROR:
return {
...state,
error: true,
errorMessage: "Error loading data",
};
default:
return state;
}
};
The data I'm returning from my api is shaped like this:
{
"data": [
{
"id": 1,
"name": "Name 1",
"items": [
{
"id": "klqo1gnh",
"name": "Item 1",
"date": "2019-05-12"
}
]
},
{
"id": 2,
"name": "Name 2",
"items": [
{
"id": "klqo2fho",
"name": "Item 1",
"date": "2021-05-05"
},
{
"id": "klro8wip",
"name": "Item 2",
"date": "2012-05-05"
}
]
}
]
}
And I've written a simple function that finds the item whose nested array, items here, has the earliest date, using moment:
const sortDataByDate = (items) => {
return items.sort((first, second) => {
if (moment(first.items.date).isSame(second.items.date)) {
return -1;
} else if (moment(first.items.date).isBefore(second.items.date)) {
return -1;
} else {
return 1;
}
});
};
I then fetch everything in this function:
const fetchData = useCallback(async () => {
try {
await getData().then((response) => {
dispatch({
type: ACTIONS.FETCH_SUCCESS,
payload: response,
});
});
} catch (error) {
dispatch({ type: ACTIONS.FETCH_ERROR });
}
}, []);
I then run fetchData() inside a useEffect within my context:
useEffect(() => {
fetchData();
}, [fetchData]);
All this to say, here's the problem. My sortDataByDate function works sporadically; sometimes the data is ordered correctly, other times it's not. What I'd like to do is fetch my data, sort it with sortDataByDate, and then set the payload with that sorted data, so it's sorted globally rather than on a component level. Inside my App it seems to work consistently, so I think that I have missed something on a context level. Any suggestions?
You need to sort inner items first and get the earliest date:
const sortDataByDate = (items) => {
return items.sort((first, second) => {
if (moment(first.items[0].date).isSame(second.items[0].date)) {
return -1;
} else if (moment(first.items[0].date).isBefore(second.items[0].date)) {
return -1;
} else {
return 1;
}
});
};

How to get separate daily array from the weather API array (For React Redux) which gives a 5 days every 3 hours report

The Openweather API provides 5 days forecast for every 3 hours. We need to display data only for the current day. How can I separate the data for the current day and bind data only for the current day and each hour in that day? Here is the code:
Data looks like this:
{
"data": {
"cod": "200",
"message": 0.0062,
"cnt": 39,
"list": [
{
"dt": 1540177200,
"main": {
"temp": 24.55,
"temp_min": 20.88,
"temp_max": 24.55,
"pressure": 1008.67,
"sea_level": 1025.96,
"grnd_level": 1008.67,
"humidity": 58,
"temp_kf": 3.67
},
"weather": [
{
"id": 800,
"main": "Clear",
"description": "clear sky",
"icon": "02d"
}
],
"clouds": {
"all": 8
},
"wind": {
"speed": 3.82,
"deg": 340.5
},
"sys": {
"pod": "d"
},
"dt_txt": "2018-10-22 03:00:00"
},
{
"dt": 1540188000,
"main": {
...
},
},
{
"dt": 1540198800,
"main": {
...
},
},
{
"dt": 1540587600,
"main": {
. . .
}
}
]
}
Redux Saga
export const fetchWeatherListStart = () => ({
type: ActionTypes.FETCH_WEATHER_START
});
...
import { takeLatest, put } from "redux-saga/effects";
function* fetchWeatherSaga(){
try {
const weatherResponse = yield fetch("https://samples.openweathermap.org/data/2.5/forecast?lat=35&lon=139&appid=439d4b804bc8187953eb36d2a8c26a02")
const weatherlist = yield weatherResponse.json()
yield put(fetchWeatherSuccess(weatherlist));
} catch (error) {
yield put(fetchWeatherError(error.message));
}
}
export default function* watchFetchWeatherSaga(){
yield takeLatest("FETCH_WEATHER_START", fetchWeatherSaga)
}
Reducer
const INITIAL_STATE = {
weatherList: null,
isFetching: false,
errorMessage: undefined
};
const fetchWeatherListReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case "FETCH_WEATHER_START":
return {
...state,
isFetching: true
};
case "FETCH_WEATHER_SUCCESS":
return {
...state,
isFetching: false,
weatherlist: action.payload
};
case "FETCH_WEATHER_ERROR":
return {
...state,
isFetching: false,
errorMessage: action.payload
};
default:
return state;
}
};
export default fetchWeatherReducer;
Component
export const WeatherPage = ({ fetchWeatherListStart}) => {
useEffect(() => {
fetchWeatherListStart();
}, [fetchWeatherListStart]);
...
I thought about using a selector but not sure if it's the best approach... How can be it solved?
You can do it on reducer level, the only thing you need is the current date stamp
case "FETCH_WEATHER_SUCCESS":
// pass today's date along when calling this action
const myDate = action.payload.todayDate
// get only data for today
const filteredList = action.payload.list.filter(item => item.dt === myDate)
return {
...state,
isFetching: false,
weatherlist: filteredList
};

react type error when fetching new array state

I'm trying to convert this code
// this.setState({
// description:'', // resets title after upload
// images: [
// {
// id: newImage[0].id,
// user:{
// username: newImage[0].user.username
// },
// // comments:{
// // comment_body: newImage[0].comments.comment_body
// // },
// image_title: newImage[0].image_title,
// img_url: newImage[0].img_url,
// created_at: new Date().toLocaleString().replace(',', ''),
// updated_at: new Date().toLocaleString().replace(',', '')
// },
// ...this.state.images
// ]
// })
to a redux reducer, here was my attempt
Snippet Reducer
case UPLOAD_IMAGE:
const newState = {...state}
const myImages = newState.images // old images
const newImage = action.newImage
console.log(newImage); // gets the new uploaded image.
return {
images:[
...myImages,
{
id: newImage[0].id,
user:{
username: newImage[0].user.username
},
// comments:{
// comment_body: newImage[0].comments.comment_body
// },
image_title: newImage[0].image_title,
img_url: newImage[0].img_url,
created_at: new Date().toLocaleString().replace(',', ''),
updated_at: new Date().toLocaleString().replace(',', '')
}
]
}
As of right now, images[] are being mapped in the dashboard component, and it fetches images from redux reducer case GET_IMAGES:. The issue comes when uploading an image. I get the error
TypeError: Cannot read property 'length' of undefined
and on refresh, the new uploaded images shows within the map iteration.
My guess is that the old images are contained in an array, and the new images is in an array within an object i guess i don't know.
old images data structure (edited for privacy)
[
{
"id": 217,
"image_title": "ddddddf",
"img_url": "http:/************579/uploads/lshndpj***g",
"created_at": "2019-06-16T17:03:00.605Z",
"updated_at": "2019-06-16T17:03:00.605Z",
"user_id": 1,
"user": {
"id": 1,
"googleId": null,
"username": "E****",
"password": *****$12$16UTTfIH6gXhnRMnBHFGHuHiI***EAGF3GCjO62",
"email": "e****",
"created_at": "2019-06-05T04:50:20.133Z",
"updated_at": "2019-06-05T04:50:20.133Z"
},
"comments": []
},
New uploaded Image data structure(edited for privacy)
{
"0": {
"id": 218,
"image_title": "owl",
"img_url": "http:/*********0705077/uploads******",
"created_at": "2019-06-16T17:11:19.066Z",
"updated_at": "2019-06-16T17:11:19.066Z",
"user_id": 1,
"user": {
"id": 1,
"googleId": null,
"username": "BOB",
"password": "$2b$12******"
"created_at": "2019-06-05T04:50:20.133Z",
"updated_at": "2019-06-05T04:50:20.133Z"
},
"comments": []
},
Maybe there is a discrepancy with the data structure which is why it gives me the type error when looping through the array.
full code
actions
export const uploadImage = data => {
return (dispatch) => {
Axios.post('/images/upload', data).then((response) => {
const newImage = {...response.data}
console.log(newImage);
dispatch({type:UPLOAD_IMAGE, newImage})
}
}
// get images
export const getImages = () => {
return (dispatch) => {
return Axios.get('/images/uploads').then( (response) => {
const data = response.data;
dispatch({
type: GET_IMAGES,
data
})
});
}
}
reducer
import { GET_IMAGES, POST_COMMENT, DELETE_IMAGE, UPLOAD_IMAGE } from '../actions/types';
const initialState = {
images:[],
}
export default (state = initialState, action) => {
switch (action.type) {
case GET_IMAGES:
console.log(action.data);
return{
...state,
images:action.data
}
case UPLOAD_IMAGE:
const newState = {...state}
const myImages = newState.images // old images
const newImage = action.newImage
console.log(newImage); // gets the new uploaded image.
return {
images:[
...myImages,
{
id: newImage[0].id,
user:{
username: newImage[0].user.username
},
// comments:{
// comment_body: newImage[0].comments.comment_body
// },
image_title: newImage[0].image_title,
img_url: newImage[0].img_url,
created_at: new Date().toLocaleString().replace(',', ''),
updated_at: new Date().toLocaleString().replace(',', '')
}
]
}
default:
return state;
}
}
Dashboard.js
const { image} = this.props
{image.images.length > 0 ? (
image.images.map( (img, i) => (
<div key={i}>
<ImageContainer img={img} deleteImg={() => this.deleteImg(img.id)}/>
</div>
))
) : (
<div>
<Grid item md={8}>
<Typography>No Images yet</Typography>
</Grid>
</div>
)}
const mapStateToProps = (state) => ({
image: state.image
})
const mapDispatchToProps = (dispatch) => ({
getImages: () => dispatch(getImages()),
deleteImage : (id) => dispatch(deleteImage (id)),
uploadImage: (data) => dispatch(uploadImage(data))
})
export default connect(mapStateToProps, mapDispatchToProps)(Dashboard)
Whatever you return from a reducer replaces the previous redux state. So in your case, your initial state was something similar to:
{
images: [],
...
}
But in UPLOAD_IMAGE action, your are returning an object with only likes property:
{
likes: ...
}
So the previous state gets replaced by this and your state.image.images returns undefined. To solve this use the spread pattern, assign previous state into the new state first, then assign likes data:
{
...previousState,
likes: ...
}
As I've mentioned in comment that there is issue in your reducer, you can fix it by
case UPDATE_IMAGE:
return {
images: [ ...state.images, action.newImage[0]]
}
with the help from #mezba and #Abhay Sehgal
I found a working solution, thanks folks
case UPLOAD_IMAGE:
const newState = {...state}
const myImages = newState.images // old images
const newImage = action.newImage
console.log(newImage[0]); // gets the new uploaded image.
return {
images:[
{
id: newImage[0].id,
user:{
username: newImage[0].user.username
},
// comments:{
// comment_body: newImage[0].comments.comment_body
// },
image_title: newImage[0].image_title,
img_url: newImage[0].img_url,
created_at: new Date().toLocaleString().replace(',', ''),
updated_at: new Date().toLocaleString().replace(',', '')
},
myImages[0]
]
}

Express Response: Sending an Array as JSON

I'm having an issue trying to get data from my backend express api. I'm using mongodb and mongoose too. Here's my code:
Code:
const show = (req, res) => {
const product = {}
product.array = new Array()
console.log(req.cart.product[1])
for (let i = 0; i < req.cart.product.length; i++) {
Product.find({_id: ObjectId(req.cart.product[i])},function(err,products){
if (err) {
res.sendStatus(500)
} else {
product.array.push(products)
console.log(product.array)
}
})
}
req.cart.product = product.array
res.json({
cart: req.cart.toJSON({ virtuals: true, user: req.user })
})
}
Console.logs:
[ [ { _id: 5952b57ea52d092b8d34c6b0,
name: 'test00000',
price: 0,
description: 'test',
__v: 0 } ] ]
[ [ { _id: 5952b57ea52d092b8d34c6b0,
name: 'test00000',
price: 0,
description: 'test',
__v: 0 } ],
[ { _id: 5952b57ea52d092b8d34c6b0,
name: 'test00000',
price: 0,
description: 'test',
__v: 0 } ] ]
URL Response:
{
"cart": {
"_id": "5953b153d2108941d15a7fe9",
"updatedAt": "2017-06-28T13:38:27.406Z",
"createdAt": "2017-06-28T13:38:27.406Z",
"owner": "595153ad6f18427ef38c416b",
"__v": 0,
"product": [],
"id": "5953b153d2108941d15a7fe9",
"editable": false
}
}
Everything in the console logs is what I want to return in the products array for my response but it won't populate the array when I push it. Any thoughts?
You are trying to call asynchronous code, (e.g. Db query) inside the synchronous code (e.g. for-loop). That's why it returns data to client once it gets the data for the first time. You can async or promise.all to solve the problem.
var async = require('async')
const show = (req, res) => {
const product = {}
product.array = new Array()
console.log(req.cart.product[1])
async.each(req.cart.product, function(id, cb){
Product.find({_id: ObjectId(id)},function(err,products){
if (err) {
cb(err)
} else {
product.array.push(products)
console.log(product.array)
cb()
}
})
}, function(err){
if (err) {
return res.sendStatus(500)
} else {
req.cart.product = product.array
return res.json({
cart: req.cart.toJSON({ virtuals: true, user: req.user })
})
}
})
}
Promise based solution:
const show = (req, res) => {
const product = {}
product.array = new Array()
console.log(req.cart.product[1])
const promises = []
req.cart.product.forEach(function(id){
promises.push(Product.find({_id: ObjectId(req.cart.product[i])}))
})
Promise.all(req.cart.product.map(function(id) {
return Product.find({_id: ObjectId(id)})
})).then(function(products){
req.cart.product = product.array
return res.json({
cart: req.cart.toJSON({ virtuals: true, user: req.user })
})
}).catch(function(err){
return res.sendStatus(500)
})
}

Resources