Chore: Test coverage hunt (#36)

* chore: add brand mocking in gallery view

* feat: dev gallery app

* chore: link mock block ids to real block type api

* feat: image settings page features

* chore: more tests

* chore: keystore util and more testing

* chore: more tests

* chore: re-install lint plugin...

* chore: lint fixes

* chore: moar tests

* chore: remove brand from module.config and link gallery to edx.org brand
This commit is contained in:
Ben Warzeski
2022-03-24 11:15:32 -04:00
committed by GitHub
parent 284601d6d2
commit 09e9d865c2
30 changed files with 577 additions and 23773 deletions

2
.gitignore vendored
View File

@@ -108,3 +108,5 @@ dist
*.swo
*.swp
### local overrides ###
module.config.js

23601
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,11 +40,13 @@
"@testing-library/dom": "^8.11.1",
"@testing-library/react": "12.1.1",
"@testing-library/user-event": "^13.5.0",
"axios": "^0.26.1",
"codecov": "3.8.3",
"eslint-import-resolver-alias": "^1.1.2",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.6",
"enzyme-to-json": "^3.6.2",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.25.4",
"husky": "7.0.4",
"prop-types": "15.7.2",
"react": "16.14.0",

View File

@@ -48,7 +48,7 @@ export const TextEditor = ({
// selected image file reference data object.
// this field determines the step of the ImageUploadModal
const [imageSelection, setImageSelection] = selectedImage(null);
const imageSelection = selectedImage(null);
return (
<div className="editor-body h-75">
@@ -56,8 +56,7 @@ export const TextEditor = ({
isOpen={isOpen}
close={closeModal}
editorRef={editorRef}
selection={imageSelection}
setSelection={setImageSelection}
{...imageSelection}
/>
<Toast show={blockFailed} onClose={nullMethod}>
@@ -77,7 +76,8 @@ export const TextEditor = ({
blockValue,
openModal,
initializeEditor,
setSelection: setImageSelection,
setSelection: imageSelection.setSelection,
clearSelection: imageSelection.clearSelection,
})}
/>
)}

View File

@@ -12,21 +12,26 @@ jest.mock('@tinymce/tinymce-react', () => {
return {
__esModule: true,
...originalModule,
Editor: () => 'TiNYmCE EDitOR'
,
Editor: () => 'TiNYmCE EDitOR',
};
});
jest.mock('./components/ImageUploadModal', () => 'ImageUploadModal');
jest.mock('./hooks', () => {
const updateState = jest.fn();
return ({
editorConfig: jest.fn(args => ({ editorConfig: args })),
modalToggle: jest.fn(args => ({ modalToggle: args })),
selectedImage: jest.fn(val => ([{ state: val }, jest.fn((newVal) => updateState({ val, newVal })).mockName('setSelection')])),
nullMethod: jest.fn().mockName('nullMethod'),
});
});
jest.mock('./hooks', () => ({
editorConfig: jest.fn(args => ({ editorConfig: args })),
modalToggle: jest.fn(() => ({
isOpen: true,
openModal: jest.fn().mockName('openModal'),
closeModal: jest.fn().mockName('closeModal'),
})),
selectedImage: jest.fn(() => ({
selection: 'hooks.selectedImage.selection',
setSelection: jest.fn().mockName('hooks.selectedImage.setSelection'),
clearSelection: jest.fn().mockName('hooks.selectedImage.clearSelection'),
})),
nullMethod: jest.fn().mockName('hooks.nullMethod'),
}));
jest.mock('react', () => {
const updateState = jest.fn();

View File

@@ -5,6 +5,7 @@ exports[`TextEditor snapshots block failed to load, Toast is shown 1`] = `
className="editor-body h-75"
>
<ImageUploadModal
clearSelection={[MockFunction hooks.selectedImage.clearSelection]}
close={[MockFunction modal.closeModal]}
editorRef={
Object {
@@ -14,15 +15,11 @@ exports[`TextEditor snapshots block failed to load, Toast is shown 1`] = `
}
}
isOpen={false}
selection={
Object {
"state": null,
}
}
setSelection={[MockFunction setSelection]}
selection="hooks.selectedImage.selection"
setSelection={[MockFunction hooks.selectedImage.setSelection]}
/>
<Toast
onClose={[MockFunction nullMethod]}
onClose={[MockFunction hooks.nullMethod]}
show={true}
>
<FormattedMessage
@@ -39,10 +36,11 @@ exports[`TextEditor snapshots block failed to load, Toast is shown 1`] = `
"some": "eDiTablE Text",
},
},
"clearSelection": [MockFunction hooks.selectedImage.clearSelection],
"initializeEditor": [MockFunction args.intializeEditor],
"openModal": [MockFunction modal.openModal],
"setEditorRef": [MockFunction args.setEditorRef],
"setSelection": [MockFunction setSelection],
"setSelection": [MockFunction hooks.selectedImage.setSelection],
}
}
/>
@@ -54,6 +52,7 @@ exports[`TextEditor snapshots not yet loaded, Spinner appears 1`] = `
className="editor-body h-75"
>
<ImageUploadModal
clearSelection={[MockFunction hooks.selectedImage.clearSelection]}
close={[MockFunction modal.closeModal]}
editorRef={
Object {
@@ -63,15 +62,11 @@ exports[`TextEditor snapshots not yet loaded, Spinner appears 1`] = `
}
}
isOpen={false}
selection={
Object {
"state": null,
}
}
setSelection={[MockFunction setSelection]}
selection="hooks.selectedImage.selection"
setSelection={[MockFunction hooks.selectedImage.setSelection]}
/>
<Toast
onClose={[MockFunction nullMethod]}
onClose={[MockFunction hooks.nullMethod]}
show={false}
>
<FormattedMessage
@@ -97,6 +92,7 @@ exports[`TextEditor snapshots renders as expected with default behavior 1`] = `
className="editor-body h-75"
>
<ImageUploadModal
clearSelection={[MockFunction hooks.selectedImage.clearSelection]}
close={[MockFunction modal.closeModal]}
editorRef={
Object {
@@ -106,15 +102,11 @@ exports[`TextEditor snapshots renders as expected with default behavior 1`] = `
}
}
isOpen={false}
selection={
Object {
"state": null,
}
}
setSelection={[MockFunction setSelection]}
selection="hooks.selectedImage.selection"
setSelection={[MockFunction hooks.selectedImage.setSelection]}
/>
<Toast
onClose={[MockFunction nullMethod]}
onClose={[MockFunction hooks.nullMethod]}
show={false}
>
<FormattedMessage
@@ -131,10 +123,11 @@ exports[`TextEditor snapshots renders as expected with default behavior 1`] = `
"some": "eDiTablE Text",
},
},
"clearSelection": [MockFunction hooks.selectedImage.clearSelection],
"initializeEditor": [MockFunction args.intializeEditor],
"openModal": [MockFunction modal.openModal],
"setEditorRef": [MockFunction args.setEditorRef],
"setSelection": [MockFunction setSelection],
"setSelection": [MockFunction hooks.selectedImage.setSelection],
}
}
/>

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { shallow } from 'enzyme';
import { BaseModal } from './BaseModal';
describe('BaseModal ImageUploadModal template component', () => {
test('snapshot', () => {
const props = {
isOpen: true,
close: jest.fn().mockName('props.close'),
title: 'props.title node',
children: 'props.children node',
confirmAction: 'props.confirmAction node',
};
expect(shallow(<BaseModal {...props} />)).toMatchSnapshot();
});
});

View File

@@ -87,7 +87,7 @@ export const dimensionLockHooks = () => {
const initializeLock = ({ width, height }) => {
// find minimum viable increment
let gcd = findGcd(width, height);
let gcd = module.findGcd(width, height);
if ([width, height].some(v => !Number.isInteger(v / gcd))) {
gcd = 1;
}

View File

@@ -135,6 +135,11 @@ describe('ImageSettingsModal hooks', () => {
hook.initializeLock(multiDims);
expect(state.setState.lockDims).toHaveBeenCalledWith(reducedDims);
});
it('returns the values themselves if they have no gcd', () => {
jest.spyOn(hooks, hookKeys.findGcd).mockReturnValueOnce(2);
hook.initializeLock(simpleDims);
expect(state.setState.lockDims).toHaveBeenCalledWith(simpleDims);
});
});
test('lock sets isLocked to true', () => {
hook = hooks.dimensionLockHooks({ dimensions: simpleDims });
@@ -280,4 +285,12 @@ describe('ImageSettingsModal hooks', () => {
});
});
});
describe('isSaveDisabled', () => {
it('returns true iff is not decorative and altText value is empty', () => {
hook = hooks.isSaveDisabled;
expect(hook({ isDecorative: false, value: '' })).toEqual(true);
expect(hook({ isDecorative: false, value: 'test' })).toEqual(false);
expect(hook({ isDecorative: true, value: '' })).toEqual(false);
});
});
});

View File

@@ -1,44 +1,67 @@
import React from 'react';
import PropTypes from 'prop-types';
import tinyMCEKeys from '../../../data/constants/tinyMCE';
import ImageSettingsModal from './ImageSettingsModal';
import SelectImageModal from './SelectImageModal';
import * as module from './ImageUploadModal';
export const propsString = (props) => Object.keys(props)
.map(key => `${key}="${props[key]}"`)
.join(' ');
export const imgProps = ({ settings, selection }) => ({
src: selection.externalUrl,
alt: settings.isDecorative ? '' : settings.altText,
width: settings.dimensions.width,
height: settings.dimensions.height,
});
export const hooks = {
createSaveCallback: ({
close, editorRef, setSelection, selection,
}) => (settings) => {
editorRef.current.execCommand('mceInsertContent', false, module.hooks.getImgTag({ settings, selection }));
editorRef.current.execCommand(
tinyMCEKeys.commands.insertContent,
false,
module.hooks.imgTag({ settings, selection }),
);
setSelection(null);
close();
},
getImgTag: ({ settings, selection }) => `<img src="${selection.externalUrl}" alt="${settings.isDecorative ? '' : settings.altText}" width="${settings.dimensions.width}" height="${settings.dimensions.height}">`,
onClose: ({ clearSelection, close }) => {
clearSelection();
close();
},
imgTag: ({ settings, selection }) => {
const props = module.imgProps({ settings, selection });
return `<img ${propsString(props)} />`;
},
};
const ImageUploadModal = ({
export const ImageUploadModal = ({
// eslint-disable-next-line
editorRef,
isOpen,
close,
clearSelection,
selection,
setSelection,
}) => {
const saveToEditor = module.hooks.createSaveCallback({
close, editorRef, setSelection, selection,
});
const closeAndReset = () => {
setSelection(null);
close();
};
if (selection) {
return (
<ImageSettingsModal
{...{
isOpen,
close: closeAndReset,
close: module.hooks.onClose({ clearSelection, close }),
selection,
saveToEditor,
returnToSelection: () => setSelection(null),
saveToEditor: module.hooks.createSaveCallback({
close,
editorRef,
selection,
setSelection,
}),
returnToSelection: clearSelection,
}}
/>
);
@@ -51,17 +74,18 @@ ImageUploadModal.defaultProps = {
selection: null,
};
ImageUploadModal.propTypes = {
clearSelection: PropTypes.func.isRequired,
close: PropTypes.func.isRequired,
editorRef: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.any }),
]),
isOpen: PropTypes.bool.isRequired,
selection: PropTypes.shape({
url: PropTypes.string,
externalUrl: PropTypes.string,
altText: PropTypes.bool,
}),
setSelection: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
close: PropTypes.func.isRequired,
editorRef: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.any }),
]),
};
export default ImageUploadModal;

View File

@@ -1,68 +1,122 @@
import React from 'react';
import { shallow } from 'enzyme';
import { keyStore } from '../../../utils';
import tinyMCEKeys from '../../../data/constants/tinyMCE';
import * as module from './ImageUploadModal';
describe('ImageUploadModal hooks', () => {
describe('getImgTag', () => {
const mockSelection = { externalUrl: 'sOmEuRl.cOm' };
let output;
test('It returns a html string which matches an image tag', () => {
const mockSettings = {
altText: 'aLt tExt',
isDecorative: false,
dimensions: {
width: 2022,
height: 1619,
},
jest.mock('./ImageSettingsModal', () => 'ImageSettingsModal');
jest.mock('./SelectImageModal', () => 'SelectImageModal');
const { ImageUploadModal } = module;
const hookKeys = keyStore(module.hooks);
const settings = {
altText: 'aLt tExt',
isDecorative: false,
dimensions: {
width: 2022,
height: 1619,
},
};
describe('ImageUploadModal', () => {
describe('hooks', () => {
describe('imgTag', () => {
const selection = { externalUrl: 'sOmEuRl.cOm' };
const expected = {
src: selection.externalUrl,
alt: settings.altText,
width: settings.dimensions.width,
height: settings.dimensions.height,
};
output = module.hooks.getImgTag({ selection: mockSelection, settings: mockSettings });
expect(output).toEqual(`<img src="${mockSelection.externalUrl}" alt="${mockSettings.altText}" width="${mockSettings.dimensions.width}" height="${mockSettings.dimensions.height}">`);
const testImgTag = (args) => {
const output = module.hooks.imgTag({
settings: args.settings,
selection,
});
expect(output).toEqual(`<img ${module.propsString(args.expected)} />`);
};
test('It returns a html string which matches an image tag', () => {
testImgTag({ settings, expected });
});
test('If isDecorative is true, alt text is an empty string', () => {
testImgTag({
settings: { ...settings, isDecorative: true },
expected: { ...expected, alt: '' },
});
});
});
test('If Is decorative is true, alt text is an empty string', () => {
const mockSettings = {
isDecorative: true,
altText: 'aLt tExt',
dimensions: {
width: 2022,
height: 1619,
},
};
output = module.hooks.getImgTag({ selection: mockSelection, settings: mockSettings });
expect(output).toEqual(`<img src="${mockSelection.externalUrl}" alt="" width="${mockSettings.dimensions.width}" height="${mockSettings.dimensions.height}">`);
describe('createSaveCallback', () => {
const close = jest.fn();
const execCommandMock = jest.fn();
const editorRef = { current: { some: 'dATa', execCommand: execCommandMock } };
const setSelection = jest.fn();
const selection = jest.fn();
let output;
beforeEach(() => {
output = module.hooks.createSaveCallback({
close, editorRef, setSelection, selection,
});
});
afterEach(() => {
jest.clearAllMocks();
});
test('It creates a callback, that when called, inserts to the editor, sets the selection to be null, and calls close', () => {
jest.spyOn(module.hooks, hookKeys.imgTag)
.mockImplementationOnce((props) => ({ selection, settings: props.settings }));
expect(execCommandMock).not.toBeCalled();
expect(setSelection).not.toBeCalled();
expect(close).not.toBeCalled();
output(settings);
expect(execCommandMock).toBeCalledWith(
tinyMCEKeys.commands.insertContent,
false,
{ selection, settings },
);
expect(setSelection).toBeCalledWith(null);
expect(close).toBeCalled();
});
});
describe('onClose', () => {
it('takes and calls clearSelection and close callbacks', () => {
const clearSelection = jest.fn();
const close = jest.fn();
module.hooks.onClose({ clearSelection, close });
expect(clearSelection).toHaveBeenCalled();
expect(close).toHaveBeenCalled();
});
});
});
describe('createSaveCallback', () => {
const close = jest.fn();
const execCommandMock = jest.fn();
const editorRef = { current: { some: 'dATa', execCommand: execCommandMock } };
const setSelection = jest.fn();
const selection = jest.fn();
const mockSettings = {
altText: 'aLt tExt',
isDecorative: false,
dimensions: {
width: 2022,
height: 1619,
},
};
let output;
beforeEach(() => {
output = module.hooks.createSaveCallback({
close, editorRef, setSelection, selection,
});
describe('component', () => {
let props;
let hooks;
beforeAll(() => {
hooks = module.hooks;
props = {
editorRef: { current: null },
isOpen: false,
close: jest.fn().mockName('props.close'),
clearSelection: jest.fn().mockName('props.clearSelection'),
selection: { some: 'images' },
setSelection: jest.fn().mockName('props.setSelection'),
};
module.hooks = {
createSaveCallback: jest.fn().mockName('hooks.createSaveCallback'),
onClose: jest.fn().mockName('hooks.onClose'),
};
});
afterEach(() => {
jest.clearAllMocks();
afterAll(() => {
module.hooks = hooks;
});
test('It creates a callback, that when called, inserts to the editor, sets the selection to be null, and calls close', () => {
jest.spyOn(module.hooks, 'getImgTag').mockImplementationOnce(({ settings }) => ({ selection, settings }));
expect(execCommandMock).not.toBeCalled();
expect(setSelection).not.toBeCalled();
expect(close).not.toBeCalled();
output(mockSettings);
expect(execCommandMock).toBeCalledWith('mceInsertContent', false, { selection, settings: mockSettings });
expect(setSelection).toBeCalledWith(null);
expect(close).toBeCalled();
test('snapshot: with selection content (ImageSettingsUpload)', () => {
expect(shallow(<ImageUploadModal {...props} />)).toMatchSnapshot();
});
test('snapshot: no selection (Select Image Modal)', () => {
expect(shallow(<ImageUploadModal {...props} selection={null} />)).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BaseModal ImageUploadModal template component snapshot 1`] = `
<ModalDialog
hasCloseButton={true}
isFullscreenOnMobile={true}
isOpen={true}
onClose={[MockFunction props.close]}
size="lg"
title="My dialog"
variant="default"
>
<ModalDialog.Header>
<ModalDialog.Title>
props.title node
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
props.children node
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton
onClick={[MockFunction props.close]}
variant="tertiary"
>
Cancel
</ModalDialog.CloseButton>
props.confirmAction node
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
`;

View File

@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ImageUploadModal component snapshot: no selection (Select Image Modal) 1`] = `
<SelectImageModal
close={[MockFunction props.close]}
isOpen={false}
setSelection={[MockFunction props.setSelection]}
/>
`;
exports[`ImageUploadModal component snapshot: with selection content (ImageSettingsUpload) 1`] = `
<ImageSettingsModal
isOpen={false}
returnToSelection={[MockFunction props.clearSelection]}
selection={
Object {
"some": "images",
}
}
/>
`;

View File

@@ -1,6 +1,12 @@
import { useState } from 'react';
import { StrictDict } from '../../utils';
import * as module from './hooks';
import { StrictDict } from '../../utils/index';
export const state = StrictDict({
isModalOpen: (val) => useState(val),
imageSelection: (val) => useState(val),
});
export const openModalWithSelectedImage = (editor, setImage, openModal) => () => {
const imgHTML = editor.selection.getNode();
@@ -96,10 +102,17 @@ export const editorConfig = ({
},
});
export const selectedImage = (val) => useState(val);
export const selectedImage = (val) => {
const [selection, setSelection] = module.state.imageSelection(val);
return {
clearSelection: () => setSelection(null),
selection,
setSelection,
};
};
export const modalToggle = () => {
const [isOpen, setIsOpen] = useState(false);
const [isOpen, setIsOpen] = module.state.isModalOpen(false);
return {
isOpen,
openModal: () => setIsOpen(true),

View File

@@ -1,17 +1,20 @@
import React from 'react';
import * as module from './hooks';
jest.mock('react', () => {
const updateState = jest.fn();
return {
updateState,
useState: jest.fn(val => ([{ state: val }, (newVal) => updateState({ val, newVal })])),
createRef: jest.fn(val => ({ ref: val })),
};
});
import { MockUseState } from '../../../testUtils';
jest.mock('react', () => ({
...jest.requireActual('react'),
createRef: jest.fn(val => ({ ref: val })),
}));
const state = new MockUseState(module);
describe('TextEditor hooks', () => {
describe('Editor Init hooks', () => {
describe('state hooks', () => {
state.testGetter(state.keys.isModalOpen);
state.testGetter(state.keys.imageSelection);
});
const mockOpenModal = jest.fn();
const mockAddbutton = jest.fn(val => ({ onAction: val }));
const mockNode = {
@@ -136,37 +139,42 @@ describe('TextEditor hooks', () => {
});
});
});
describe('selectedImage', () => {
describe('selectedImage hooks', () => {
const val = { a: 'VaLUe' };
const newVal = { some: 'vAlUe' };
let output;
let setter;
let hook;
beforeEach(() => {
[output, setter] = module.selectedImage(val);
state.mock();
hook = module.selectedImage(val);
});
test('returns a field which with state input val', () => {
expect(output).toMatchObject({ state: val });
test('selection: state value', () => {
expect(hook.selection).toEqual(state.stateVals[state.keys.imageSelection]);
});
test('calling setter with new val sets with respect to new val', () => {
setter(newVal);
expect(React.updateState).toHaveBeenCalledWith({ val, newVal });
test('setSelection: setter for value', () => {
expect(hook.setSelection).toEqual(state.setState[state.keys.imageSelection]);
});
test('clearSelection: calls setter with null', () => {
expect(hook.setSelection).not.toHaveBeenCalled();
hook.clearSelection();
expect(hook.setSelection).toHaveBeenCalledWith(null);
});
});
describe('modalToggle hook', () => {
let output;
let hook;
const hookKey = state.keys.isModalOpen;
beforeEach(() => {
output = module.modalToggle();
state.mock();
hook = module.modalToggle();
});
test('returns isOpen field, defaulted to false', () => {
expect(output.isOpen).toEqual({ state: false });
test('isOpen: state value', () => {
expect(hook.isOpen).toEqual(state.stateVals[hookKey]);
});
test('returns openModal field, which sets modal to true and calls updateState', () => {
output.openModal();
expect(React.updateState).toHaveBeenCalledWith({ val: false, newVal: true });
test('openModal: calls setter with true', () => {
hook.openModal();
expect(state.setState[hookKey]).toHaveBeenCalledWith(true);
});
test('returns closeModal field, which sets modal to true and calls updateState', () => {
output.closeModal();
expect(React.updateState).toHaveBeenCalledWith({ val: false, newVal: false });
test('closeModal: calls setter with false', () => {
hook.closeModal();
expect(state.setState[hookKey]).toHaveBeenCalledWith(false);
});
});
describe('nullMethod hook', () => {

View File

@@ -0,0 +1,9 @@
import { StrictDict } from '../../utils';
const commands = StrictDict({
insertContent: 'mceInsertContent',
});
export default StrictDict({
commands,
});

View File

@@ -46,6 +46,16 @@ describe('app reducer', () => {
['setBlockTitle', 'blockTitle'],
['setSaveResponse', 'saveResponse'],
].map(args => setterTest(...args));
describe('setBlockValue', () => {
it('sets blockValue, as well as setting the blockTitle from data.display_name', () => {
const blockValue = { data: { display_name: 'my test name' }, other: 'data' };
expect(reducer(testingState, actions.setBlockValue(blockValue))).toEqual({
...testingState,
blockValue,
blockTitle: blockValue.data.display_name,
});
});
});
describe('initializeEditor', () => {
it('sets editorInitialized to true', () => {
expect(reducer(testingState, actions.initializeEditor())).toEqual({

View File

@@ -1,4 +1,5 @@
// import * in order to mock in-file references
import { keyStore } from '../../../utils';
import * as urls from '../../services/cms/urls';
import * as selectors from './selectors';
@@ -30,17 +31,19 @@ describe('app selectors unit tests', () => {
expect(cb({ ...testState, [key]: testValue })).toEqual(testValue);
});
};
const simpleKeys = keyStore(simpleSelectors);
describe('simple selectors link their values from app store', () => {
[
'blockContent',
'blockId',
'blockType',
'blockValue',
'courseId',
'editorInitialized',
'saveResponse',
'studioEndpointUrl',
'unitUrl',
simpleKeys.blockContent,
simpleKeys.blockId,
simpleKeys.blockTitle,
simpleKeys.blockType,
simpleKeys.blockValue,
simpleKeys.courseId,
simpleKeys.editorInitialized,
simpleKeys.saveResponse,
simpleKeys.studioEndpointUrl,
simpleKeys.unitUrl,
].map(testSimpleSelector);
});
});

View File

@@ -36,50 +36,88 @@ describe('requests thunkActions module', () => {
const testData = ({ some: 'test data' });
let resolveFn;
let rejectFn;
beforeEach(() => {
onSuccess = jest.fn();
onFailure = jest.fn();
requests.networkRequest({
requestKey,
promise: new Promise((resolve, reject) => {
resolveFn = resolve;
rejectFn = reject;
}),
onSuccess,
onFailure,
})(dispatch);
});
test('calls startRequest action with requestKey', async () => {
expect(dispatch.mock.calls).toEqual([[actions.requests.startRequest(requestKey)]]);
});
describe('on success', () => {
beforeEach(async () => {
await resolveFn(testData);
describe('without success and failure handlers', () => {
beforeEach(() => {
requests.networkRequest({
requestKey,
promise: new Promise((resolve, reject) => {
resolveFn = resolve;
rejectFn = reject;
}),
})(dispatch);
});
it('dispatches completeRequest', async () => {
expect(dispatch.mock.calls).toEqual([
[actions.requests.startRequest(requestKey)],
[actions.requests.completeRequest({ requestKey, response: testData })],
]);
test('calls startRequest action with requestKey', async () => {
expect(dispatch.mock.calls).toEqual([[actions.requests.startRequest(requestKey)]]);
});
it('calls onSuccess with response', async () => {
expect(onSuccess).toHaveBeenCalledWith(testData);
expect(onFailure).not.toHaveBeenCalled();
describe('on success', () => {
beforeEach(async () => {
await resolveFn(testData);
});
it('dispatches completeRequest', async () => {
expect(dispatch.mock.calls).toEqual([
[actions.requests.startRequest(requestKey)],
[actions.requests.completeRequest({ requestKey, response: testData })],
]);
});
});
describe('on failure', () => {
beforeEach(async () => {
await rejectFn(testData);
});
test('dispatches completeRequest', async () => {
expect(dispatch.mock.calls).toEqual([
[actions.requests.startRequest(requestKey)],
[actions.requests.failRequest({ requestKey, error: testData })],
]);
});
});
});
describe('on failure', () => {
beforeEach(async () => {
await rejectFn(testData);
describe('with handlers', () => {
beforeEach(() => {
onSuccess = jest.fn();
onFailure = jest.fn();
requests.networkRequest({
requestKey,
promise: new Promise((resolve, reject) => {
resolveFn = resolve;
rejectFn = reject;
}),
onSuccess,
onFailure,
})(dispatch);
});
test('dispatches completeRequest', async () => {
expect(dispatch.mock.calls).toEqual([
[actions.requests.startRequest(requestKey)],
[actions.requests.failRequest({ requestKey, error: testData })],
]);
test('calls startRequest action with requestKey', async () => {
expect(dispatch.mock.calls).toEqual([[actions.requests.startRequest(requestKey)]]);
});
test('calls onSuccess with response', async () => {
expect(onFailure).toHaveBeenCalledWith(testData);
expect(onSuccess).not.toHaveBeenCalled();
describe('on success', () => {
beforeEach(async () => {
await resolveFn(testData);
});
it('dispatches completeRequest', async () => {
expect(dispatch.mock.calls).toEqual([
[actions.requests.startRequest(requestKey)],
[actions.requests.completeRequest({ requestKey, response: testData })],
]);
});
it('calls onSuccess with response', async () => {
expect(onSuccess).toHaveBeenCalledWith(testData);
expect(onFailure).not.toHaveBeenCalled();
});
});
describe('on failure', () => {
beforeEach(async () => {
await rejectFn(testData);
});
test('dispatches completeRequest', async () => {
expect(dispatch.mock.calls).toEqual([
[actions.requests.startRequest(requestKey)],
[actions.requests.failRequest({ requestKey, error: testData })],
]);
});
test('calls onFailure with response', async () => {
expect(onFailure).toHaveBeenCalledWith(testData);
expect(onSuccess).not.toHaveBeenCalled();
});
});
});
});

View File

@@ -1,4 +1,5 @@
/* eslint-disable import/no-extraneous-dependencies */
/* istanbul ignore file */
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';

View File

@@ -50,8 +50,6 @@ export const apiMethods = {
export const checkMockApi = (key) => {
if (process.env.REACT_APP_DEVGALLERY) {
// eslint-disable-next-line
console.log('use devgallery api methods');
return mockApi[key];
}
return module.apiMethods[key];

View File

@@ -1,3 +1,5 @@
/* istanbul ignore file */
import * as urls from './urls';
const mockPromise = (returnValue) => new Promise(resolve => resolve(returnValue));

View File

@@ -1,16 +1,25 @@
import {
useRef, useEffect, useCallback, useState,
} from 'react';
import { StrictDict } from './utils';
import * as module from './hooks';
export const initializeApp = ({ initialize, data }) => useEffect(() => initialize(data), []);
export const state = StrictDict({
refReady: (val) => useState(val),
});
export const initializeApp = ({ initialize, data }) => useEffect(
() => initialize(data),
[],
);
export const prepareEditorRef = () => {
const editorRef = useRef(null);
const setEditorRef = useCallback((ref) => {
editorRef.current = ref;
}, []);
const [refReady, setRefReady] = useState(false);
const [refReady, setRefReady] = module.state.refReady(false);
useEffect(() => setRefReady(true), []);
return { editorRef, refReady, setEditorRef };
};

View File

@@ -1,16 +1,17 @@
import { useEffect, updateState } from 'react';
import { useEffect } from 'react';
import { MockUseState } from '../testUtils';
import * as module from './hooks';
jest.mock('react', () => {
const updateStateMock = jest.fn();
return {
updateState: updateStateMock,
useState: jest.fn(val => ([{ state: val }, (newVal) => updateStateMock({ val, newVal })])),
useRef: jest.fn(val => ({ current: val })),
useEffect: jest.fn(),
useCallback: (cb, prereqs) => ({ cb, prereqs }),
};
});
jest.mock('react', () => ({
...jest.requireActual('react'),
useRef: jest.fn(val => ({ current: val })),
useEffect: jest.fn(),
useCallback: (cb, prereqs) => ({ cb, prereqs }),
}));
const state = new MockUseState(module);
describe('hooks', () => {
const locationTemp = window.location;
beforeAll(() => {
@@ -22,6 +23,9 @@ describe('hooks', () => {
afterAll(() => {
window.location = locationTemp;
});
describe('state hooks', () => {
state.testGetter(state.keys.refReady);
});
describe('initializeApp', () => {
test('calls provided function with provided data as args when useEffect is called', () => {
const mockIntialize = jest.fn(val => (val));
@@ -35,29 +39,32 @@ describe('hooks', () => {
});
});
describe('prepareEditorRef', () => {
let output;
let hook;
beforeEach(() => {
output = module.prepareEditorRef();
state.mock();
hook = module.prepareEditorRef();
});
afterEach(() => {
state.restore();
jest.clearAllMocks();
});
const key = state.keys.refReady;
test('sets refReady to false by default, ref is null', () => {
expect(output.refReady.state).toBe(false);
expect(output.editorRef.current).toBe(null);
expect(state.stateVals[key]).toEqual(false);
expect(hook.editorRef.current).toBe(null);
});
test('when useEffect triggers, refReady is set to true', () => {
expect(updateState).not.toHaveBeenCalled();
expect(state.setState[key]).not.toHaveBeenCalled();
const [cb, prereqs] = useEffect.mock.calls[0];
expect(prereqs).toStrictEqual([]);
cb();
expect(updateState).toHaveBeenCalledWith({ newVal: true, val: false });
expect(state.setState[key]).toHaveBeenCalledWith(true);
});
test('calling setEditorRef sets the ref value', () => {
const fakeEditor = { editor: 'faKe Editor' };
expect(output.editorRef.current).not.toBe(fakeEditor);
output.setEditorRef.cb(fakeEditor);
expect(output.editorRef.current).toBe(fakeEditor);
expect(hook.editorRef.current).not.toBe(fakeEditor);
hook.setEditorRef.cb(fakeEditor);
expect(hook.editorRef.current).toBe(fakeEditor);
});
});
describe('navigateTo', () => {

View File

@@ -1,2 +1,3 @@
/* eslint-disable import/prefer-default-export */
export { default as StrictDict } from './StrictDict';
export { default as keyStore } from './keyStore';

View File

@@ -0,0 +1,10 @@
import StrictDict from './StrictDict';
const keyStore = (collection) => StrictDict(
Object.keys(collection).reduce(
(obj, key) => ({ ...obj, [key]: key }),
{},
),
);
export default keyStore;

View File

@@ -69,6 +69,8 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
Footer: 'ModalDialog.Footer',
Header: 'ModalDialog.Header',
Title: 'ModalDialog.Title',
Body: 'ModalDialog.Body',
CloseButton: 'ModalDialog.CloseButton',
},
Form: {
Checkbox: 'Form.Checkbox',

View File

@@ -1,3 +1,5 @@
/* istanbul ignore file */
import react from 'react';
import { StrictDict } from './editors/utils';
/**
* Mocked formatMessage provided by react-intl
@@ -90,6 +92,11 @@ export const mockNestedComponents = (mapping) => Object.entries(mapping).reduce(
* import * as hooks from './hooks';
* const state = new MockUseState(hooks)
* ...
* describe('state hooks', () => {
* state.testGetter(state.keys.isOpen);
* state.testGetter(state.keys.hasDoors);
* state.testGetter(state.keys.selected);
* });
* describe('exampleHook', () => {
* beforeEach(() => { state.mock(); });
* it('returns null if isOpen is default value', () => {
@@ -112,10 +119,12 @@ export class MockUseState {
this.hooks = hooks;
this.oldState = null;
this.setState = {};
this.stateVals = {};
this.mock = this.mock.bind(this);
this.restore = this.restore.bind(this);
this.mockVal = this.mockVal.bind(this);
this.testGetter = this.testGetter.bind(this);
}
/**
@@ -134,10 +143,18 @@ export class MockUseState {
mock() {
this.oldState = this.hooks.state;
Object.keys(this.keys).forEach(key => {
this.hooks.state[key] = jest.fn(val => [val, this.setState[key]]);
this.hooks.state[key] = jest.fn(val => {
this.stateVals[key] = val;
return [val, this.setState[key]];
});
});
this.setState = Object.keys(this.keys).reduce(
(obj, key) => ({ ...obj, [key]: jest.fn() }),
(obj, key) => ({
...obj,
[key]: jest.fn(val => {
this.hooks.state[key] = val;
}),
}),
{},
);
}
@@ -157,4 +174,13 @@ export class MockUseState {
mockVal(key, val) {
this.hooks.state[key].mockReturnValueOnce([val, this.setState[key]]);
}
testGetter(key) {
test(`${key} state getter should return useState passthrough`, () => {
const testValue = 'some value';
const useState = (val) => ({ useState: val });
jest.spyOn(react, 'useState').mockImplementationOnce(useState);
expect(this.hooks.state[key](testValue)).toEqual(useState(testValue));
});
}
}

8
www/package-lock.json generated
View File

@@ -1173,9 +1173,9 @@
"integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA=="
},
"@edx/brand": {
"version": "npm:@edx/brand-openedx@1.1.0",
"resolved": "https://registry.npmjs.org/@edx/brand-openedx/-/brand-openedx-1.1.0.tgz",
"integrity": "sha512-ne2ZKF1r0akkt0rEzCAQAk4cTDTI2GiWCpc+T7ldQpw9X57OnUB16dKsFNe40C9uEjL5h3Ps/ZsFM5dm4cIkEQ=="
"version": "npm:@edx/brand-edx.org@2.0.3",
"resolved": "https://registry.npmjs.org/@edx/brand-edx.org/-/brand-edx.org-2.0.3.tgz",
"integrity": "sha512-QRmq2su1Xy+9GhY3NRZ+WdjtYWHmgfuKbVCW2skxgfgW9Q6kea8L6LrgigfrZtW+kQq/KdX2tVJcYBkB9xALtQ=="
},
"@edx/eslint-config": {
"version": "2.0.0",
@@ -2419,7 +2419,7 @@
"integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA=="
},
"@edx/brand": {
"version": "npm:@edx/brand@1.1.0",
"version": "npm:@edx/brand-openedx@1.1.0",
"resolved": "https://registry.npmjs.org/@edx/brand-openedx/-/brand-openedx-1.1.0.tgz",
"integrity": "sha512-ne2ZKF1r0akkt0rEzCAQAk4cTDTI2GiWCpc+T7ldQpw9X57OnUB16dKsFNe40C9uEjL5h3Ps/ZsFM5dm4cIkEQ=="
},

View File

@@ -10,7 +10,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
"@edx/frontend-build": "^9.1.1",
"@edx/frontend-platform": "1.14.0",
"@edx/frontend-lib-content-components": "file:..",