Skip to content
This repository was archived by the owner on Nov 18, 2024. It is now read-only.

Commit fa26555

Browse files
committed
Add error logs for API calls
1 parent 96e1df9 commit fa26555

File tree

6 files changed

+163
-29
lines changed

6 files changed

+163
-29
lines changed

src/api/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type CallApiParams = {
2020
query?: { [key: string]: string };
2121
};
2222

23-
type ErrorResponseType = { error: Error };
23+
export type ErrorResponseType = { error: Error };
2424

2525
type CallApiResponse<SuccessResponseType> =
2626
| SuccessResponseType

src/components/App/index.spec.tsx

Lines changed: 80 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,28 @@ import configureStore from '../../configureStore';
88
import { actions as apiActions } from '../../reducers/api';
99
import { actions as userActions } from '../../reducers/users';
1010
import {
11+
updateWithProps,
1112
createContextWithFakeRouter,
1213
fakeUser,
14+
getFakeLogger,
1315
shallowUntilTarget,
1416
} from '../../test-helpers';
1517
import Navbar from '../Navbar';
18+
import * as api from '../../api';
1619

17-
import App, { AppBase } from '.';
20+
import App, { AppBase, DefaultProps } from '.';
1821

1922
describe(__filename, () => {
2023
type RenderParams = {
21-
store?: Store;
24+
_log?: DefaultProps['_log'];
2225
authToken?: string | null;
26+
store?: Store;
2327
};
2428

25-
const render = ({
26-
store = configureStore(),
29+
const render = async ({
30+
_log,
2731
authToken = 'some-token',
32+
store = configureStore(),
2833
}: RenderParams = {}) => {
2934
const contextWithRouter = createContextWithFakeRouter();
3035
const context = {
@@ -35,71 +40,125 @@ describe(__filename, () => {
3540
},
3641
};
3742

38-
const root = shallowUntilTarget(<App authToken={authToken} />, AppBase, {
43+
const props = {
44+
_log,
45+
authToken,
46+
};
47+
48+
return shallowUntilTarget(<App {...props} />, AppBase, {
3949
shallowOptions: { ...context },
4050
});
41-
42-
return root;
4351
};
4452

45-
it('renders without an authentication token', () => {
46-
const root = render({ authToken: null });
53+
it('renders without an authentication token', async () => {
54+
const root = await render({ authToken: null });
4755

4856
expect(root.find(Container)).toHaveClassName(styles.container);
4957
expect(root.find(`.${styles.content}`)).toHaveLength(1);
5058
expect(root.find(Navbar)).toHaveLength(1);
5159
});
5260

53-
it('renders with an empty authentication token', () => {
54-
const root = render({ authToken: '' });
61+
it('renders with an empty authentication token', async () => {
62+
const root = await render({ authToken: '' });
5563

5664
expect(root.find(Container)).toHaveClassName(styles.container);
5765
expect(root.find(`.${styles.content}`)).toHaveLength(1);
5866
expect(root.find(Navbar)).toHaveLength(1);
5967
});
6068

61-
it('displays a loading message until the user profile gets loaded', () => {
62-
const root = render();
69+
it('displays a loading message until the user profile gets loaded', async () => {
70+
const root = await render();
6371

6472
expect(root).toIncludeText('Getting your workspace ready');
6573
expect(root.find(Navbar)).toHaveLength(0);
6674
});
6775

68-
it('dispatches setAuthToken on mount when authToken is valid', () => {
76+
it('dispatches setAuthToken on mount when authToken is valid', async () => {
6977
const authToken = 'my-token';
7078
const store = configureStore();
7179
const dispatch = jest.spyOn(store, 'dispatch');
7280

73-
render({ authToken, store });
81+
await render({ authToken, store });
7482

7583
expect(dispatch).toHaveBeenCalledWith(
7684
apiActions.setAuthToken({ authToken }),
7785
);
7886
});
7987

80-
it('does not dispatch setAuthToken on mount when authToken is null', () => {
88+
it('does not dispatch setAuthToken on mount when authToken is null', async () => {
8189
const store = configureStore();
8290
const dispatch = jest.spyOn(store, 'dispatch');
8391

84-
render({ authToken: null, store });
92+
await render({ authToken: null, store });
8593

8694
expect(dispatch).not.toHaveBeenCalled();
8795
});
8896

89-
it('configures no route when the user is not logged in', () => {
90-
const root = render({ authToken: null });
97+
it('configures no route when the user is not logged in', async () => {
98+
const root = await render({ authToken: null });
9199

92100
expect(root.find(Route)).toHaveLength(0);
93101
expect(root.find(`.${styles.loginMessage}`)).toIncludeText('Please log in');
94102
});
95103

96-
it('exposes more routes when the user is logged in', () => {
104+
it('exposes more routes when the user is logged in', async () => {
97105
const store = configureStore();
98106
store.dispatch(userActions.loadCurrentUser({ user: fakeUser }));
99107

100-
const root = render({ store });
108+
const root = await render({ store });
101109

102110
expect(root.find(Route)).toHaveLength(3);
103111
expect(root.find(`.${styles.loginMessage}`)).toHaveLength(0);
104112
});
113+
114+
it('dispatches loadCurrentUser() on update', async () => {
115+
const _log = getFakeLogger();
116+
const authToken = 'my-token';
117+
const user = fakeUser;
118+
119+
const store = configureStore();
120+
const dispatch = jest.spyOn(store, 'dispatch');
121+
122+
const mockApi = jest.spyOn(api, 'getCurrentUserProfile');
123+
mockApi.mockReturnValue(Promise.resolve(user));
124+
125+
const root = await render({ _log, authToken: null, store });
126+
127+
// Set the authentication token into the state.
128+
store.dispatch(apiActions.setAuthToken({ authToken }));
129+
const { api: apiState } = store.getState();
130+
dispatch.mockReset();
131+
132+
// Wait until componentDidUpdate() logic has been executed with the new
133+
// props.
134+
await updateWithProps(root, { apiState });
135+
136+
expect(dispatch).toHaveBeenCalledWith(
137+
userActions.loadCurrentUser({
138+
user,
139+
}),
140+
);
141+
});
142+
143+
it('logs an error when the API request to retrieve the current user profile has failed', async () => {
144+
const _log = getFakeLogger();
145+
const authToken = 'my-token';
146+
const store = configureStore();
147+
148+
const error = new Error('server error');
149+
const mockApi = jest.spyOn(api, 'getCurrentUserProfile');
150+
mockApi.mockReturnValue(Promise.resolve({ error }));
151+
152+
const root = await render({ _log, store });
153+
154+
// Set the authentication token into the state.
155+
store.dispatch(apiActions.setAuthToken({ authToken }));
156+
const { api: apiState } = store.getState();
157+
158+
// Wait until componentDidUpdate() logic has been executed with the new
159+
// props.
160+
await updateWithProps(root, { apiState });
161+
162+
expect(_log.error).toHaveBeenCalled();
163+
});
105164
});

src/components/App/index.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
withRouter,
99
} from 'react-router-dom';
1010
import makeClassName from 'classnames';
11+
import log from 'loglevel';
1112

1213
import { ApplicationState, ConnectedReduxProps } from '../../configureStore';
1314
import styles from './styles.module.scss';
@@ -34,14 +35,23 @@ type PropsFromState = {
3435
profile: User | null;
3536
};
3637

38+
export type DefaultProps = {
39+
_log: typeof log;
40+
};
41+
3742
/* eslint-disable @typescript-eslint/indent */
3843
type Props = PublicProps &
3944
PropsFromState &
45+
DefaultProps &
4046
ConnectedReduxProps &
4147
RouteComponentProps<{}>;
4248
/* eslint-enable @typescript-eslint/indent */
4349

4450
export class AppBase extends React.Component<Props> {
51+
static defaultProps = {
52+
_log: log,
53+
};
54+
4555
componentDidMount() {
4656
const { authToken, dispatch } = this.props;
4757

@@ -51,12 +61,14 @@ export class AppBase extends React.Component<Props> {
5161
}
5262

5363
async componentDidUpdate(prevProps: Props) {
54-
const { apiState, dispatch, profile } = this.props;
64+
const { _log, apiState, dispatch, profile } = this.props;
5565

5666
if (!profile && prevProps.apiState.authToken !== apiState.authToken) {
5767
const response = await getCurrentUserProfile(apiState);
5868

59-
if (!isErrorResponse(response)) {
69+
if (isErrorResponse(response)) {
70+
_log.error(`TODO: handle this error response: ${response.error}`);
71+
} else {
6072
dispatch(userActions.loadCurrentUser({ user: response }));
6173
}
6274
}

src/pages/Browse/index.spec.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import * as React from 'react';
2+
import { Store } from 'redux';
23

34
import {
45
createFakeHistory,
56
fakeVersion,
7+
getFakeLogger,
68
shallowUntilTarget,
79
} from '../../test-helpers';
810
import * as api from '../../api';
@@ -13,7 +15,7 @@ import {
1315
} from '../../reducers/versions';
1416
import FileTree from '../../components/FileTree';
1517

16-
import Browse, { BrowseBase } from '.';
18+
import Browse, { BrowseBase, DefaultProps } from '.';
1719

1820
describe(__filename, () => {
1921
const createFakeRouteComponentProps = ({
@@ -35,13 +37,22 @@ describe(__filename, () => {
3537
};
3638
};
3739

40+
type RenderParams = {
41+
_log?: DefaultProps['_log'];
42+
addonId?: string;
43+
versionId?: string;
44+
store?: Store;
45+
};
46+
3847
const render = async ({
48+
_log,
3949
addonId = '999',
4050
versionId = '123',
4151
store = configureStore(),
42-
} = {}) => {
52+
}: RenderParams = {}) => {
4353
const props = {
4454
...createFakeRouteComponentProps({ params: { addonId, versionId } }),
55+
_log,
4556
};
4657

4758
return shallowUntilTarget(<Browse {...props} />, BrowseBase, {
@@ -102,4 +113,16 @@ describe(__filename, () => {
102113
versionActions.loadVersionInfo({ version }),
103114
);
104115
});
116+
117+
it('logs an error when the API request to retrieve the current user profile has failed', async () => {
118+
const _log = getFakeLogger();
119+
const error = new Error('server error');
120+
121+
const mockApi = jest.spyOn(api, 'getVersionFile');
122+
mockApi.mockReturnValue(Promise.resolve({ error }));
123+
124+
await render({ _log });
125+
126+
expect(_log.error).toHaveBeenCalled();
127+
});
105128
});

src/pages/Browse/index.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22
import { RouteComponentProps } from 'react-router-dom';
33
import { connect } from 'react-redux';
44
import { Col } from 'react-bootstrap';
5+
import log from 'loglevel';
56

67
import { ApplicationState, ConnectedReduxProps } from '../../configureStore';
78
import { ApiState } from '../../reducers/api';
@@ -23,15 +24,24 @@ type PropsFromState = {
2324
version: Version;
2425
};
2526

27+
export type DefaultProps = {
28+
_log: typeof log;
29+
};
30+
2631
/* eslint-disable @typescript-eslint/indent */
2732
type Props = RouteComponentProps<PropsFromRouter> &
2833
PropsFromState &
34+
DefaultProps &
2935
ConnectedReduxProps;
3036
/* eslint-enable @typescript-eslint/indent */
3137

3238
export class BrowseBase extends React.Component<Props> {
39+
static defaultProps = {
40+
_log: log,
41+
};
42+
3343
async componentDidMount() {
34-
const { apiState, dispatch, match } = this.props;
44+
const { _log, apiState, dispatch, match } = this.props;
3545
const { addonId, versionId } = match.params;
3646

3747
const response = await getVersionFile({
@@ -40,7 +50,9 @@ export class BrowseBase extends React.Component<Props> {
4050
versionId: parseInt(versionId, 10),
4151
});
4252

43-
if (!isErrorResponse(response)) {
53+
if (isErrorResponse(response)) {
54+
_log.error(`TODO: handle this error response: ${response.error}`);
55+
} else {
4456
dispatch(versionActions.loadVersionInfo({ version: response }));
4557
}
4658
}

src/test-helpers.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import PropTypes from 'prop-types';
22
import { History, Location } from 'history';
3-
import { shallow } from 'enzyme';
3+
import { shallow, ShallowWrapper } from 'enzyme';
4+
import log from 'loglevel';
45

56
import { ExternalUser } from './reducers/users';
67
import {
@@ -197,3 +198,30 @@ export const shallowUntilTarget = (
197198
gave up after ${maxTries} tries`,
198199
);
199200
};
201+
202+
export const getFakeLogger = () => {
203+
return {
204+
...log,
205+
debug: jest.fn(),
206+
error: jest.fn(),
207+
info: jest.fn(),
208+
};
209+
};
210+
211+
export const updateWithProps = (root: ShallowWrapper, props: object) => {
212+
return new Promise((resolve) => {
213+
const instance = root.instance();
214+
const oldComponentDidUpdate = instance.componentDidUpdate;
215+
216+
instance.componentDidUpdate = (...args) => {
217+
if (oldComponentDidUpdate) {
218+
instance.componentDidUpdate = oldComponentDidUpdate;
219+
oldComponentDidUpdate.apply(instance, args);
220+
}
221+
222+
resolve();
223+
};
224+
225+
root.setProps(props);
226+
});
227+
};

0 commit comments

Comments
 (0)