Merge pull request #18 from muselesscreator/integration

feat: Integration testing - round 1
This commit is contained in:
Ben Warzeski
2021-10-26 16:34:30 -04:00
committed by GitHub
15 changed files with 331 additions and 10 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.snap linguist-generated=false

View File

@@ -15,6 +15,7 @@ export const ConfirmModal = ({
<AlertModal
className="confirm-modal"
title={title}
onClose={() => ({})}
isOpen={isOpen}
footerNode={(
<ActionRow>

View File

@@ -29,6 +29,7 @@ export const InfoPopover = ({ children }) => (
src={InfoOutline}
alt="criterion info"
iconAs={Icon}
onClick={() => {}}
/>
</OverlayTrigger>
);

View File

@@ -20,6 +20,7 @@ exports[`ConfirmModal snapshot: closed 1`] = `
</ActionRow>
}
isOpen={false}
onClose={[Function]}
title="test-title"
>
<p>
@@ -48,6 +49,7 @@ exports[`ConfirmModal snapshot: open 1`] = `
</ActionRow>
}
isOpen={true}
onClose={[Function]}
title="test-title"
>
<p>

View File

@@ -21,6 +21,7 @@ exports[`Info Popover Component snapshot 1`] = `
alt="criterion info"
className="criteria-help-icon"
iconAs={[MockFunction Icon]}
onClick={[Function]}
src={[MockFunction icons.InfoOutline]}
/>
</OverlayTrigger>

View File

@@ -2,8 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { ArrowBack } from '@edx/paragon/icons';
import { Hyperlink } from '@edx/paragon';
import { ArrowBack, Launch } from '@edx/paragon/icons';
import { Hyperlink, Icon } from '@edx/paragon';
import selectors from 'data/selectors';
import { locationId } from 'data/constants/app';
@@ -15,12 +15,14 @@ import urls from 'data/services/lms/urls';
export const ListViewBreadcrumb = ({ courseId, oraName }) => (
<>
<Hyperlink className="py-4" destination={urls.openResponse(courseId)}>
<ArrowBack className="mr-3" />
<Icon icon={ArrowBack} className="mr-3" />
Back to all open responses
</Hyperlink>
<p className="h3 py-4">
{oraName}
<Hyperlink destination={urls.ora(courseId, locationId)} target="_blank" />
<Hyperlink destination={urls.ora(courseId, locationId)} target="_blank">
<Icon icon={Launch} />
</Hyperlink>
</p>
</>
);

View File

@@ -16,6 +16,11 @@ import {
jest.mock('@edx/paragon', () => ({
Hyperlink: () => 'Hyperlink',
Icon: () => 'Icon',
}));
jest.mock('@edx/paragon/icons', () => ({
ArrowBack: 'icons.ArrowBack',
Launch: 'icons.Launch',
}));
jest.mock('data/selectors', () => ({

View File

@@ -6,8 +6,9 @@ exports[`ListViewBreadcrumb component component snapshot: empty (no list data) 1
className="py-4"
destination="openResponseUrl(test-course-id)"
>
<SvgArrowBack
<Icon
className="mr-3"
icon="icons.ArrowBack"
/>
Back to all open responses
</Hyperlink>
@@ -18,7 +19,11 @@ exports[`ListViewBreadcrumb component component snapshot: empty (no list data) 1
<Hyperlink
destination="oraUrl(test-course-id, fake-location-id)"
target="_blank"
/>
>
<Icon
icon="icons.Launch"
/>
</Hyperlink>
</p>
</Fragment>
`;

View File

@@ -110,6 +110,7 @@ exports[`ListView component component render tests snapshots snapshot: happy pat
Array [
Object {
"buttonText": "View all responses",
"className": "view-all-responses-btn",
"handleClick": [MockFunction this.handleViewAllResponsesClick],
"variant": "primary",
},

View File

@@ -58,6 +58,7 @@ export class ListView extends React.Component {
selectedBulkAction(selectedFlatRows) {
return {
buttonText: `View selected responses (${selectedFlatRows.length})`,
className: 'view-selected-responses-btn',
handleClick: this.handleViewAllResponsesClick,
variant: 'primary',
};
@@ -86,6 +87,7 @@ export class ListView extends React.Component {
{
buttonText: 'View all responses',
handleClick: this.handleViewAllResponsesClick,
className: 'view-all-responses-btn',
variant: 'primary',
},
]}

View File

@@ -100,7 +100,7 @@ const app = createReducer(initialState, {
[actions.grading.preloadPrev]: (state, { payload }) => ({ ...state, prev: payload }),
[actions.grading.loadNext]: (state, { payload }) => ({
...state,
prev: state.current,
prev: { response: state.current.response },
current: { response: state.next.response, ...payload },
activeIndex: state.activeIndex + 1,
gradeData: {
@@ -111,7 +111,7 @@ const app = createReducer(initialState, {
}),
[actions.grading.loadPrev]: (state, { payload }) => ({
...state,
next: state.current,
next: { response: state.current.response },
current: { response: state.prev.response, ...payload },
gradeData: {
...state.gradeData,

View File

@@ -140,7 +140,21 @@ export const startGrading = () => (dispatch, getState) => {
};
/**
* Stops the grading process for the current submisison
* Cancels the grading process for the current submisison.
* Releases the lock and dispatches stopGrading on success.
*/
export const cancelGrading = () => (dispatch, getState) => {
dispatch(requests.setLock({
value: false,
submissionId: selectors.grading.selected.submissionId(getState()),
onSuccess: () => {
dispatch(module.stopGrading());
},
}));
};
/**
* Stops the grading process for the current submission (local only)
* Clears the local grade data for the current submission and sets grading state
* to False
*/
@@ -154,5 +168,6 @@ export default StrictDict({
loadNext,
loadPrev,
startGrading,
cancelGrading,
stopGrading,
});

View File

@@ -313,9 +313,38 @@ describe('grading thunkActions', () => {
});
});
describe('cancelGrading', () => {
let stopGrading;
beforeAll(() => {
stopGrading = thunkActions.stopGrading;
thunkActions.stopGrading = () => 'stop grading';
});
beforeEach(() => {
getDispatched(thunkActions.cancelGrading());
actionArgs = dispatched.setLock;
});
afterAll(() => {
thunkActions.stopGrading = stopGrading;
});
test('dispatches setLock with selected submissionId and value: false', () => {
expect(actionArgs).not.toEqual(undefined);
expect(actionArgs.value).toEqual(false);
expect(actionArgs.submissionId).toEqual(selectors.grading.selected.submissionId(testState));
});
describe('onSuccess', () => {
beforeEach(() => {
dispatch.mockClear();
});
test('dispatches stopGrading thunkAction', () => {
actionArgs.onSuccess();
expect(dispatch.mock.calls).toContainEqual([thunkActions.stopGrading()]);
});
});
});
describe('stopGrading', () => {
it('dispatches grading.clearGrade and app.setGrading(false)', () => {
thunkActions.stopGrading()(dispatch);
thunkActions.stopGrading()(dispatch, getState);
expect(dispatch.mock.calls).toEqual([
[actions.grading.clearGrade()],
[actions.app.setGrading(false)],

249
src/test/app.test.jsx Normal file
View File

@@ -0,0 +1,249 @@
import React from 'react';
import * as redux from 'redux';
import { Provider } from 'react-redux';
import {
act,
render,
waitFor,
within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import thunk from 'redux-thunk';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import fakeData from 'data/services/lms/fakeData';
import api from 'data/services/lms/api';
import reducers from 'data/reducers';
import { gradingStatuses } from 'data/services/lms/constants';
import messages from 'i18n';
import App from 'App';
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
getLoginRedirectUrl: jest.fn(),
}));
const configureStore = () => redux.createStore(
reducers,
redux.compose(redux.applyMiddleware(thunk)),
);
let el;
let store;
let state;
/**
* Simple wrapper for updating the top-level state variable, that also returns the new value
* @return {obj} - current redux store state
*/
const getState = () => {
state = store.getState();
return state;
};
/** Fake Data for quick access */
const submissionIds = [
fakeData.ids.submissionId(0),
fakeData.ids.submissionId(1),
fakeData.ids.submissionId(2),
fakeData.ids.submissionId(3),
fakeData.ids.submissionId(4),
];
const submissions = submissionIds.map(id => fakeData.mockSubmission(id));
const responses = submissions.map(({ response }) => response);
const statuses = submissionIds.map(id => fakeData.mockSubmissionStatus(id));
const resolveFns = {};
/**
* Mock the api with jest functions that can be tested against.
*/
const mockApi = () => {
api.initializeApp = jest.fn(() => new Promise(
(resolve) => {
resolveFns.initialize = () => resolve({
oraMetadata: fakeData.oraMetadata,
courseMetadata: fakeData.courseMetadata,
submissions: fakeData.submissions,
});
},
));
api.fetchSubmission = jest.fn((submissionId) => new Promise(
(resolve) => resolve(fakeData.mockSubmission(submissionId)),
));
api.fetchSubmissionStatus = jest.fn((submissionId) => new Promise(
(resolve) => resolve(fakeData.mockSubmissionStatus(submissionId)),
));
api.fetchSubmissionResponse = jest.fn((submissionId) => new Promise(
(resolve) => resolve({ response: fakeData.mockSubmission(submissionId).response }),
));
};
/**
* load and configure the store, render the element, and populate the top-level state object
*/
const renderEl = async () => {
store = configureStore();
el = await render(
<IntlProvider locale="en" messages={messages.en}>
<Provider store={store}>
<App />
</Provider>
</IntlProvider>,
);
getState();
};
/**
* resolve the initalization promise, and update state object
*/
const initialize = async () => {
resolveFns.initialize();
await act(() => el.findByText(fakeData.ids.username(0)));
getState();
};
/**
* Select the first 5 entries in the table and click the "View Selected Responses" button
* Wait for the review page to show and update the top-level state object.
*/
const makeTableSelections = async () => {
const table = el.getByRole('table');
const rows = table.querySelectorAll('tbody tr');
const checkbox = (index) => within(rows.item(index)).getByTitle('Toggle Row Selected');
const clickIndex = (index) => userEvent.click(checkbox(index));
[0, 1, 2, 3, 4].forEach(clickIndex);
userEvent.click(el.container.querySelector('.view-selected-responses-btn'));
await act(() => el.findByText('Show Rubric'));
getState();
};
/**
* Click the "next" button in review modal
*/
const clickNext = async () => {
userEvent.click(el.getByLabelText('Load next submission'));
};
/**
* Click the "next" button in review modal
*/
const clickPrev = async () => {
userEvent.click(el.getByLabelText('Load previous submission'));
};
/**
* Wait for the prev and next values to be populated based on the current selection index
*/
const waitForNeighbors = async (currentIndex) => {
await waitFor(
() => {
const { prev, next } = getState().grading;
expect(prev).toEqual(
(currentIndex > 0) ? { response: responses[currentIndex - 1] } : null,
);
expect(next).toEqual(
(currentIndex < 4) ? { response: responses[currentIndex + 1] } : null,
);
},
);
};
/**
* Wait for neighbors, and then verify prev, current, and next grading fields have the appropriate
* data. Also ensure that the app is "grading" iff the "current" response's lockStatus is inProgress.
*/
const checkLoadedResponses = async (currentIndex) => {
await waitForNeighbors(currentIndex);
const { prev, current, next } = state.grading;
expect({ prev, current, next }).toEqual({
prev: currentIndex > 0 ? ({ response: responses[currentIndex - 1] }) : null,
current: {
submissionId: submissionIds[currentIndex],
response: submissions[currentIndex].response,
...statuses[currentIndex],
},
next: currentIndex < 4 ? ({ response: responses[currentIndex + 1] }) : null,
});
expect(state.app.showReview).toEqual(true);
const shouldBeGrading = statuses[currentIndex].lockStatus === gradingStatuses.inProgress;
expect(state.app.isGrading).toEqual(shouldBeGrading);
};
describe('ESG app integration tests', () => {
beforeAll(() => mockApi());
test('initialState', async () => {
await renderEl();
expect(state.app).toEqual(jest.requireActual('data/reducers/app').initialState);
expect(state.submissions).toEqual(
jest.requireActual('data/reducers/submissions').initialState,
);
expect(state.grading).toEqual(jest.requireActual('data/reducers/grading').initialState);
});
test('initialization', async () => {
await renderEl();
await initialize();
expect(state.app.courseMetadata).toEqual(fakeData.courseMetadata);
expect(state.app.oraMetadata).toEqual(fakeData.oraMetadata);
expect(state.submissions.allSubmissions).toEqual(fakeData.submissions);
});
describe('table selection', () => {
beforeAll(async () => {
await renderEl();
await initialize();
await makeTableSelections();
});
it('loads selected submission ids', () => {
expect(state.grading.selected).toEqual(submissionIds);
});
test('app flags, { showReview: true, isGrading: false, showRubric: false }', () => {
expect(state.app.showReview).toEqual(true);
expect(state.app.isGrading).toEqual(false);
expect(state.app.showRubric).toEqual(false);
});
it('loads current submission', () => {
const submissionId = fakeData.ids.submissionId(0);
expect(state.grading.current).toEqual({
submissionId,
...fakeData.mockSubmission(submissionId),
});
});
it('loads response for next submission', () => {
expect(state.grading.next).toEqual({
response: fakeData.mockSubmission(submissionIds[1]).response,
});
});
});
describe('review navigation', () => {
test('loads full submission for current, and response for neighbors when navigating', async () => {
await renderEl();
await initialize();
await makeTableSelections();
await clickNext();
await checkLoadedResponses(1);
await clickNext();
await checkLoadedResponses(2);
await clickPrev();
await checkLoadedResponses(1);
await clickNext();
await checkLoadedResponses(2);
await clickNext();
await checkLoadedResponses(3);
await clickNext();
await checkLoadedResponses(4);
await clickPrev();
await checkLoadedResponses(3);
await clickPrev();
await checkLoadedResponses(2);
await clickPrev();
await checkLoadedResponses(1);
await clickPrev();
await checkLoadedResponses(0);
});
});
});

7
src/test/utils.js Normal file
View File

@@ -0,0 +1,7 @@
export const mockSuccess = (returnValFn) => (...args) => (
new Promise((resolve) => resolve(returnValFn(...args)))
);
export const mockFailure = (returnValFn) => (...args) => (
new Promise((resolve, reject) => reject(returnValFn(...args)))
);