Test add requests redux tests (#24)

Add redux tests as well as get save functionality working.
This commit is contained in:
connorhaugh
2022-03-07 19:43:46 -05:00
committed by GitHub
parent c4cd0c44ce
commit 5258e93972
23 changed files with 158 additions and 105 deletions

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import { shallow } from 'enzyme';
import { Editor, mapDispatchToProps, supportedEditors } from './Editor';
import { thunkActions } from './data/redux';
import * as hooks from './hooks';
@@ -36,9 +36,9 @@ describe('Editor', () => {
() => ({ editorRef: { current: 'ref' }, refReady: true, setEditorRef: jest.fn().mockName('setEditorRef') }),
);
const wrapper = shallow(<Editor blockType={blockType} {...props} />);
if(blockType == 'html'){ // snap just one editor to make viewing easier
if (blockType === 'html') { // snap just one editor to make viewing easier
expect(wrapper).toMatchSnapshot();
};
}
expect(wrapper.children().children().at(1).is(supportedEditors[blockType])).toBe(true);
});
test('presents error message if no relevant editor found and ref ready', () => {

View File

@@ -38,7 +38,7 @@ exports[`EditorFooter snapshots Save Failed, error message raised 1`] = `
"handleSaveClicked": Object {
"editorRef": [MockFunction args.editorRef],
"returnUrl": "hocuspocus.ca",
"saveBlock": [MockFunction args.saveBlock],
"saveBlockContent": [MockFunction args.saveBlock],
},
}
}
@@ -82,7 +82,7 @@ exports[`EditorFooter snapshots not intialized, Spinner appears and button is di
"handleSaveClicked": Object {
"editorRef": [MockFunction args.editorRef],
"returnUrl": "hocuspocus.ca",
"saveBlock": [MockFunction args.saveBlock],
"saveBlockContent": [MockFunction args.saveBlock],
},
}
}
@@ -125,7 +125,7 @@ exports[`EditorFooter snapshots renders as expected with default behavior 1`] =
"handleSaveClicked": Object {
"editorRef": [MockFunction args.editorRef],
"returnUrl": "hocuspocus.ca",
"saveBlock": [MockFunction args.saveBlock],
"saveBlockContent": [MockFunction args.saveBlock],
},
}
}

View File

@@ -27,7 +27,7 @@ export const EditorFooter = ({
isInitialized,
returnUrl,
saveFailed,
saveBlock,
saveBlockContent,
}) => (
<div className="editor-footer mt-auto">
{saveFailed && (
@@ -49,7 +49,7 @@ export const EditorFooter = ({
onClick={module.handleSaveClicked({
editorRef,
returnUrl,
saveBlock,
saveBlockContent,
})}
disabled={!isInitialized}
>
@@ -74,17 +74,18 @@ EditorFooter.propTypes = {
isInitialized: PropTypes.bool.isRequired,
returnUrl: PropTypes.string,
saveFailed: PropTypes.bool.isRequired,
saveBlock: PropTypes.func.isRequired,
saveBlockContent: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
returnUrl: selectors.app.returnUrl(state),
isInitialized: selectors.app.isInitialized(state),
saveFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.saveBlock }),
studioEndpointUrl: selectors.app.studioEndpointUrl(state),
});
export const mapDispatchToProps = {
saveBlock: thunkActions.app.saveBlock,
saveBlockContent: thunkActions.app.saveBlock,
};
export default connect(mapStateToProps, mapDispatchToProps)(EditorFooter);

View File

@@ -9,12 +9,14 @@ jest.mock('../../data/redux', () => ({
thunkActions: {
app: {
saveBlock: jest.fn().mockName('thunkActions.app.saveBlock'),
fetchImages: jest.fn().mockName('thunkActions.app.fetchImages'),
},
},
selectors: {
app: {
isInitialized: jest.fn(state => ({ isInitialized: state })),
studioEndpointUrl: jest.fn(state => ({ studioEndpointUrl: state })),
returnUrl: jest.fn(state => ({ returnUrl: state })),
},
requests: {
isFailed: jest.fn((state, params) => ({ isFailed: { state, params } })),
@@ -41,7 +43,7 @@ describe('EditorFooter', () => {
isInitialized: true,
returnUrl: 'hocuspocus.ca',
saveFailed: false,
saveBlock: jest.fn().mockName('args.saveBlock'),
saveBlockContent: jest.fn().mockName('args.saveBlock'),
};
describe('behavior', () => {
const realmodule = jest.requireActual('./index');
@@ -68,6 +70,11 @@ describe('EditorFooter', () => {
});
describe('mapStateToProps', () => {
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
test('isInitialized from app.returnUrl', () => {
expect(
module.mapStateToProps(testState).returnUrl,
).toEqual(selectors.app.returnUrl(testState));
});
test('isInitialized from app.isInitialized', () => {
expect(
module.mapStateToProps(testState).isInitialized,
@@ -86,7 +93,7 @@ describe('EditorFooter', () => {
});
describe('mapDispatchToProps', () => {
test('saveBlock from thunkActions.app.saveBlock', () => {
expect(module.mapDispatchToProps.saveBlock).toEqual(thunkActions.app.saveBlock);
expect(module.mapDispatchToProps.saveBlockContent).toEqual(thunkActions.app.saveBlock);
});
});
});

View File

@@ -19,7 +19,7 @@ export const EditableHeader = ({
onKeyDown={handleKeyDown}
placeholder="Title"
ref={inputRef}
trailingInputElement={<Icon src={Edit} />}
trailingElement={<Icon src={Edit} />}
value={localTitle}
/>
</Form.Group>

View File

@@ -23,7 +23,7 @@ describe('EditableHeader', () => {
});
test('displays Edit Icon', () => {
const formControl = el.find(Form.Control);
expect(formControl.props().trailingInputElement).toMatchObject(<Icon src={Edit} />);
expect(formControl.props().trailingElement).toMatchObject(<Icon src={Edit} />);
});
});
});

View File

@@ -18,7 +18,6 @@ export const HeaderTitle = ({
typeHeader,
}) => {
if (!isInitialized) { return <FormattedMessage {...messages.loading} />; }
const {
inputRef,
isEditing,

View File

@@ -8,7 +8,7 @@ exports[`EditableHeader snapshot snapshot 1`] = `
onChange={[MockFunction args.handleChange]}
onKeyDown={[MockFunction args.handleKeyDown]}
placeholder="Title"
trailingInputElement={
trailingElement={
<Icon
src={[MockFunction icons.Edit]}
/>

View File

@@ -16,6 +16,9 @@ jest.mock('@tinymce/tinymce-react', () => {
,
};
});
jest.mock('./components/ImageUploadModal', () => 'ImageUploadModal');
jest.mock('./components/SelectImageModal', () => 'SelectImageModal');
jest.mock('./components/ImageSettingsModal', () => 'ImageSettingsModal');
jest.mock('./hooks', () => ({
editorConfig: jest.fn(args => ({ editorConfig: args })),
@@ -27,6 +30,7 @@ jest.mock('../../data/redux', () => ({
actions: {
app: {
initializeEditor: jest.fn().mockName('actions.app.initializeEditor'),
fetchImages: jest.fn().mockName('actions.app.fetchImages'),
},
},
selectors: {

View File

@@ -22,7 +22,7 @@ exports[`TextEditor snapshots block failed to load, Toast is shown 1`] = `
editorConfig={
Object {
"blockValue": Object {
"data": "HYleTsEditTeaxt",
"data": "eDiTablE Text",
},
"initializeEditor": [MockFunction args.intializeEditor],
"openModal": [MockFunction modal.openModal],
@@ -85,7 +85,7 @@ exports[`TextEditor snapshots renders as expected with default behavior 1`] = `
editorConfig={
Object {
"blockValue": Object {
"data": "HYleTsEditTeaxt",
"data": "eDiTablE Text",
},
"initializeEditor": [MockFunction args.intializeEditor],
"openModal": [MockFunction modal.openModal],

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { Button, Image } from '@edx/paragon';
import { thunkActions } from '../../../data/redux';
import { actions } from '../../../data/redux';
import BaseModal from './BaseModal';
import * as module from './SelectImageModal';
@@ -60,7 +60,7 @@ SelectImageModal.propTypes = {
export const mapStateToProps = () => ({});
export const mapDispatchToProps = {
fetchImages: thunkActions.app.fetchImages,
fetchImages: actions.app.fetchImages,
};
export default connect(mapStateToProps, mapDispatchToProps)(SelectImageModal);

View File

@@ -8,7 +8,9 @@ export const addImageUploadButton = (openModal) => (editor) => {
});
};
export const initializeEditorRef = (setRef) => (evt, editor) => { setRef(editor); };
export const initializeEditorRef = (setRef) => (evt, editor) => {
setRef(editor);
};
// for toast onClose to avoid console warnings
export const nullMethod = () => {};
@@ -19,8 +21,8 @@ export const editorConfig = ({
openModal,
initializeEditor,
}) => ({
onInit: () => {
module.initializeEditorRef(setEditorRef);
onInit: (evt, editor) => {
module.initializeEditorRef(setEditorRef)(evt, editor);
initializeEditor();
},
initialValue: blockValue ? blockValue.data.data : '',

View File

@@ -58,11 +58,11 @@ describe('TextEditor hooks', () => {
initializeEditor: jest.fn(),
};
let output;
test('It creates an onInit which calls initializeEditor, but not setEditorRef', () => {
test('It creates an onInit which calls initializeEditor and setEditorRef', () => {
output = module.editorConfig(props);
output.onInit();
expect(props.initializeEditor).toHaveBeenCalled();
expect(props.setEditorRef).not.toHaveBeenCalled();
expect(props.setEditorRef).toHaveBeenCalled();
});
test('It sets the blockvalue to be empty string by default', () => {
output = module.editorConfig(props);

View File

@@ -1,5 +1,3 @@
import { blockTypes } from './app';
export const mockImageData = [
{
displayName: 'shahrukh.jpg',

View File

@@ -29,7 +29,11 @@ const app = createSlice({
blockType: payload.blockType,
}),
setUnitUrl: (state, { payload }) => ({ ...state, unitUrl: payload }),
setBlockValue: (state, { payload }) => ({ ...state, blockValue: payload }),
setBlockValue: (state, { payload }) => ({
...state,
blockValue: payload,
blockTitle: payload.data.display_name,
}),
setBlockContent: (state, { payload }) => ({ ...state, blockContent: payload }),
setBlockTitle: (state, { payload }) => ({ ...state, blockTitle: payload }),
setSaveResponse: (state, { payload }) => ({ ...state, saveResponse: payload }),

View File

@@ -42,7 +42,6 @@ describe('app reducer', () => {
};
[
['setUnitUrl', 'unitUrl'],
['setBlockValue', 'blockValue'],
['setBlockContent', 'blockContent'],
['setBlockTitle', 'blockTitle'],
['setSaveResponse', 'saveResponse'],

View File

@@ -19,6 +19,7 @@ export const simpleSelectors = {
saveResponse: mkSimpleSelector(app => app.saveResponse),
studioEndpointUrl: mkSimpleSelector(app => app.studioEndpointUrl),
unitUrl: mkSimpleSelector(app => app.unitUrl),
blockTitle: mkSimpleSelector(app => app.blockTitle),
};
export const returnUrl = createSelector(

View File

@@ -0,0 +1,49 @@
import { initialState, actions, reducer } from './reducer';
import { RequestStates, RequestKeys } from '../../constants/requests';
describe('requests reducer', () => {
test('intial state generated on create', () => {
expect(reducer(undefined, {})).toEqual(initialState);
});
describe('handling actions', () => {
const arbitraryKey = 'ArbItrAryKey';
const requestsList = [RequestKeys.fetchUnit, RequestKeys.fetchBlock, RequestKeys.saveBlock, arbitraryKey];
requestsList.forEach(requestKey => {
describe(`${requestKey} lifecycle`, () => {
const testAction = (action, args, expected) => {
const testingState = {
...initialState,
arbitraryField: 'arbitrary',
[requestKey]: { arbitrary: 'state' },
};
expect(reducer(testingState, actions[action](args))).toEqual({
...testingState,
[requestKey]: expected,
});
};
test('startRequest sets pending status', () => {
testAction('startRequest', requestKey, { status: RequestStates.pending });
});
test('completeRequest sets completed status and loads response', () => {
testAction(
'completeRequest',
{ requestKey },
{ status: RequestStates.completed },
);
});
test('failRequest sets failed state and loads error', () => {
testAction(
'failRequest',
{ requestKey },
{ status: RequestStates.failed },
);
});
test('clearRequest clears request state', () => {
testAction('clearRequest', { requestKey }, {});
});
});
});
});
});

View File

@@ -82,7 +82,7 @@ export const saveBlock = ({ content, ...rest }) => (dispatch, getState) => {
courseId: selectors.app.courseId(getState()),
content,
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
title: selectors.app.title(getState()),
title: selectors.app.blockTitle(getState()),
}),
...rest,
}));

View File

@@ -1,15 +1,24 @@
import { actions } from '..';
import { RequestKeys } from '../../constants/requests';
import api from '../../services/cms/api';
import * as requests from './requests';
import { actions, selectors } from '../index';
jest.mock('data/services/lms/api', () => ({
initializeApp: (locationId) => ({ initializeApp: locationId }),
fetchSubmissionStatus: (submissionUUID) => ({ fetchSubmissionStatus: submissionUUID }),
fetchSubmission: (submissionUUID) => ({ fetchSubmission: submissionUUID }),
lockSubmission: ({ submissionUUID }) => ({ lockSubmission: { submissionUUID } }),
unlockSubmission: ({ submissionUUID }) => ({ unlockSubmission: { submissionUUID } }),
updateGrade: (submissionUUID, gradeData) => ({ updateGrade: { submissionUUID, gradeData } }),
const testState = {
some: 'data',
};
jest.mock('../app/selectors', () => ({
studioEndpointUrl: (state) => ({ studioEndpointUrl: state }),
blockId: (state) => ({ blockId: state }),
blockType: (state) => ({ blockType: state }),
courseId: (state) => ({ courseId: state }),
blockTitle: (state) => ({ title: state }),
}));
jest.mock('../../services/cms/api', () => ({
fetchBlockById: ({ id, url }) => ({ id, url }),
fetchByUnitId: ({ id, url }) => ({ id, url }),
saveBlock: (args) => args,
}));
let dispatch;
@@ -24,7 +33,7 @@ describe('requests thunkActions module', () => {
describe('networkRequest', () => {
const requestKey = 'test-request';
const testData = { some: 'test data' };
const testData = ({ some: 'test data' });
let resolveFn;
let rejectFn;
beforeEach(() => {
@@ -83,7 +92,7 @@ describe('requests thunkActions module', () => {
}) => {
let dispatchedAction;
beforeEach(() => {
action({ ...args, onSuccess, onFailure })(dispatch);
action({ ...args, onSuccess, onFailure })(dispatch, () => testState);
[[dispatchedAction]] = dispatch.mock.calls;
});
it('dispatches networkRequest', () => {
@@ -101,77 +110,58 @@ describe('requests thunkActions module', () => {
});
});
};
describe('network request actions', () => {
const submissionUUID = 'test-submission-id';
const locationId = 'test-location-id';
const fetchParams = { fetchParam1: 'param1', fetchParam2: 'param2' };
beforeEach(() => {
requests.networkRequest = jest.fn(args => ({ networkRequest: args }));
});
describe('initializeApp', () => {
describe('fetchBlock', () => {
testNetworkRequestAction({
action: requests.initializeApp,
args: { locationId },
expectedString: 'with initialize key, initializeApp promise',
action: requests.fetchBlock,
args: fetchParams,
expectedString: 'with fetchBlock promise',
expectedData: {
requestKey: RequestKeys.initialize,
promise: api.initializeApp(locationId),
...fetchParams,
requestKey: RequestKeys.fetchBlock,
promise: api.fetchBlockById({
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
blockId: selectors.app.blockId(testState),
}),
},
});
});
describe('fetchSubmissionStatus', () => {
describe('fetchUnit', () => {
testNetworkRequestAction({
action: requests.fetchSubmissionStatus,
args: { submissionUUID },
expectedString: 'with fetchSubmissionStatus promise',
action: requests.fetchUnit,
args: fetchParams,
expectedString: 'with fetchUnit promise',
expectedData: {
requestKey: RequestKeys.fetchSubmissionStatus,
promise: api.fetchSubmissionStatus(submissionUUID),
...fetchParams,
requestKey: RequestKeys.fetchUnit,
promise: api.fetchByUnitId({
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
blockId: selectors.app.blockId(testState),
}),
},
});
});
describe('fetchSubmission', () => {
describe('saveBlock', () => {
const content = 'SoME HtMl CoNtent As String';
testNetworkRequestAction({
action: requests.fetchSubmission,
args: { submissionUUID },
expectedString: 'with fetchSubmission promise',
action: requests.saveBlock,
args: { content, some: 'data' },
expectedString: 'with saveBlock promise',
expectedData: {
requestKey: RequestKeys.fetchSubmission,
promise: api.fetchSubmission(submissionUUID),
},
});
});
describe('setLock: true', () => {
testNetworkRequestAction({
action: requests.setLock,
args: { submissionUUID, value: true },
expectedString: 'with setLock promise',
expectedData: {
requestKey: RequestKeys.setLock,
promise: api.lockSubmission(submissionUUID),
},
});
});
describe('setLock: false', () => {
testNetworkRequestAction({
action: requests.setLock,
args: { submissionUUID, value: false },
expectedString: 'with setLock promise',
expectedData: {
requestKey: RequestKeys.setLock,
promise: api.unlockSubmission(submissionUUID),
},
});
});
describe('submitGrade', () => {
const gradeData = 'test-grade-data';
testNetworkRequestAction({
action: requests.submitGrade,
args: { submissionUUID, gradeData },
expectedString: 'with submitGrade promise',
expectedData: {
requestKey: RequestKeys.submitGrade,
promise: api.updateGrade(submissionUUID, gradeData),
...testState,
requestKey: RequestKeys.saveBlock,
promise: api.saveBlock({
blockId: selectors.app.blockId(testState),
blockType: selectors.app.blockType(testState),
courseId: selectors.app.courseId(testState),
content,
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
title: selectors.app.blockTitle(testState),
}),
},
});
});

View File

@@ -1,6 +1,4 @@
import {
fetchBlockById, fetchByUnitId, normalizeContent, saveBlock,
} from './api';
import { apiMethods } from './api';
import * as urls from './urls';
import { get, post } from './utils';
@@ -23,21 +21,21 @@ const title = 'remember this needs to go into metadata to save';
describe('cms api', () => {
describe('fetchBlockId', () => {
it('should call get with url.blocks', () => {
fetchBlockById({ blockId, studioEndpointUrl });
apiMethods.fetchBlockById({ blockId, studioEndpointUrl });
expect(get).toHaveBeenCalledWith(urls.block({ blockId, studioEndpointUrl }));
});
});
describe('fetchByUnitId', () => {
it('should call get with url.blockAncestor', () => {
fetchByUnitId({ blockId, studioEndpointUrl });
apiMethods.fetchByUnitId({ blockId, studioEndpointUrl });
expect(get).toHaveBeenCalledWith(urls.blockAncestor({ studioEndpointUrl, blockId }));
});
});
describe('normalizeContent', () => {
test('return value for blockType: html', () => {
expect(normalizeContent({
expect(apiMethods.normalizeContent({
blockId,
blockType: 'html',
content,
@@ -53,14 +51,14 @@ describe('cms api', () => {
});
});
test('throw error for invalid blockType', () => {
expect(() => { normalizeContent({ blockType: 'somethingINVALID' }); })
expect(() => { apiMethods.normalizeContent({ blockType: 'somethingINVALID' }); })
.toThrow(TypeError);
});
});
describe('saveBlock', () => {
it('should call post with urls.block and normalizeContent', () => {
saveBlock({
apiMethods.saveBlock({
blockId,
blockType: 'html',
content,
@@ -70,7 +68,7 @@ describe('cms api', () => {
});
expect(post).toHaveBeenCalledWith(
urls.block({ studioEndpointUrl }),
normalizeContent({
apiMethods.normalizeContent({
blockType: 'html',
content,
blockId,

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Form } from '@edx/paragon';
// eslint-disable-next-line
import { EditorPage } from '@edx/frontend-lib-content-components';
// eslint-disable-next-line
import { blockTypes } from '@edx/frontend-lib-content-components/editors/data/constants/app';

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line
import 'core-js/stable';
import 'regenerator-runtime/runtime';