diff --git a/package-lock.json b/package-lock.json
index 27b2104..ee211de 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 7992844..d7a1bf3 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
-}
+}
\ No newline at end of file
diff --git a/src/api.js b/src/api.js
index e3965fd..418f1d9 100644
--- a/src/api.js
+++ b/src/api.js
@@ -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;
},
diff --git a/src/components/RestaurantList.js b/src/components/RestaurantList.js
index 51ec86c..a782617 100644
--- a/src/components/RestaurantList.js
+++ b/src/components/RestaurantList.js
@@ -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 (
-
- {restaurants.map(restaurant =>
- (
-
- {restaurant.name}
-
- ))}
-
+ <>
+ {loading && }
+ {loadError && (Restaurants could not be loaded.)}
+
+ {restaurants.map(restaurant =>
+ (
+
+ {restaurant.name}
+
+ ))}
+
+ >
);
};
const mapStateToProps = state => ({
restaurants: state.restaurants.records,
+ loading: state.restaurants.loading,
+ loadError: state.restaurants.loadError,
});
const mapDispatchToProps = { loadRestaurants };
diff --git a/src/components/RestaurantList.spec.js b/src/components/RestaurantList.spec.js
index 0e74dec..caa7813 100644
--- a/src/components/RestaurantList.spec.js
+++ b/src/components/RestaurantList.spec.js
@@ -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(
-
- )
- }
+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();
+}
+
+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();
- })
-});
\ No newline at end of file
+ });
+
+ 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();
+ });
+});
diff --git a/src/store/restaurants.spec.js b/src/store/restaurants.spec.js
index 47135f3..437f374 100644
--- a/src/store/restaurants.spec.js
+++ b/src/store/restaurants.spec.js
@@ -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);
+ });
});
});
});
\ No newline at end of file
diff --git a/src/store/restaurants/actions.js b/src/store/restaurants/actions.js
index 9da3494..67a8342 100644
--- a/src/store/restaurants/actions.js
+++ b/src/store/restaurants/actions.js
@@ -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
-});
\ No newline at end of file
+});
+const recordLoadingError = () => ({ type: RECORD_LOADING_ERROR });
\ No newline at end of file
diff --git a/src/store/restaurants/reducers.js b/src/store/restaurants/reducers.js
index db8ea93..691699d 100644
--- a/src/store/restaurants/reducers.js
+++ b/src/store/restaurants/reducers.js
@@ -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,
})
\ No newline at end of file