feat: change static url to asset url in editor

This commit is contained in:
Kristin Aoki
2022-08-24 12:53:18 -04:00
committed by GitHub
parent 09110ec0b0
commit 2b49304ecc
16 changed files with 174 additions and 75 deletions

View File

@@ -29,6 +29,13 @@ exports[`TextEditor snapshots block failed to load, Toast is shown 1`] = `
},
}
}
images={
Object {
"sOmEuiMAge": Object {
"staTICUrl": "/assets/sOmEuiMAge",
},
}
}
isOpen={false}
selection="hooks.selectedImage.selection"
setSelection={[MockFunction hooks.selectedImage.setSelection]}
@@ -63,6 +70,11 @@ exports[`TextEditor snapshots block failed to load, Toast is shown 1`] = `
},
},
"clearSelection": [MockFunction hooks.selectedImage.clearSelection],
"images": Object {
"sOmEuiMAge": Object {
"staTICUrl": "/assets/sOmEuiMAge",
},
},
"initializeEditor": [MockFunction args.intializeEditor],
"lmsEndpointUrl": "sOmEvaLue.cOm",
"openImgModal": [MockFunction modal.openModal],
@@ -106,6 +118,13 @@ exports[`TextEditor snapshots loaded, raw editor 1`] = `
},
}
}
images={
Object {
"sOmEuiMAge": Object {
"staTICUrl": "/assets/sOmEuiMAge",
},
}
}
isOpen={false}
selection="hooks.selectedImage.selection"
setSelection={[MockFunction hooks.selectedImage.setSelection]}
@@ -173,6 +192,13 @@ exports[`TextEditor snapshots not yet loaded, Spinner appears 1`] = `
},
}
}
images={
Object {
"sOmEuiMAge": Object {
"staTICUrl": "/assets/sOmEuiMAge",
},
}
}
isOpen={false}
selection="hooks.selectedImage.selection"
setSelection={[MockFunction hooks.selectedImage.setSelection]}
@@ -240,6 +266,13 @@ exports[`TextEditor snapshots renders as expected with default behavior 1`] = `
},
}
}
images={
Object {
"sOmEuiMAge": Object {
"staTICUrl": "/assets/sOmEuiMAge",
},
}
}
isOpen={false}
selection="hooks.selectedImage.selection"
setSelection={[MockFunction hooks.selectedImage.setSelection]}
@@ -274,6 +307,11 @@ exports[`TextEditor snapshots renders as expected with default behavior 1`] = `
},
},
"clearSelection": [MockFunction hooks.selectedImage.clearSelection],
"images": Object {
"sOmEuiMAge": Object {
"staTICUrl": "/assets/sOmEuiMAge",
},
},
"initializeEditor": [MockFunction args.intializeEditor],
"lmsEndpointUrl": "sOmEvaLue.cOm",
"openImgModal": [MockFunction modal.openModal],

View File

@@ -52,6 +52,7 @@ export const ImageUploadModal = ({
clearSelection,
selection,
setSelection,
images,
}) => {
if (selection) {
return (
@@ -78,6 +79,7 @@ export const ImageUploadModal = ({
close,
setSelection,
clearSelection,
images,
}}
/>
);
@@ -101,5 +103,6 @@ ImageUploadModal.propTypes = {
altText: PropTypes.bool,
}),
setSelection: PropTypes.func.isRequired,
images: PropTypes.shape({}).isRequired,
};
export default ImageUploadModal;

View File

@@ -7,7 +7,6 @@ import { sortFunctions, sortKeys } from './utils';
export const state = {
highlighted: (val) => React.useState(val),
images: (val) => React.useState(val),
showSelectImageError: (val) => React.useState(val),
searchString: (val) => React.useState(val),
sortBy: (val) => React.useState(val),
@@ -36,9 +35,7 @@ export const displayList = ({ sortBy, searchString, images }) => (
imageList: Object.values(images),
}).sort(sortFunctions[sortBy in sortKeys ? sortKeys[sortBy] : sortKeys.dateNewest]));
export const imgListHooks = ({ searchSortProps, setSelection }) => {
const dispatch = useDispatch();
const [images, setImages] = module.state.images({});
export const imgListHooks = ({ searchSortProps, setSelection, images }) => {
const [highlighted, setHighlighted] = module.state.highlighted(null);
const [
showSelectImageError,
@@ -47,10 +44,6 @@ export const imgListHooks = ({ searchSortProps, setSelection }) => {
const [showSizeError, setShowSizeError] = module.state.showSelectImageError(false);
const list = module.displayList({ ...searchSortProps, images });
React.useEffect(() => {
dispatch(thunkActions.app.fetchImages({ setImages }));
}, []);
return {
galleryError: {
show: showSelectImageError,
@@ -126,9 +119,9 @@ export const fileInputHooks = ({ setSelection, clearSelection, imgList }) => {
};
};
export const imgHooks = ({ setSelection, clearSelection }) => {
export const imgHooks = ({ setSelection, clearSelection, images }) => {
const searchSortProps = module.searchAndSortHooks();
const imgList = module.imgListHooks({ setSelection, searchSortProps });
const imgList = module.imgListHooks({ setSelection, searchSortProps, images });
const fileInput = module.fileInputHooks({
setSelection,
clearSelection,

View File

@@ -27,25 +27,6 @@ jest.mock('react-redux', () => {
jest.mock('../../../../data/redux', () => ({
thunkActions: {
app: {
fetchImages: jest.fn(),
uploadImage: jest.fn(),
},
},
}));
jest.mock('react-redux', () => {
const dispatchFn = jest.fn();
return {
...jest.requireActual('react-redux'),
dispatch: dispatchFn,
useDispatch: jest.fn(() => dispatchFn),
};
});
jest.mock('../../../../data/redux', () => ({
thunkActions: {
app: {
fetchImages: jest.fn(),
uploadImage: jest.fn(),
},
},
@@ -63,7 +44,6 @@ describe('SelectImageModal hooks', () => {
});
describe('state hooks', () => {
state.testGetter(state.keys.highlighted);
state.testGetter(state.keys.images);
state.testGetter(state.keys.showSelectImageError);
state.testGetter(state.keys.searchString);
state.testGetter(state.keys.sortBy);
@@ -156,6 +136,12 @@ describe('SelectImageModal hooks', () => {
const props = {
setSelection: jest.fn(),
searchSortProps: { searchString: 'Es', sortBy: sortKeys.dateNewest },
images: {
sOmEuiMAgeURl: {
displayName: 'sOmEuiMAge',
staTICUrl: '/assets/sOmEuiMAge',
},
},
};
const displayList = (args) => ({ displayList: args });
const load = () => {
@@ -165,31 +151,17 @@ describe('SelectImageModal hooks', () => {
beforeEach(() => {
load();
});
it('returns images value, initialized to an empty object', () => {
expect(state.stateVals.images).toEqual(hook.images);
expect(state.stateVals.images).toEqual({});
});
it('dispatches fetchImages thunkAction once, with setImages as onSuccess param', () => {
expect(React.useEffect.mock.calls.length).toEqual(1);
const [cb, prereqs] = React.useEffect.mock.calls[0];
expect(prereqs).toEqual([]);
cb();
expect(dispatch).toHaveBeenCalledWith(
thunkActions.app.fetchImages({ setImages: state.setState.images }),
);
});
describe('selectBtnProps', () => {
test('on click, if sets selection to the image with the same id', () => {
const highlighted = 'id1';
state.mockVal(state.keys.images, { [highlighted]: testValue });
const highlighted = 'sOmEuiMAgeURl';
const highlightedValue = { displayName: 'sOmEuiMAge', staTICUrl: '/assets/sOmEuiMAge' };
state.mockVal(state.keys.highlighted, highlighted);
load();
expect(props.setSelection).not.toHaveBeenCalled();
hook.selectBtnProps.onClick();
expect(props.setSelection).toHaveBeenCalledWith(testValue);
expect(props.setSelection).toHaveBeenCalledWith(highlightedValue);
});
test('on click, sets showSelectImageError to true if nothing is highlighted', () => {
state.mockVal(state.keys.images, { });
state.mockVal(state.keys.highlighted, null);
load();
hook.selectBtnProps.onClick();
@@ -209,7 +181,7 @@ describe('SelectImageModal hooks', () => {
test('displayList returns displayListhook called with searchSortProps and images', () => {
expect(hook.galleryProps.displayList).toEqual(displayList({
...props.searchSortProps,
images: hook.images,
images: props.images,
}));
});
});
@@ -307,6 +279,7 @@ describe('SelectImageModal hooks', () => {
};
const searchAndSortHooks = { search: 'props' };
const fileInputHooks = { file: 'input hooks' };
const images = { sOmEuiMAge: { staTICUrl: '/assets/sOmEuiMAge' } };
const setSelection = jest.fn();
const clearSelection = jest.fn();
@@ -318,7 +291,7 @@ describe('SelectImageModal hooks', () => {
.mockReturnValueOnce(searchAndSortHooks);
spies.file = jest.spyOn(hooks, hookKeys.fileInputHooks)
.mockReturnValueOnce(fileInputHooks);
hook = hooks.imgHooks({ setSelection, clearSelection });
hook = hooks.imgHooks({ setSelection, clearSelection, images });
});
it('forwards fileInputHooks as fileInput, called with uploadImage prop', () => {
expect(hook.fileInput).toEqual(fileInputHooks);
@@ -327,11 +300,12 @@ describe('SelectImageModal hooks', () => {
setSelection, clearSelection, imgList: imgListHooks,
});
});
it('initializes imgListHooks with setSelection and searchAndSortHooks', () => {
it('initializes imgListHooks with setSelection,searchAndSortHooks, and images', () => {
expect(spies.imgList.mock.calls.length).toEqual(1);
expect(spies.imgList).toHaveBeenCalledWith({
setSelection,
searchSortProps: searchAndSortHooks,
images,
});
});
it('forwards searchAndSortHooks as searchSortProps', () => {

View File

@@ -27,6 +27,7 @@ export const SelectImageModal = ({
close,
setSelection,
clearSelection,
images,
// injected
intl,
// redux
@@ -39,7 +40,7 @@ export const SelectImageModal = ({
galleryProps,
searchSortProps,
selectBtnProps,
} = hooks.imgHooks({ setSelection, clearSelection });
} = hooks.imgHooks({ setSelection, clearSelection, images });
return (
<BaseModal
@@ -96,6 +97,7 @@ SelectImageModal.propTypes = {
close: PropTypes.func.isRequired,
setSelection: PropTypes.func.isRequired,
clearSelection: PropTypes.func.isRequired,
images: PropTypes.shape({}).isRequired,
// injected
intl: intlShape.isRequired,
// redux

View File

@@ -60,6 +60,33 @@ export const setupCustomBehavior = ({
});
};
export const replaceStaticwithAsset = (editor, imageUrls) => {
const content = editor.getContent();
const imageSrcs = content.split('img src="');
imageSrcs.forEach(src => {
if (src.startsWith('/static/') && imageUrls.length > 0) {
const imgName = src.substring(8, src.indexOf('"'));
let staticFullUrl;
imageUrls.forEach((url) => {
if (url.includes(imgName)) {
staticFullUrl = url;
}
});
const currentSrc = src.substring(0, src.indexOf('"'));
const updatedContent = content.replace(currentSrc, staticFullUrl);
editor.setContent(updatedContent);
}
});
};
export const checkRelativeUrl = (imageUrls) => (editor) => {
editor.on('ExecCommand', (e) => {
if (e.command === 'mceFocus') {
module.replaceStaticwithAsset(editor, imageUrls);
}
});
};
// imagetools_cors_hosts needs a protocol-sanatized url
export const removeProtocolFromUrl = (url) => url.replace(/^https?:\/\//, '');
@@ -72,6 +99,7 @@ export const editorConfig = ({
setEditorRef,
setSelection,
studioEndpointUrl,
images,
}) => ({
onInit: (evt, editor) => {
setEditorRef(editor);
@@ -85,6 +113,7 @@ export const editorConfig = ({
content_style: tinyMCEStyles,
contextmenu: 'link table',
document_base_url: lmsEndpointUrl,
init_instance_callback: module.checkRelativeUrl(module.fetchImageUrls(images)),
imagetools_cors_hosts: [removeProtocolFromUrl(lmsEndpointUrl), removeProtocolFromUrl(studioEndpointUrl)],
imagetools_toolbar: pluginConfig.imageToolbar,
plugins: pluginConfig.plugins,
@@ -108,12 +137,15 @@ export const imgModalToggle = () => {
};
};
export const sourceCodeModalToggle = () => {
export const sourceCodeModalToggle = (editorRef) => {
const [isSourceCodeOpen, setIsOpen] = module.state.isSourceCodeModalOpen(false);
return {
isSourceCodeOpen,
openSourceCodeModal: () => setIsOpen(true),
closeSourceCodeModal: () => setIsOpen(false),
closeSourceCodeModal: () => {
setIsOpen(false);
editorRef.current.focus();
},
};
};
@@ -145,6 +177,15 @@ export const getContent = ({ editorRef, isRaw }) => () => {
return editorRef.current?.getContent();
};
export const fetchImageUrls = (images) => {
const imageUrls = [];
const imgsArray = Object.values(images);
imgsArray.forEach(image => {
imageUrls.push(image.staticFullUrl);
});
return imageUrls;
};
export const selectedImage = (val) => {
const [selection, setSelection] = module.state.imageSelection(val);
return {

View File

@@ -86,11 +86,27 @@ describe('TextEditor hooks', () => {
});
});
describe('replaceStaticwithAsset', () => {
const editor = { getContent: jest.fn(() => '<img src="/static/soMEImagEURl1.jpeg"/>'), setContent: jest.fn() };
const imageUrls = ['soMEImagEURl1.jpeg'];
module.replaceStaticwithAsset(editor, imageUrls);
expect(editor.getContent).toHaveBeenCalled();
expect(editor.setContent).toHaveBeenCalled();
});
describe('checkRelativeUrl', () => {
const editor = { on: jest.fn() };
const imageUrls = ['soMEImagEURl1'];
module.checkRelativeUrl(imageUrls)(editor);
expect(editor.on).toHaveBeenCalled();
});
describe('editorConfig', () => {
const props = {
blockValue: null,
lmsEndpointUrl: 'sOmEuRl.cOm',
studioEndpointUrl: 'sOmEoThEruRl.cOm',
images: { sOmEuiMAge: { staTICUrl: '/assets/sOmEuiMAge' } },
};
const evt = 'fakeEvent';
const editor = 'myEditor';
@@ -159,9 +175,10 @@ describe('TextEditor hooks', () => {
});
describe('sourceCodeModalToggle', () => {
const editorRef = { current: { focus: jest.fn() } };
const hookKey = state.keys.isSourceCodeModalOpen;
beforeEach(() => {
hook = module.sourceCodeModalToggle();
hook = module.sourceCodeModalToggle(editorRef);
});
test('isOpen: state value', () => {
expect(hook.isSourceCodeOpen).toEqual(state.stateVals[hookKey]);

View File

@@ -44,14 +44,15 @@ export const TextEditor = ({
lmsEndpointUrl,
studioEndpointUrl,
blockFailed,
blockFinished,
initializeEditor,
images,
imagesFinished,
// inject
intl,
}) => {
const { editorRef, refReady, setEditorRef } = hooks.prepareEditorRef();
const { isImgOpen, openImgModal, closeImgModal } = hooks.imgModalToggle();
const { isSourceCodeOpen, openSourceCodeModal, closeSourceCodeModal } = hooks.sourceCodeModalToggle();
const { isSourceCodeOpen, openSourceCodeModal, closeSourceCodeModal } = hooks.sourceCodeModalToggle(editorRef);
const imageSelection = hooks.selectedImage(null);
if (!refReady) { return null; }
@@ -75,6 +76,7 @@ export const TextEditor = ({
initializeEditor,
lmsEndpointUrl,
studioEndpointUrl,
images,
setSelection: imageSelection.setSelection,
clearSelection: imageSelection.clearSelection,
})}
@@ -92,6 +94,7 @@ export const TextEditor = ({
isOpen={isImgOpen}
close={closeImgModal}
editorRef={editorRef}
images={images}
{...imageSelection}
/>
<SourceCodeModal
@@ -104,7 +107,7 @@ export const TextEditor = ({
<FormattedMessage {...messages.couldNotLoadTextContext} />
</Toast>
{(!blockFinished)
{(!imagesFinished)
? (
<div className="text-center p-6">
<Spinner
@@ -124,6 +127,8 @@ TextEditor.defaultProps = {
isRaw: null,
lmsEndpointUrl: null,
studioEndpointUrl: null,
images: null,
imagesFinished: null,
};
TextEditor.propTypes = {
onClose: PropTypes.func.isRequired,
@@ -134,9 +139,10 @@ TextEditor.propTypes = {
lmsEndpointUrl: PropTypes.string,
studioEndpointUrl: PropTypes.string,
blockFailed: PropTypes.bool.isRequired,
blockFinished: PropTypes.bool.isRequired,
initializeEditor: PropTypes.func.isRequired,
isRaw: PropTypes.bool,
imagesFinished: PropTypes.bool,
images: PropTypes.shape({}),
// inject
intl: intlShape.isRequired,
};
@@ -146,8 +152,9 @@ export const mapStateToProps = (state) => ({
lmsEndpointUrl: selectors.app.lmsEndpointUrl(state),
studioEndpointUrl: selectors.app.studioEndpointUrl(state),
blockFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchBlock }),
blockFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }),
isRaw: selectors.app.isRaw(state),
imagesFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchImages }),
images: selectors.app.images(state),
});
export const mapDispatchToProps = {

View File

@@ -70,6 +70,7 @@ jest.mock('../../data/redux', () => ({
lmsEndpointUrl: jest.fn(state => ({ lmsEndpointUrl: state })),
studioEndpointUrl: jest.fn(state => ({ studioEndpointUrl: state })),
isRaw: jest.fn(state => ({ isRaw: state })),
images: jest.fn(state => ({ images: state })),
},
requests: {
isFailed: jest.fn((state, params) => ({ isFailed: { state, params } })),
@@ -86,9 +87,10 @@ describe('TextEditor', () => {
lmsEndpointUrl: 'sOmEvaLue.cOm',
studioEndpointUrl: 'sOmEoThERvaLue.cOm',
blockFailed: false,
blockFinished: true,
initializeEditor: jest.fn().mockName('args.intializeEditor'),
isRaw: false,
imagesFinished: true,
images: { sOmEuiMAge: { staTICUrl: '/assets/sOmEuiMAge' } },
// inject
intl: { formatMessage },
};
@@ -107,7 +109,7 @@ describe('TextEditor', () => {
expect(shallow(<TextEditor {...props} />)).toMatchSnapshot();
});
test('not yet loaded, Spinner appears', () => {
expect(shallow(<TextEditor {...props} blockFinished={false} />)).toMatchSnapshot();
expect(shallow(<TextEditor {...props} imagesFinished={false} />)).toMatchSnapshot();
});
test('loaded, raw editor', () => {
expect(shallow(<TextEditor {...props} isRaw />)).toMatchSnapshot();
@@ -128,15 +130,20 @@ describe('TextEditor', () => {
mapStateToProps(testState).lmsEndpointUrl,
).toEqual(selectors.app.lmsEndpointUrl(testState));
});
test('images from app.images', () => {
expect(
mapStateToProps(testState).images,
).toEqual(selectors.app.images(testState));
});
test('blockFailed from requests.isFailed', () => {
expect(
mapStateToProps(testState).blockFailed,
).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.fetchBlock }));
});
test('blockFinished from requests.isFinished', () => {
test('imagesFinished from requests.isFinished', () => {
expect(
mapStateToProps(testState).blockFinished,
).toEqual(selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchBlock }));
mapStateToProps(testState).imagesFinished,
).toEqual(selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchImages }));
});
});
describe('mapDispatchToProps', () => {

View File

@@ -49,6 +49,7 @@ export default StrictDict({
menubar: false,
min_height: 500,
toolbar_sticky: true,
relative_urls: false,
relative_urls: true,
convert_urls: false,
},
});

View File

@@ -8,7 +8,6 @@ const initialState = {
blockContent: null,
studioView: null,
saveResponse: null,
blockId: null,
blockTitle: null,
blockType: null,
@@ -16,6 +15,7 @@ const initialState = {
editorInitialized: false,
studioEndpointUrl: null,
lmsEndpointUrl: null,
images: {},
};
// eslint-disable-next-line no-unused-vars
@@ -45,6 +45,7 @@ const app = createSlice({
setBlockTitle: (state, { payload }) => ({ ...state, blockTitle: payload }),
setSaveResponse: (state, { payload }) => ({ ...state, saveResponse: payload }),
initializeEditor: (state) => ({ ...state, editorInitialized: true }),
setImages: (state, { payload }) => ({ ...state, images: payload }),
},
});

View File

@@ -47,6 +47,7 @@ describe('app reducer', () => {
['setBlockContent', 'blockContent'],
['setBlockTitle', 'blockTitle'],
['setSaveResponse', 'saveResponse'],
['setImages', 'images'],
].map(args => setterTest(...args));
describe('setBlockValue', () => {
it('sets blockValue, as well as setting the blockTitle from data.display_name', () => {

View File

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

View File

@@ -46,6 +46,7 @@ describe('app selectors unit tests', () => {
simpleKeys.unitUrl,
simpleKeys.blockTitle,
simpleKeys.studioView,
simpleKeys.images,
].map(testSimpleSelector);
});
});

View File

@@ -24,6 +24,12 @@ export const fetchUnit = () => (dispatch) => {
}));
};
export const fetchImages = () => (dispatch) => {
dispatch(requests.fetchImages({
onSuccess: (response) => dispatch(actions.app.setImages(response)),
}));
};
/**
* @param {string} studioEndpointUrl
* @param {string} blockId
@@ -35,6 +41,7 @@ export const initialize = (data) => (dispatch) => {
dispatch(module.fetchBlock());
dispatch(module.fetchUnit());
dispatch(module.fetchStudioView());
dispatch(module.fetchImages());
};
/**
@@ -51,10 +58,6 @@ export const saveBlock = ({ content, returnToUnit }) => (dispatch) => {
}));
};
export const fetchImages = ({ setImages }) => (dispatch) => {
dispatch(requests.fetchImages({ onSuccess: setImages }));
};
export const uploadImage = ({ file, setSelection }) => (dispatch) => {
dispatch(requests.uploadImage({
image: file,

View File

@@ -80,20 +80,28 @@ describe('app thunkActions', () => {
});
describe('initialize', () => {
it('dispatches actions.app.initialize, and then fetches both block and unit', () => {
const { fetchBlock, fetchUnit, fetchStudioView } = thunkActions;
const {
fetchBlock,
fetchUnit,
fetchStudioView,
fetchImages,
} = thunkActions;
thunkActions.fetchBlock = () => 'fetchBlock';
thunkActions.fetchUnit = () => 'fetchUnit';
thunkActions.fetchStudioView = () => 'fetchStudioView';
thunkActions.fetchImages = () => 'fetchImages';
thunkActions.initialize(testValue)(dispatch);
expect(dispatch.mock.calls).toEqual([
[actions.app.initialize(testValue)],
[thunkActions.fetchBlock()],
[thunkActions.fetchUnit()],
[thunkActions.fetchStudioView()],
[thunkActions.fetchImages()],
]);
thunkActions.fetchBlock = fetchBlock;
thunkActions.fetchUnit = fetchUnit;
thunkActions.fetchStudioView = fetchStudioView;
thunkActions.fetchImages = fetchImages;
});
});
describe('saveBlock', () => {
@@ -122,10 +130,11 @@ describe('app thunkActions', () => {
});
describe('fetchImages', () => {
it('dispatches fetchUnit action with setImages for onSuccess param', () => {
const setImages = jest.fn();
thunkActions.fetchImages({ setImages })(dispatch);
[[dispatchedAction]] = dispatch.mock.calls;
expect(dispatchedAction.fetchImages).toEqual({ onSuccess: setImages });
const response = 'testRESPONSE';
thunkActions.fetchImages()(dispatch);
const [[dispatchCall]] = dispatch.mock.calls;
dispatchCall.fetchImages.onSuccess(response);
expect(dispatch).toHaveBeenCalledWith(actions.app.setImages(response));
});
});
describe('uploadImage', () => {