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",
|
"name": "opinion-ate",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@emotion/react": "^11.10.4",
|
"@emotion/react": "^11.10.4",
|
||||||
"@emotion/styled": "^11.10.4",
|
"@emotion/styled": "^11.10.4",
|
||||||
"@mui/material": "^5.10.7",
|
"@mui/material": "^5.10.7",
|
||||||
@ -670,9 +671,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-proposal-private-property-in-object": {
|
"node_modules/@babel/plugin-proposal-private-property-in-object": {
|
||||||
"version": "7.21.0-placeholder-for-preset-env.2",
|
"version": "7.21.11",
|
||||||
"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",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz",
|
||||||
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
|
"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": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
},
|
},
|
||||||
@ -1915,6 +1923,17 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@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": {
|
"node_modules/@babel/preset-env/node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@emotion/react": "^11.10.4",
|
"@emotion/react": "^11.10.4",
|
||||||
"@emotion/styled": "^11.10.4",
|
"@emotion/styled": "^11.10.4",
|
||||||
"@mui/material": "^5.10.7",
|
"@mui/material": "^5.10.7",
|
||||||
@ -47,4 +48,4 @@
|
|||||||
"redux": "^4.2.0",
|
"redux": "^4.2.0",
|
||||||
"redux-thunk": "^2.4.1"
|
"redux-thunk": "^2.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,7 +4,8 @@ const client = axios.create({
|
|||||||
baseURL: 'https://api.outsidein.dev/Sy0NsAyaS8uuURS1zyCDp3Fzxcpg25iw',
|
baseURL: 'https://api.outsidein.dev/Sy0NsAyaS8uuURS1zyCDp3Fzxcpg25iw',
|
||||||
});
|
});
|
||||||
const api = {
|
const api = {
|
||||||
async loadRestaurants(){
|
async loadRestaurants()
|
||||||
|
{
|
||||||
const response = await client.get('/restaurants');
|
const response = await client.get('/restaurants');
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
@ -1,30 +1,34 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import List from '@mui/material/List';
|
import { CircularProgress, List, ListItem, ListItemText, Alert } from "@mui/material";
|
||||||
import ListItem from '@mui/material/ListItem';
|
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
|
||||||
import { loadRestaurants } from '../store/restaurants/actions';
|
import { loadRestaurants } from '../store/restaurants/actions';
|
||||||
|
|
||||||
export function RestaurantList({ loadRestaurants, restaurants })
|
export function RestaurantList({ loadRestaurants, restaurants, loading, loadError })
|
||||||
{
|
{
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
loadRestaurants();
|
loadRestaurants();
|
||||||
}, [loadRestaurants]);
|
}, [loadRestaurants]);
|
||||||
return (
|
return (
|
||||||
<List>
|
<>
|
||||||
{restaurants.map(restaurant =>
|
{loading && <CircularProgress />}
|
||||||
(
|
{loadError && (<Alert severity="error">Restaurants could not be loaded.</Alert>)}
|
||||||
<ListItem key={restaurant.id}>
|
<List>
|
||||||
<ListItemText>{restaurant.name}</ListItemText>
|
{restaurants.map(restaurant =>
|
||||||
</ListItem>
|
(
|
||||||
))}
|
<ListItem key={restaurant.id}>
|
||||||
</List>
|
<ListItemText>{restaurant.name}</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
restaurants: state.restaurants.records,
|
restaurants: state.restaurants.records,
|
||||||
|
loading: state.restaurants.loading,
|
||||||
|
loadError: state.restaurants.loadError,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = { loadRestaurants };
|
const mapDispatchToProps = { loadRestaurants };
|
||||||
|
|
||||||
|
@ -1,29 +1,64 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { RestaurantList } from './RestaurantList';
|
import { RestaurantList } from './RestaurantList';
|
||||||
|
|
||||||
describe('RestaurantList', () => {
|
let loadRestaurants;
|
||||||
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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
it('loads restaurants on first render', () => {
|
const restaurants = [
|
||||||
renderComponent();
|
{ 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();
|
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();
|
renderComponent();
|
||||||
expect(screen.getByText('Sushi Place')).toBeInTheDocument();
|
expect(screen.getByText('Sushi Place')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Pizza 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 restaurantReducer from './restaurants/reducers';
|
||||||
import { loadRestaurants } from "./restaurants/actions";
|
import { loadRestaurants } from "./restaurants/actions";
|
||||||
|
|
||||||
describe('restaurants', () => {
|
describe('restaurants', () =>
|
||||||
describe('loadRestaurants action', () => {
|
{
|
||||||
it('stores the restaurants', async () => {
|
describe('initially', () =>
|
||||||
const records = [
|
{
|
||||||
{id: 1, name: 'Sushi Place'},
|
let store;
|
||||||
{id: 2, name: 'Pizza Place'}
|
|
||||||
];
|
|
||||||
|
|
||||||
const api = {
|
beforeEach(() =>
|
||||||
loadRestaurants: () => Promise.resolve(records),
|
{
|
||||||
};
|
const initialState = {};
|
||||||
const initialState = {
|
store = createStore(
|
||||||
records: [],
|
|
||||||
};
|
|
||||||
const store = createStore(
|
|
||||||
restaurantReducer,
|
restaurantReducer,
|
||||||
initialState,
|
initialState,
|
||||||
applyMiddleware(
|
applyMiddleware(thunk),
|
||||||
thunk.withExtraArgument(api),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
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 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) => {
|
export const loadRestaurants = () => async (dispatch, getState, api) =>
|
||||||
const records = await api.loadRestaurants();
|
{
|
||||||
dispatch(storeRestaurants(records));
|
try
|
||||||
|
{
|
||||||
|
dispatch(startLoading());
|
||||||
|
const records = await api.loadRestaurants();
|
||||||
|
dispatch(storeRestaurants(records));
|
||||||
|
} catch
|
||||||
|
{
|
||||||
|
dispatch(recordLoadingError())
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startLoading = () => ({ type: START_LOADING })
|
||||||
|
|
||||||
const storeRestaurants = records => ({
|
const storeRestaurants = records => ({
|
||||||
type: STORE_RESTAURANTS,
|
type: STORE_RESTAURANTS,
|
||||||
records
|
records
|
||||||
});
|
});
|
||||||
|
const recordLoadingError = () => ({ type: RECORD_LOADING_ERROR });
|
@ -1,15 +1,45 @@
|
|||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
import { STORE_RESTAURANTS } from './actions';
|
import { RECORD_LOADING_ERROR, STORE_RESTAURANTS, START_LOADING } from './actions';
|
||||||
|
|
||||||
function records(state = [], action) {
|
function records(state = [], action)
|
||||||
switch(action.type) {
|
{
|
||||||
|
switch (action.type)
|
||||||
|
{
|
||||||
case STORE_RESTAURANTS:
|
case STORE_RESTAURANTS:
|
||||||
return action.records;
|
return action.records;
|
||||||
default:
|
default:
|
||||||
return state;
|
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({
|
export default combineReducers({
|
||||||
records,
|
records,
|
||||||
|
loading,
|
||||||
|
loadError,
|
||||||
})
|
})
|
Loading…
Reference in New Issue
Block a user