Merge pull request 'error and loading' (#3) from edge-cases into main
Reviewed-on: #3
This commit is contained in:
commit
85682daf45
25
package-lock.json
generated
25
package-lock.json
generated
@ -8,6 +8,7 @@
|
||||
"name": "opinion-ate",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@mui/material": "^5.10.7",
|
||||
@ -670,9 +671,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-proposal-private-property-in-object": {
|
||||
"version": "7.21.0-placeholder-for-preset-env.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
|
||||
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
|
||||
"version": "7.21.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz",
|
||||
"integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==",
|
||||
"deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.18.6",
|
||||
"@babel/helper-create-class-features-plugin": "^7.21.0",
|
||||
"@babel/helper-plugin-utils": "^7.20.2",
|
||||
"@babel/plugin-syntax-private-property-in-object": "^7.14.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
@ -1915,6 +1923,17 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-private-property-in-object": {
|
||||
"version": "7.21.0-placeholder-for-preset-env.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
|
||||
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/preset-env/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
|
@ -3,6 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@mui/material": "^5.10.7",
|
||||
@ -47,4 +48,4 @@
|
||||
"redux": "^4.2.0",
|
||||
"redux-thunk": "^2.4.1"
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,8 @@ const client = axios.create({
|
||||
baseURL: 'https://api.outsidein.dev/Sy0NsAyaS8uuURS1zyCDp3Fzxcpg25iw',
|
||||
});
|
||||
const api = {
|
||||
async loadRestaurants(){
|
||||
async loadRestaurants()
|
||||
{
|
||||
const response = await client.get('/restaurants');
|
||||
return response.data;
|
||||
},
|
||||
|
@ -1,30 +1,34 @@
|
||||
import { useEffect } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { CircularProgress, List, ListItem, ListItemText, Alert } from "@mui/material";
|
||||
import { loadRestaurants } from '../store/restaurants/actions';
|
||||
|
||||
export function RestaurantList({ loadRestaurants, restaurants })
|
||||
export function RestaurantList({ loadRestaurants, restaurants, loading, loadError })
|
||||
{
|
||||
useEffect(() =>
|
||||
{
|
||||
loadRestaurants();
|
||||
}, [loadRestaurants]);
|
||||
return (
|
||||
<List>
|
||||
{restaurants.map(restaurant =>
|
||||
(
|
||||
<ListItem key={restaurant.id}>
|
||||
<ListItemText>{restaurant.name}</ListItemText>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<>
|
||||
{loading && <CircularProgress />}
|
||||
{loadError && (<Alert severity="error">Restaurants could not be loaded.</Alert>)}
|
||||
<List>
|
||||
{restaurants.map(restaurant =>
|
||||
(
|
||||
<ListItem key={restaurant.id}>
|
||||
<ListItemText>{restaurant.name}</ListItemText>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
restaurants: state.restaurants.records,
|
||||
loading: state.restaurants.loading,
|
||||
loadError: state.restaurants.loadError,
|
||||
});
|
||||
const mapDispatchToProps = { loadRestaurants };
|
||||
|
||||
|
@ -1,29 +1,64 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { RestaurantList } from './RestaurantList';
|
||||
|
||||
describe('RestaurantList', () => {
|
||||
let loadRestaurants;
|
||||
const restaurants = [
|
||||
{id: 1, name: 'Sushi Place'},
|
||||
{id: 2, name: 'Pizza Place'}
|
||||
];
|
||||
function renderComponent(){
|
||||
loadRestaurants = jest.fn().mockName('loadRestaurnats');
|
||||
render(
|
||||
<RestaurantList
|
||||
loadRestaurants={loadRestaurants}
|
||||
restaurants={restaurants}
|
||||
/>
|
||||
)
|
||||
}
|
||||
let loadRestaurants;
|
||||
|
||||
it('loads restaurants on first render', () => {
|
||||
renderComponent();
|
||||
const restaurants = [
|
||||
{ id: 1, name: 'Sushi Place' },
|
||||
{ id: 2, name: 'Pizza Place' }
|
||||
];
|
||||
|
||||
function renderComponent(propsOverride = {})
|
||||
{
|
||||
const props = {
|
||||
loadRestaurants: jest.fn().mockName('loadRestaurnats'),
|
||||
restaurants,
|
||||
loading: false,
|
||||
...propsOverride,
|
||||
};
|
||||
loadRestaurants = props.loadRestaurants;
|
||||
|
||||
render(<RestaurantList {...props} />);
|
||||
}
|
||||
|
||||
describe('When Loading Succeeds', () =>
|
||||
{
|
||||
it('loads restaurants on first render', () =>
|
||||
{
|
||||
renderComponent();
|
||||
expect(loadRestaurants).toHaveBeenCalled();
|
||||
});
|
||||
it('displays the restaurants', () => {
|
||||
|
||||
it('does not display the error message', () =>
|
||||
{
|
||||
renderComponent();
|
||||
expect(screen.queryByText('Restautants could not be loaded.')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays the restaurants', () =>
|
||||
{
|
||||
renderComponent();
|
||||
expect(screen.getByText('Sushi Place')).toBeInTheDocument();
|
||||
expect(screen.getByText('Pizza Place')).toBeInTheDocument();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('displays the loading indicator while loading', () =>
|
||||
{
|
||||
renderComponent({ loading: true });
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not display the loading indicator while not loading', () =>
|
||||
{
|
||||
renderComponent();
|
||||
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
describe('When Loading Fails', () =>
|
||||
{
|
||||
it('displays the error message', () =>
|
||||
{
|
||||
renderComponent({ loadError: true });
|
||||
expect(screen.getByText('Restaurants could not be loaded.'),).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -3,30 +3,125 @@ import thunk from 'redux-thunk';
|
||||
import restaurantReducer from './restaurants/reducers';
|
||||
import { loadRestaurants } from "./restaurants/actions";
|
||||
|
||||
describe('restaurants', () => {
|
||||
describe('loadRestaurants action', () => {
|
||||
it('stores the restaurants', async () => {
|
||||
const records = [
|
||||
{id: 1, name: 'Sushi Place'},
|
||||
{id: 2, name: 'Pizza Place'}
|
||||
];
|
||||
describe('restaurants', () =>
|
||||
{
|
||||
describe('initially', () =>
|
||||
{
|
||||
let store;
|
||||
|
||||
const api = {
|
||||
loadRestaurants: () => Promise.resolve(records),
|
||||
};
|
||||
const initialState = {
|
||||
records: [],
|
||||
};
|
||||
const store = createStore(
|
||||
beforeEach(() =>
|
||||
{
|
||||
const initialState = {};
|
||||
store = createStore(
|
||||
restaurantReducer,
|
||||
initialState,
|
||||
applyMiddleware(
|
||||
thunk.withExtraArgument(api),
|
||||
),
|
||||
applyMiddleware(thunk),
|
||||
);
|
||||
await store.dispatch(loadRestaurants());
|
||||
});
|
||||
it('does not have the loading flag set', () =>
|
||||
{
|
||||
expect(store.getState().loading).toEqual(false);
|
||||
});
|
||||
it('does not have the error flag set', () =>
|
||||
{
|
||||
expect(store.getState().loadError).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('loadRestaurants Action', () =>
|
||||
{
|
||||
describe('When Loading Succeeds', () =>
|
||||
{
|
||||
const records = [
|
||||
{ id: 1, name: 'Sushi Place' },
|
||||
{ id: 2, name: 'Pizza Place' }
|
||||
];
|
||||
let store;
|
||||
|
||||
expect(store.getState().records).toEqual(records)
|
||||
beforeEach(() =>
|
||||
{
|
||||
const api = {
|
||||
loadRestaurants: () => Promise.resolve(records),
|
||||
};
|
||||
const initialState = {
|
||||
records: [],
|
||||
};
|
||||
store = createStore(
|
||||
restaurantReducer,
|
||||
initialState,
|
||||
applyMiddleware(
|
||||
thunk.withExtraArgument(api),
|
||||
),
|
||||
);
|
||||
return store.dispatch(loadRestaurants());
|
||||
})
|
||||
|
||||
it('stores the restaurants', async () =>
|
||||
{
|
||||
expect(store.getState().records).toEqual(records)
|
||||
});
|
||||
it('clears the loading flag', () =>
|
||||
{
|
||||
expect(store.getState().loading).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('While Loading', () =>
|
||||
{
|
||||
let store;
|
||||
|
||||
beforeEach(() =>
|
||||
{
|
||||
const api = {
|
||||
loadRestaurants: () => new Promise(() => { }),
|
||||
}
|
||||
const initialState = { loadError: true };
|
||||
store = createStore(
|
||||
restaurantReducer,
|
||||
initialState,
|
||||
applyMiddleware(
|
||||
thunk.withExtraArgument(api),
|
||||
),
|
||||
);
|
||||
store.dispatch(loadRestaurants());
|
||||
});
|
||||
it('sets a loading flag', () =>
|
||||
{
|
||||
expect(store.getState().loading).toEqual(true);
|
||||
});
|
||||
it('clears the error flag', () =>
|
||||
{
|
||||
expect(store.getState().loadError).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('When Loading Fails', () =>
|
||||
{
|
||||
let store;
|
||||
|
||||
beforeEach(() =>
|
||||
{
|
||||
const api = {
|
||||
loadRestaurants: () => Promise.reject(),
|
||||
};
|
||||
|
||||
const initialState = {};
|
||||
|
||||
store = createStore(
|
||||
restaurantReducer,
|
||||
initialState,
|
||||
applyMiddleware(
|
||||
thunk.withExtraArgument(api),
|
||||
),
|
||||
);
|
||||
return store.dispatch(loadRestaurants());
|
||||
});
|
||||
|
||||
it('sets an error flag', () =>
|
||||
{
|
||||
expect(store.getState().loadError).toEqual(true);
|
||||
});
|
||||
it('clears the loading flag', () =>
|
||||
{
|
||||
expect(store.getState().loading).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,11 +1,24 @@
|
||||
export const STORE_RESTAURANTS = 'STORE_RESTAURANTS';
|
||||
export const START_LOADING = 'START_LOADING';
|
||||
export const RECORD_LOADING_ERROR = 'RECORD_LOADING_ERROR';
|
||||
|
||||
export const loadRestaurants = () => async (dispatch, getState, api) => {
|
||||
const records = await api.loadRestaurants();
|
||||
dispatch(storeRestaurants(records));
|
||||
export const loadRestaurants = () => async (dispatch, getState, api) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
dispatch(startLoading());
|
||||
const records = await api.loadRestaurants();
|
||||
dispatch(storeRestaurants(records));
|
||||
} catch
|
||||
{
|
||||
dispatch(recordLoadingError())
|
||||
}
|
||||
};
|
||||
|
||||
const startLoading = () => ({ type: START_LOADING })
|
||||
|
||||
const storeRestaurants = records => ({
|
||||
type: STORE_RESTAURANTS,
|
||||
records
|
||||
});
|
||||
});
|
||||
const recordLoadingError = () => ({ type: RECORD_LOADING_ERROR });
|
@ -1,15 +1,45 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { STORE_RESTAURANTS } from './actions';
|
||||
import { RECORD_LOADING_ERROR, STORE_RESTAURANTS, START_LOADING } from './actions';
|
||||
|
||||
function records(state = [], action) {
|
||||
switch(action.type) {
|
||||
function records(state = [], action)
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case STORE_RESTAURANTS:
|
||||
return action.records;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
function loading(state = false, action)
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case START_LOADING:
|
||||
return true;
|
||||
case STORE_RESTAURANTS:
|
||||
return false;
|
||||
case RECORD_LOADING_ERROR:
|
||||
return false;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
function loadError(state = false, action)
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case START_LOADING:
|
||||
return false;
|
||||
case RECORD_LOADING_ERROR:
|
||||
return true;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
records,
|
||||
loading,
|
||||
loadError,
|
||||
})
|
Loading…
Reference in New Issue
Block a user