Skip to content

Commit

Permalink
Merge pull request #261 from 3pillarlabs/develop
Browse files Browse the repository at this point in the history
Merge PR #260
  • Loading branch information
sayantam committed Oct 31, 2021
2 parents 61de165 + 620e102 commit 4398337
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 33 deletions.
2 changes: 1 addition & 1 deletion docker-compose.yml
@@ -1,7 +1,7 @@
version: '3.2'
services:
web:
image: "hailstorm3/hailstorm-web-client:1.9.10"
image: "hailstorm3/hailstorm-web-client:1.9.11"
ports:
- "8080:80"
networks:
Expand Down
2 changes: 1 addition & 1 deletion hailstorm-web-client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion hailstorm-web-client/package.json
@@ -1,6 +1,6 @@
{
"name": "hailstorm-web-client",
"version": "1.9.10",
"version": "1.9.11",
"private": true,
"dependencies": {
"date-fns": "^2.6.0",
Expand Down
129 changes: 104 additions & 25 deletions hailstorm-web-client/src/ProjectList/ProjectList.test.tsx
Expand Up @@ -3,20 +3,57 @@ import { Project, ExecutionCycleStatus } from '../domain';
import { mount } from 'enzyme';
import { MemoryRouter, Route } from 'react-router-dom';
import { ProjectList } from './ProjectList';
import { AppStateContext } from '../appStateContext';
import { ProjectService } from "../services/ProjectService";
import { AppStateProviderWithProps } from '../AppStateProvider/AppStateProvider';
import { AppNotificationProviderWithProps } from '../AppNotificationProvider/AppNotificationProvider';
import { AppNotificationContextProps } from '../app-notifications';
import { render } from '@testing-library/react';

describe('<ProjectList />', () => {
it('should show the loader when projects are being fetched', () => {
jest.spyOn(ProjectService.prototype, "list").mockResolvedValue([]);
const component = mount(
<AppStateContext.Provider value={{appState: {activeProject: undefined, runningProjects: []}, dispatch: jest.fn()}}>
<MemoryRouter>
<ProjectList />
</MemoryRouter>
</AppStateContext.Provider>

function ComponentWrapper({
notifiers,
loadRetryInterval,
maxLoadRetries,
dispatch,
children,
initialEntries
}: React.PropsWithChildren<{
notifiers?: {[K in keyof AppNotificationContextProps]?: AppNotificationContextProps[K]};
loadRetryInterval?: number;
maxLoadRetries?: number;
dispatch?: React.Dispatch<any>;
initialEntries?: Array<any>;
}>) {
const {notifyError, notifyInfo, notifySuccess, notifyWarning} = notifiers || {};

return (
<AppStateProviderWithProps
appState={{activeProject: undefined, runningProjects: []}}
dispatch={dispatch || jest.fn()}
>
<AppNotificationProviderWithProps
notifyError={notifyError || jest.fn()}
notifyInfo={notifyInfo || jest.fn()}
notifySuccess={notifySuccess || jest.fn()}
notifyWarning={notifyWarning || jest.fn()}
>
<MemoryRouter {...{initialEntries}}>
<ProjectList {...{loadRetryInterval, maxLoadRetries}} />
{children}
</MemoryRouter>
</AppNotificationProviderWithProps>
</AppStateProviderWithProps>
);
}

beforeEach(() => {
jest.resetAllMocks();
});

it('should show the loader when projects are being fetched', () => {
jest.spyOn(ProjectService.prototype, "list").mockResolvedValue([]);
const component = mount(<ComponentWrapper />);
expect(component!).toContainExactlyOneMatchingElement('Loader');
});

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

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

const component = mount(
<AppStateContext.Provider value={{appState: {activeProject: undefined, runningProjects: []}, dispatch: jest.fn()}}>
<MemoryRouter>
<ProjectList />
</MemoryRouter>
</AppStateContext.Provider>
);

expect(apiSpy).toBeCalled();
const component = mount(<ComponentWrapper />);
await dataPromise;
component.update();
console.debug(component.html());

expect(apiSpy).toBeCalled();
expect(component.find('div.running')).toContainMatchingElements(1, '.is-warning');
expect(component.find('div.justCompleted')).toContainMatchingElements(1, '.is-success');
expect(component.find('div.justCompleted')).toContainMatchingElements(1, '.is-warning');
Expand All @@ -86,16 +116,65 @@ describe('<ProjectList />', () => {
);

const component = mount(
<AppStateContext.Provider value={{appState: {activeProject: undefined, runningProjects: []}, dispatch: jest.fn()}}>
<MemoryRouter initialEntries={['/projects']}>
<ProjectList />
<Route exact path="/wizard/projects/new" component={NewProjectWizard} />
</MemoryRouter>
</AppStateContext.Provider>
<ComponentWrapper
initialEntries={['/projects']}
>
<Route exact path="/wizard/projects/new" component={NewProjectWizard} />
</ComponentWrapper>
);

await emptyPromise;
component.update();
expect(component).toContainExactlyOneMatchingElement("#NewProjectWizard");
});

describe('on project list API call error', () => {

it('should retry the call', async (done) => {
const listSpy = jest.spyOn(ProjectService.prototype, 'list').mockRejectedValue('Network error');
render(<ComponentWrapper loadRetryInterval={10} />);
setTimeout(() => {
done();
expect(listSpy.mock.calls.length).toBeGreaterThan(1);
}, 50);
});

it('should notify on eventual failure', (done) => {
jest.spyOn(ProjectService.prototype, 'list').mockRejectedValue("Network error");
const notifyError = jest.fn();
render(<ComponentWrapper notifiers={{notifyError}} loadRetryInterval={10} />);
setTimeout(() => {
done();
expect(notifyError).toHaveBeenCalledTimes(1);
}, 50);
});

it('should be able to eventually succeed', (done) => {
const listSpy = jest
.spyOn(ProjectService.prototype, 'list')
.mockRejectedValueOnce("Network Error")
.mockRejectedValueOnce("Network Error")
.mockResolvedValueOnce([]);

const notifyError = jest.fn();
const notifyWarning = jest.fn();
const dispatch = jest.fn();
render(
<ComponentWrapper
notifiers={{notifyWarning, notifyError}}
loadRetryInterval={10}
maxLoadRetries={3}
{...{dispatch}}
/>
);

setTimeout(() => {
done();
expect(listSpy).toHaveBeenCalledTimes(3);
expect(notifyWarning).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenCalled();
expect(notifyError).not.toHaveBeenCalled();
}, 50);
});
});
});
29 changes: 24 additions & 5 deletions hailstorm-web-client/src/ProjectList/ProjectList.tsx
Expand Up @@ -13,6 +13,8 @@ import { ProjectSetupAction } from '../NewProjectWizard/actions';
import { useNotifications } from '../app-notifications';

const RECENT_MINUTES = 60;
const RETRY_INTERVAL_MS = 5000;
const MAX_RETRY_ATTEMPTS = 3;

function runningTime(now: Date, then: Date) {
let diff = differenceInHours(now, then);
Expand Down Expand Up @@ -119,30 +121,47 @@ function projectItem(project: Project): JSX.Element {
);
}

export const ProjectList: React.FC = () => {
export const ProjectList: React.FC<{
loadRetryInterval?: number;
maxLoadRetries?: number;
}> = ({
loadRetryInterval = RETRY_INTERVAL_MS,
maxLoadRetries = MAX_RETRY_ATTEMPTS
}) => {
const [loading, setLoading] = useState(true);
const [projects, setProjects] = useState<Project[]>([]);
const [fetchTriesCount, setFetchTriesCount] = useState(0);
const {dispatch} = useContext(AppStateContext);
const {notifyError} = useNotifications();
const {notifyError, notifyWarning, notifyInfo} = useNotifications();

useEffect(() => {
console.debug(`ProjectList#useEffect(fetchTriesCount: ${fetchTriesCount})`);
if (loading) {
console.debug('ProjectList#useEffect');
ApiFactory()
.projects()
.list()
.then((fetchedProjects) => {
setProjects(fetchedProjects);
dispatch(new SetRunningProjectsAction(fetchedProjects.filter((p) => p.running)));
if (fetchedProjects.length === 0) {
notifyInfo(`You have no projects. Start by setting one up.`);
dispatch(new ProjectSetupAction());
}

setLoading(false);
})
.catch((reason) => notifyError(`Failed to fetch project list`, reason));
.catch((reason) => {
if (fetchTriesCount < maxLoadRetries) {
notifyWarning(`Loading projects failed, trying again in a few seconds`);
setTimeout(() => {
setFetchTriesCount(fetchTriesCount + 1);
}, loadRetryInterval);
} else {
notifyError(`Failed to fetch project list`, reason);
}
});
}
}, []);
}, [fetchTriesCount]);

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

Expand Down

0 comments on commit 4398337

Please sign in to comment.