Skip to content

Commit 2083818

Browse files
Ben Warzeskileangseu-edx
andauthored
Hook unit tests (#169)
* feat: more integration tests * fix: step progress overflow * chore: assessment hook tests * chore: merge conflict for tests * fix: rebase * fix: rebases * chore: assessment hook docstrings * chore: tests * chore: useConfirmAction hook tests * chore: hooks tests * fix: linting * fix: package-lock.json * fix: tests * chore: useModalActionConfig tests * chore: app component tests * chore: remove logs * fix: lint * chore: update left over test * chore: update linting --------- Co-authored-by: Leangseu Kim <[email protected]>
1 parent 13cf912 commit 2083818

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+3938
-664
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ temp/babel-plugin-react-intl
2121
### Vi ###
2222
**/*.swp
2323
**/*.swo
24+
**/*.swn

jest.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ const config = createConfig('jest', {
99
coveragePathIgnorePatterns: [
1010
'src/setupTest.js',
1111
'src/i18n',
12+
'src/hooks/testHooks', // don't check coverage for jest mocking tools
13+
// 'src/data/services/lms/fakeData', // don't check coverage for mock data
14+
'src/test', // don't check coverage for test integration test utils
1215
],
16+
testTimeout: 120000,
1317
});
1418

1519
config.moduleDirectories = ['node_modules', 'src'];

package-lock.json

Lines changed: 1391 additions & 267 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@
7676
"@edx/browserslist-config": "^1.1.1",
7777
"@edx/frontend-build": "12.9.0-alpha.6",
7878
"@edx/reactifex": "^2.1.1",
79+
"@testing-library/dom": "^9.3.3",
7980
"@testing-library/jest-dom": "5.17.0",
8081
"@testing-library/react": "12.1.5",
82+
"@testing-library/user-event": "^14.5.1",
8183
"glob": "7.2.3",
8284
"husky": "7.0.4",
8385
"jest": "^26.6.3",

src/App.jsx

Lines changed: 45 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,69 @@
11
import React from 'react';
22
import { Routes, Route } from 'react-router-dom';
3-
import {
4-
AuthenticatedPageRoute,
5-
ErrorPage,
6-
} from '@edx/frontend-platform/react';
3+
import { ErrorPage } from '@edx/frontend-platform/react';
74
import { useIntl } from '@edx/frontend-platform/i18n';
8-
import { SkeletonTheme } from '@edx/paragon';
95

106
import AssessmentView from 'views/AssessmentView';
117
import SubmissionView from 'views/SubmissionView';
128
import XBlockView from 'views/XBlockView';
139
import XBlockStudioView from 'views/XBlockStudioView';
1410
import GradeView from 'views/GradeView';
1511

16-
import AppContainer from 'components/AppContainer';
17-
import ModalContainer from 'components/ModalContainer';
12+
import PageRoute from 'components/PageRoute';
1813

19-
import { useRefreshPageData } from 'hooks/app';
20-
import { useUpdateTestProgressKey } from 'hooks/testHooks';
14+
import { useHandleModalCloseEvent } from 'hooks/modal';
2115

2216
import messages from './messages';
2317
import routes from './routes';
2418

2519
const App = () => {
26-
const refreshPageData = useRefreshPageData();
27-
React.useEffect(() => {
28-
window.addEventListener('message', (event) => {
29-
if (event.data.type === 'plugin.modal-close') {
30-
refreshPageData();
31-
}
32-
});
33-
}, [refreshPageData]);
34-
3520
const { formatMessage } = useIntl();
21+
const handleModalClose = useHandleModalCloseEvent();
3622

37-
// test
38-
useUpdateTestProgressKey();
39-
40-
const pageWrapper = (children) => (
41-
<AuthenticatedPageRoute>
42-
<AppContainer>
43-
<SkeletonTheme baseColor="#888" highlightColor="#444">
44-
{children}
45-
</SkeletonTheme>
46-
</AppContainer>
47-
</AuthenticatedPageRoute>
48-
);
49-
const appRoute = (route, Component) => (
50-
<Route
51-
path={route}
52-
key={route}
53-
element={pageWrapper(<Component />)}
54-
/>
55-
);
56-
const modalRoute = (route, Component) => (
57-
<Route
58-
key={route}
59-
path={route}
60-
element={pageWrapper(
61-
<ModalContainer>
62-
<Component />
63-
</ModalContainer>,
64-
)}
65-
/>
66-
);
67-
68-
/*
69-
const embeddedRoutes = [
70-
<Route key="embedXblock" path={routes.xblockEmbed} element={<XBlockView />} />,
71-
<Route key="embedXblockStudio" path={routes.xblockStudioEmbed} element={<XBlockStudioView />} />,
72-
<Route key="embedXblockPreview" path={routes.xblockPreviewEmbed} element={<XBlockView />} />,
73-
modalRoute(routes.peerAssessmentEmbed, PeerAssessmentView, 'ORA Peer Assessment'),
74-
modalRoute(routes.selfAssessmentEmbed, SelfAssessmentView, 'ORA Self Assessment'),
75-
modalRoute(routes.studentTrainingEmbed, StudentTrainingView, 'ORA Student Training'),
76-
modalRoute(routes.submissionEmbed, SubmissionView, 'ORA Submission'),
77-
modalRoute(routes.gradedEmbed, GradeView, 'My Grade'),
78-
<Route
79-
key="embedError"
80-
path={routes.rootEmbed}
81-
element={<ErrorPage message={formatMessage(messages.error404Message)} />}
82-
/>,
83-
];
84-
*/
85-
const baseRoutes = [
86-
appRoute(routes.xblock, XBlockView),
87-
appRoute(routes.xblockStudio, XBlockStudioView),
88-
appRoute(routes.xblockPreview, XBlockView),
89-
modalRoute(routes.peerAssessment, AssessmentView),
90-
modalRoute(routes.selfAssessment, AssessmentView),
91-
modalRoute(routes.studentTraining, AssessmentView),
92-
modalRoute(routes.submission, SubmissionView),
93-
modalRoute(routes.graded, GradeView),
94-
<Route key="error" path={routes.root} element={<ErrorPage message={formatMessage(messages.error404Message)} />} />,
95-
];
23+
React.useEffect(() => {
24+
window.addEventListener('message', handleModalClose);
25+
return () => window.removeEventListener('message', handleModalClose);
26+
}, [handleModalClose]);
9627

9728
return (
9829
<Routes>
99-
{/* embeddedRoutes */}
100-
{baseRoutes}
30+
<Route
31+
path={routes.xblock}
32+
element={(<PageRoute><XBlockView /></PageRoute>)}
33+
/>
34+
<Route
35+
path={routes.xblockStudio}
36+
element={(<PageRoute><XBlockStudioView /></PageRoute>)}
37+
/>
38+
<Route
39+
path={routes.xblockPreview}
40+
element={(<PageRoute><XBlockView /></PageRoute>)}
41+
/>
42+
<Route
43+
path={routes.peerAssessment}
44+
element={(<PageRoute isModal><AssessmentView /></PageRoute>)}
45+
/>
46+
<Route
47+
path={routes.selfAssessment}
48+
element={(<PageRoute isModal><AssessmentView /></PageRoute>)}
49+
/>
50+
<Route
51+
path={routes.studentTraining}
52+
element={(<PageRoute isModal><AssessmentView /></PageRoute>)}
53+
/>
54+
<Route
55+
path={routes.submission}
56+
element={(<PageRoute isModal><SubmissionView /></PageRoute>)}
57+
/>
58+
<Route
59+
path={routes.graded}
60+
element={(<PageRoute isModal><GradeView /></PageRoute>)}
61+
/>
62+
<Route
63+
key="error"
64+
path={routes.root}
65+
element={<ErrorPage message={formatMessage(messages.error404Message)} />}
66+
/>
10167
</Routes>
10268
);
10369
};

src/App.test.jsx

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import React from 'react';
2+
import { when } from 'jest-when';
3+
import { Route, Routes } from 'react-router-dom';
4+
5+
import { ErrorPage } from '@edx/frontend-platform/react';
6+
import { useIntl } from '@edx/frontend-platform/i18n';
7+
import { formatMessage, shallow } from '@edx/react-unit-test-utils';
8+
9+
import AssessmentView from 'views/AssessmentView';
10+
import SubmissionView from 'views/SubmissionView';
11+
import XBlockView from 'views/XBlockView';
12+
import XBlockStudioView from 'views/XBlockStudioView';
13+
import GradeView from 'views/GradeView';
14+
15+
import PageRoute from 'components/PageRoute';
16+
17+
import { useHandleModalCloseEvent } from 'hooks/modal';
18+
19+
import messages from './messages';
20+
import routes from './routes';
21+
22+
import App from './App';
23+
24+
jest.mock('react-router-dom', () => ({
25+
Routes: 'Routes',
26+
Route: 'Route',
27+
}));
28+
29+
jest.mock('@edx/frontend-platform/react', () => ({
30+
AuthenticatedPageRoute: 'AuthenticatedPageRoute',
31+
ErrorPage: 'ErrorPage',
32+
}));
33+
jest.mock('views/AssessmentView', () => 'AssessmentView');
34+
jest.mock('views/SubmissionView', () => 'SubmissionView');
35+
jest.mock('views/XBlockView', () => 'XBlockView');
36+
jest.mock('views/XBlockStudioView', () => 'XBlockStudioView');
37+
jest.mock('views/GradeView', () => 'GradeView');
38+
jest.mock('components/PageRoute', () => 'PageRoute');
39+
40+
jest.mock('hooks/modal', () => ({
41+
useHandleModalCloseEvent: jest.fn(),
42+
}));
43+
44+
const handleModalClose = jest.fn();
45+
when(useHandleModalCloseEvent).calledWith().mockReturnValue(handleModalClose);
46+
const addEventListener = jest.fn();
47+
const removeEventListener = jest.fn();
48+
49+
let el;
50+
describe('App component', () => {
51+
beforeEach(() => {
52+
jest.clearAllMocks();
53+
jest.spyOn(window, 'addEventListener').mockImplementation(addEventListener);
54+
jest.spyOn(window, 'removeEventListener').mockImplementation(removeEventListener);
55+
el = shallow(<App />);
56+
});
57+
describe('behavior', () => {
58+
it('initializes i18n and refresh event from hooks', () => {
59+
expect(useIntl).toHaveBeenCalled();
60+
expect(useHandleModalCloseEvent).toHaveBeenCalled();
61+
});
62+
it('adds handler for modal close event that refreshes page data', () => {
63+
expect(React.useEffect.mock.calls.length).toEqual(1);
64+
const [[effect, prereqs]] = React.useEffect.mock.calls;
65+
expect(prereqs).toEqual([handleModalClose]);
66+
const out = effect();
67+
expect(addEventListener).toHaveBeenCalledWith('message', handleModalClose);
68+
out();
69+
expect(removeEventListener).toHaveBeenCalledWith('message', handleModalClose);
70+
});
71+
});
72+
describe('render', () => {
73+
test('snapshot', () => {
74+
expect(el.snapshot).toMatchSnapshot();
75+
});
76+
const testComponent = (toTest, { route, Component, isModal }) => {
77+
expect(toTest.type).toEqual(Route);
78+
expect(toTest.props.path).toEqual(route);
79+
const { element } = toTest.props;
80+
expect(toTest.props.element.type).toEqual(PageRoute);
81+
if (isModal) {
82+
expect(toTest.props.element.props.isModal).toEqual(true);
83+
}
84+
const expectedElement = shallow(<PageRoute><Component /></PageRoute>);
85+
expect(shallow(element)).toMatchObject(expectedElement);
86+
};
87+
const testAssessmentRoute = (toTest, { route }) => {
88+
testComponent(toTest, { route, Component: AssessmentView, isModal: true });
89+
};
90+
test('route order', () => {
91+
const renderedRoutes = el.instance.findByType(Routes)[0].children;
92+
testComponent(renderedRoutes[0], { route: routes.xblock, Component: XBlockView });
93+
testComponent(renderedRoutes[1], { route: routes.xblockStudio, Component: XBlockStudioView });
94+
testComponent(renderedRoutes[2], { route: routes.xblockPreview, Component: XBlockView });
95+
testAssessmentRoute(renderedRoutes[3], { route: routes.peerAssessment });
96+
testAssessmentRoute(renderedRoutes[4], { route: routes.selfAssessment });
97+
testAssessmentRoute(renderedRoutes[5], { route: routes.studentTraining });
98+
testComponent(renderedRoutes[6], {
99+
route: routes.submission,
100+
Component: SubmissionView,
101+
isModal: true,
102+
});
103+
testComponent(renderedRoutes[7], {
104+
route: routes.graded,
105+
Component: GradeView,
106+
isModal: true,
107+
});
108+
expect(renderedRoutes[8].matches(shallow(
109+
<Route
110+
key="error"
111+
path={routes.root}
112+
element={<ErrorPage message={formatMessage(messages.error404Message)} />}
113+
/>,
114+
)));
115+
});
116+
});
117+
});

src/__snapshots__/App.test.jsx.snap

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`App component render snapshot 1`] = `
4+
<Routes>
5+
<Route
6+
element={
7+
<PageRoute>
8+
<XBlockView />
9+
</PageRoute>
10+
}
11+
path="xblock/:courseId/:xblockId/:progressKey?"
12+
/>
13+
<Route
14+
element={
15+
<PageRoute>
16+
<XBlockStudioView />
17+
</PageRoute>
18+
}
19+
path="xblock_studio/:courseId/:xblockId/:progressKey?"
20+
/>
21+
<Route
22+
element={
23+
<PageRoute>
24+
<XBlockView />
25+
</PageRoute>
26+
}
27+
path="xblock_preview/:courseId/:xblockId/:progressKey?"
28+
/>
29+
<Route
30+
element={
31+
<PageRoute
32+
isModal={true}
33+
>
34+
<AssessmentView />
35+
</PageRoute>
36+
}
37+
path="peer_assessment/:courseId/:xblockId/:progressKey?"
38+
/>
39+
<Route
40+
element={
41+
<PageRoute
42+
isModal={true}
43+
>
44+
<AssessmentView />
45+
</PageRoute>
46+
}
47+
path="self_assessment/:courseId/:xblockId/:progressKey?"
48+
/>
49+
<Route
50+
element={
51+
<PageRoute
52+
isModal={true}
53+
>
54+
<AssessmentView />
55+
</PageRoute>
56+
}
57+
path="student_training/:courseId/:xblockId/:progressKey?"
58+
/>
59+
<Route
60+
element={
61+
<PageRoute
62+
isModal={true}
63+
>
64+
<SubmissionView />
65+
</PageRoute>
66+
}
67+
path="submission/:courseId/:xblockId/:progressKey?"
68+
/>
69+
<Route
70+
element={
71+
<PageRoute
72+
isModal={true}
73+
>
74+
<GradeView />
75+
</PageRoute>
76+
}
77+
path="graded/:courseId/:xblockId/:progressKey?"
78+
/>
79+
<Route
80+
element={
81+
<ErrorPage
82+
message="Page not found"
83+
/>
84+
}
85+
key="error"
86+
path="/*"
87+
/>
88+
</Routes>
89+
`;

0 commit comments

Comments
 (0)