Skip to content

Commit 4398337

Browse files
authored
Merge pull request #261 from 3pillarlabs/develop
Merge PR #260
2 parents 61de165 + 620e102 commit 4398337

File tree

5 files changed

+131
-33
lines changed

5 files changed

+131
-33
lines changed

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
version: '3.2'
22
services:
33
web:
4-
image: "hailstorm3/hailstorm-web-client:1.9.10"
4+
image: "hailstorm3/hailstorm-web-client:1.9.11"
55
ports:
66
- "8080:80"
77
networks:

hailstorm-web-client/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

hailstorm-web-client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hailstorm-web-client",
3-
"version": "1.9.10",
3+
"version": "1.9.11",
44
"private": true,
55
"dependencies": {
66
"date-fns": "^2.6.0",

hailstorm-web-client/src/ProjectList/ProjectList.test.tsx

Lines changed: 104 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,57 @@ import { Project, ExecutionCycleStatus } from '../domain';
33
import { mount } from 'enzyme';
44
import { MemoryRouter, Route } from 'react-router-dom';
55
import { ProjectList } from './ProjectList';
6-
import { AppStateContext } from '../appStateContext';
76
import { ProjectService } from "../services/ProjectService";
7+
import { AppStateProviderWithProps } from '../AppStateProvider/AppStateProvider';
8+
import { AppNotificationProviderWithProps } from '../AppNotificationProvider/AppNotificationProvider';
9+
import { AppNotificationContextProps } from '../app-notifications';
10+
import { render } from '@testing-library/react';
811

912
describe('<ProjectList />', () => {
10-
it('should show the loader when projects are being fetched', () => {
11-
jest.spyOn(ProjectService.prototype, "list").mockResolvedValue([]);
12-
const component = mount(
13-
<AppStateContext.Provider value={{appState: {activeProject: undefined, runningProjects: []}, dispatch: jest.fn()}}>
14-
<MemoryRouter>
15-
<ProjectList />
16-
</MemoryRouter>
17-
</AppStateContext.Provider>
13+
14+
function ComponentWrapper({
15+
notifiers,
16+
loadRetryInterval,
17+
maxLoadRetries,
18+
dispatch,
19+
children,
20+
initialEntries
21+
}: React.PropsWithChildren<{
22+
notifiers?: {[K in keyof AppNotificationContextProps]?: AppNotificationContextProps[K]};
23+
loadRetryInterval?: number;
24+
maxLoadRetries?: number;
25+
dispatch?: React.Dispatch<any>;
26+
initialEntries?: Array<any>;
27+
}>) {
28+
const {notifyError, notifyInfo, notifySuccess, notifyWarning} = notifiers || {};
29+
30+
return (
31+
<AppStateProviderWithProps
32+
appState={{activeProject: undefined, runningProjects: []}}
33+
dispatch={dispatch || jest.fn()}
34+
>
35+
<AppNotificationProviderWithProps
36+
notifyError={notifyError || jest.fn()}
37+
notifyInfo={notifyInfo || jest.fn()}
38+
notifySuccess={notifySuccess || jest.fn()}
39+
notifyWarning={notifyWarning || jest.fn()}
40+
>
41+
<MemoryRouter {...{initialEntries}}>
42+
<ProjectList {...{loadRetryInterval, maxLoadRetries}} />
43+
{children}
44+
</MemoryRouter>
45+
</AppNotificationProviderWithProps>
46+
</AppStateProviderWithProps>
1847
);
48+
}
1949

50+
beforeEach(() => {
51+
jest.resetAllMocks();
52+
});
53+
54+
it('should show the loader when projects are being fetched', () => {
55+
jest.spyOn(ProjectService.prototype, "list").mockResolvedValue([]);
56+
const component = mount(<ComponentWrapper />);
2057
expect(component!).toContainExactlyOneMatchingElement('Loader');
2158
});
2259

@@ -60,18 +97,11 @@ describe('<ProjectList />', () => {
6097

6198
const apiSpy = jest.spyOn(ProjectService.prototype, 'list').mockReturnValueOnce(dataPromise);
6299

63-
const component = mount(
64-
<AppStateContext.Provider value={{appState: {activeProject: undefined, runningProjects: []}, dispatch: jest.fn()}}>
65-
<MemoryRouter>
66-
<ProjectList />
67-
</MemoryRouter>
68-
</AppStateContext.Provider>
69-
);
70-
71-
expect(apiSpy).toBeCalled();
100+
const component = mount(<ComponentWrapper />);
72101
await dataPromise;
73102
component.update();
74-
console.debug(component.html());
103+
104+
expect(apiSpy).toBeCalled();
75105
expect(component.find('div.running')).toContainMatchingElements(1, '.is-warning');
76106
expect(component.find('div.justCompleted')).toContainMatchingElements(1, '.is-success');
77107
expect(component.find('div.justCompleted')).toContainMatchingElements(1, '.is-warning');
@@ -86,16 +116,65 @@ describe('<ProjectList />', () => {
86116
);
87117

88118
const component = mount(
89-
<AppStateContext.Provider value={{appState: {activeProject: undefined, runningProjects: []}, dispatch: jest.fn()}}>
90-
<MemoryRouter initialEntries={['/projects']}>
91-
<ProjectList />
92-
<Route exact path="/wizard/projects/new" component={NewProjectWizard} />
93-
</MemoryRouter>
94-
</AppStateContext.Provider>
119+
<ComponentWrapper
120+
initialEntries={['/projects']}
121+
>
122+
<Route exact path="/wizard/projects/new" component={NewProjectWizard} />
123+
</ComponentWrapper>
95124
);
96125

97126
await emptyPromise;
98127
component.update();
99128
expect(component).toContainExactlyOneMatchingElement("#NewProjectWizard");
100129
});
130+
131+
describe('on project list API call error', () => {
132+
133+
it('should retry the call', async (done) => {
134+
const listSpy = jest.spyOn(ProjectService.prototype, 'list').mockRejectedValue('Network error');
135+
render(<ComponentWrapper loadRetryInterval={10} />);
136+
setTimeout(() => {
137+
done();
138+
expect(listSpy.mock.calls.length).toBeGreaterThan(1);
139+
}, 50);
140+
});
141+
142+
it('should notify on eventual failure', (done) => {
143+
jest.spyOn(ProjectService.prototype, 'list').mockRejectedValue("Network error");
144+
const notifyError = jest.fn();
145+
render(<ComponentWrapper notifiers={{notifyError}} loadRetryInterval={10} />);
146+
setTimeout(() => {
147+
done();
148+
expect(notifyError).toHaveBeenCalledTimes(1);
149+
}, 50);
150+
});
151+
152+
it('should be able to eventually succeed', (done) => {
153+
const listSpy = jest
154+
.spyOn(ProjectService.prototype, 'list')
155+
.mockRejectedValueOnce("Network Error")
156+
.mockRejectedValueOnce("Network Error")
157+
.mockResolvedValueOnce([]);
158+
159+
const notifyError = jest.fn();
160+
const notifyWarning = jest.fn();
161+
const dispatch = jest.fn();
162+
render(
163+
<ComponentWrapper
164+
notifiers={{notifyWarning, notifyError}}
165+
loadRetryInterval={10}
166+
maxLoadRetries={3}
167+
{...{dispatch}}
168+
/>
169+
);
170+
171+
setTimeout(() => {
172+
done();
173+
expect(listSpy).toHaveBeenCalledTimes(3);
174+
expect(notifyWarning).toHaveBeenCalledTimes(2);
175+
expect(dispatch).toHaveBeenCalled();
176+
expect(notifyError).not.toHaveBeenCalled();
177+
}, 50);
178+
});
179+
});
101180
});

hailstorm-web-client/src/ProjectList/ProjectList.tsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { ProjectSetupAction } from '../NewProjectWizard/actions';
1313
import { useNotifications } from '../app-notifications';
1414

1515
const RECENT_MINUTES = 60;
16+
const RETRY_INTERVAL_MS = 5000;
17+
const MAX_RETRY_ATTEMPTS = 3;
1618

1719
function runningTime(now: Date, then: Date) {
1820
let diff = differenceInHours(now, then);
@@ -119,30 +121,47 @@ function projectItem(project: Project): JSX.Element {
119121
);
120122
}
121123

122-
export const ProjectList: React.FC = () => {
124+
export const ProjectList: React.FC<{
125+
loadRetryInterval?: number;
126+
maxLoadRetries?: number;
127+
}> = ({
128+
loadRetryInterval = RETRY_INTERVAL_MS,
129+
maxLoadRetries = MAX_RETRY_ATTEMPTS
130+
}) => {
123131
const [loading, setLoading] = useState(true);
124132
const [projects, setProjects] = useState<Project[]>([]);
133+
const [fetchTriesCount, setFetchTriesCount] = useState(0);
125134
const {dispatch} = useContext(AppStateContext);
126-
const {notifyError} = useNotifications();
135+
const {notifyError, notifyWarning, notifyInfo} = useNotifications();
127136

128137
useEffect(() => {
138+
console.debug(`ProjectList#useEffect(fetchTriesCount: ${fetchTriesCount})`);
129139
if (loading) {
130-
console.debug('ProjectList#useEffect');
131140
ApiFactory()
132141
.projects()
133142
.list()
134143
.then((fetchedProjects) => {
135144
setProjects(fetchedProjects);
136145
dispatch(new SetRunningProjectsAction(fetchedProjects.filter((p) => p.running)));
137146
if (fetchedProjects.length === 0) {
147+
notifyInfo(`You have no projects. Start by setting one up.`);
138148
dispatch(new ProjectSetupAction());
139149
}
140150

141151
setLoading(false);
142152
})
143-
.catch((reason) => notifyError(`Failed to fetch project list`, reason));
153+
.catch((reason) => {
154+
if (fetchTriesCount < maxLoadRetries) {
155+
notifyWarning(`Loading projects failed, trying again in a few seconds`);
156+
setTimeout(() => {
157+
setFetchTriesCount(fetchTriesCount + 1);
158+
}, loadRetryInterval);
159+
} else {
160+
notifyError(`Failed to fetch project list`, reason);
161+
}
162+
});
144163
}
145-
}, []);
164+
}, [fetchTriesCount]);
146165

147166
if (loading) return (<Loader size={LoaderSize.APP} />);
148167

0 commit comments

Comments
 (0)