diff --git a/src/editors/containers/EditorContainer/hooks.js b/src/editors/containers/EditorContainer/hooks.js index b0385b6e5..caae0263d 100644 --- a/src/editors/containers/EditorContainer/hooks.js +++ b/src/editors/containers/EditorContainer/hooks.js @@ -27,7 +27,8 @@ export const handleSaveClicked = ({ returnFunction, }) => { // eslint-disable-next-line react-hooks/rules-of-hooks - const destination = returnFunction ? '' : useSelector(selectors.app.returnUrl); + const returnUrl = useSelector(selectors.app.returnUrl); + const destination = returnFunction ? '' : returnUrl; // eslint-disable-next-line react-hooks/rules-of-hooks const analytics = useSelector(selectors.app.analytics); @@ -54,10 +55,12 @@ export const handleCancel = ({ onClose, returnFunction }) => { if (onClose) { return onClose; } + // eslint-disable-next-line react-hooks/rules-of-hooks + const returnUrl = useSelector(selectors.app.returnUrl); return navigateCallback({ returnFunction, // eslint-disable-next-line react-hooks/rules-of-hooks - destination: returnFunction ? '' : useSelector(selectors.app.returnUrl), + destination: returnFunction ? '' : returnUrl, analyticsEvent: analyticsEvt.editorCancelClick, // eslint-disable-next-line react-hooks/rules-of-hooks analytics: useSelector(selectors.app.analytics), diff --git a/src/editors/containers/VideoGallery/hooks.js b/src/editors/containers/VideoGallery/hooks.js index 970342abc..3c8c3ec24 100644 --- a/src/editors/containers/VideoGallery/hooks.js +++ b/src/editors/containers/VideoGallery/hooks.js @@ -1,16 +1,13 @@ import React from 'react'; import { useSelector } from 'react-redux'; + import * as module from './hooks'; import messages from './messages'; import * as appHooks from '../../hooks'; import { selectors } from '../../data/redux'; import analyticsEvt from '../../data/constants/analyticsEvt'; import { - filterKeys, - filterMessages, - sortKeys, - sortMessages, - sortFunctions, + filterKeys, sortFunctions, sortKeys, sortMessages, } from './utils'; export const { @@ -18,28 +15,19 @@ export const { navigateTo, } = appHooks; -export const state = { - // eslint-disable-next-line react-hooks/rules-of-hooks - highlighted: (val) => React.useState(val), - // eslint-disable-next-line react-hooks/rules-of-hooks - searchString: (val) => React.useState(val), - // eslint-disable-next-line react-hooks/rules-of-hooks - showSelectVideoError: (val) => React.useState(val), - // eslint-disable-next-line react-hooks/rules-of-hooks - showSizeError: (val) => React.useState(val), - // eslint-disable-next-line react-hooks/rules-of-hooks - sortBy: (val) => React.useState(val), - // eslint-disable-next-line react-hooks/rules-of-hooks - filertBy: (val) => React.useState(val), - // eslint-disable-next-line react-hooks/rules-of-hooks - hideSelectedVideos: (val) => React.useState(val), -}; +export const useSearchAndSortProps = () => { + const [searchString, setSearchString] = React.useState(''); + const [sortBy, setSortBy] = React.useState(sortKeys.dateNewest); + const [filterBy, setFilterBy] = React.useState([]); + const [hideSelectedVideos, setHideSelectedVideos] = React.useState(false); -export const searchAndSortProps = () => { - const [searchString, setSearchString] = module.state.searchString(''); - const [sortBy, setSortBy] = module.state.sortBy(sortKeys.dateNewest); - const [filterBy, setFilterBy] = module.state.filertBy(filterKeys.videoStatus); - const [hideSelectedVideos, setHideSelectedVideos] = module.state.hideSelectedVideos(false); + const handleFilter = (key) => () => { + if (filterBy.includes(key)) { + setFilterBy(filterBy.filter(item => item !== key)); + } else { + setFilterBy([...filterBy, key]); + } + }; return { searchString, @@ -50,9 +38,7 @@ export const searchAndSortProps = () => { sortKeys, sortMessages, filterBy, - onFilterClick: (key) => () => setFilterBy(key), - filterKeys, - filterMessages, + onFilterClick: handleFilter, showSwitch: false, hideSelectedVideos, switchMessage: messages.hideSelectedCourseVideosSwitchLabel, @@ -60,15 +46,23 @@ export const searchAndSortProps = () => { }; }; -export const filterListBySearch = ({ searchString, videoList }) => ( - videoList.filter(({ displayName }) => displayName.toLowerCase().includes(searchString.toLowerCase())) +export const filterListBySearch = ({ + searchString, + videoList, +}) => ( + videoList.filter(({ displayName }) => displayName.toLowerCase() + .includes(searchString.toLowerCase())) ); -export const filterListByStatus = ({ statusFilter, videoList }) => { - if (statusFilter === filterKeys.videoStatus) { +export const filterListByStatus = ({ + statusFilter, + videoList, +}) => { + if (statusFilter.length === 0) { return videoList; } - return videoList.filter(({ status }) => status === statusFilter); + return videoList.filter(({ status }) => statusFilter.map(key => filterKeys[key]) + .includes(status)); }; export const filterListByHideSelectedCourse = ({ videoList }) => ( @@ -96,20 +90,24 @@ export const filterList = ({ return filteredList.sort(sortFunctions[sortBy in sortKeys ? sortKeys[sortBy] : sortKeys.dateNewest]); }; -export const videoListProps = ({ searchSortProps, videos }) => { - const [highlighted, setHighlighted] = module.state.highlighted(null); +export const useVideoListProps = ({ + searchSortProps, + videos, +}) => { + const [highlighted, setHighlighted] = React.useState(null); const [ showSelectVideoError, setShowSelectVideoError, - ] = module.state.showSelectVideoError(false); + ] = React.useState(false); const [ showSizeError, setShowSizeError, - ] = module.state.showSizeError(false); - const filteredList = module.filterList({ ...searchSortProps, videos }); - // eslint-disable-next-line react-hooks/rules-of-hooks + ] = React.useState(false); + const filteredList = module.filterList({ + ...searchSortProps, + videos, + }); const learningContextId = useSelector(selectors.app.learningContextId); - // eslint-disable-next-line react-hooks/rules-of-hooks const blockId = useSelector(selectors.app.blockId); return { galleryError: { @@ -147,26 +145,15 @@ export const videoListProps = ({ searchSortProps, videos }) => { }; }; -export const fileInputProps = () => { - const click = module.handleVideoUpload(); - return { - click, - }; -}; - -export const handleVideoUpload = () => { - // eslint-disable-next-line react-hooks/rules-of-hooks +export const useVideoUploadHandler = () => { const learningContextId = useSelector(selectors.app.learningContextId); - // eslint-disable-next-line react-hooks/rules-of-hooks const blockId = useSelector(selectors.app.blockId); return () => navigateTo(`/course/${learningContextId}/editor/video_upload/${blockId}`); }; -export const handleCancel = () => ( +export const useCancelHandler = () => ( navigateCallback({ - // eslint-disable-next-line react-hooks/rules-of-hooks destination: useSelector(selectors.app.returnUrl), - // eslint-disable-next-line react-hooks/rules-of-hooks analytics: useSelector(selectors.app.analytics), analyticsEvent: analyticsEvt.videoGalleryCancelClick, }) @@ -180,7 +167,7 @@ export const buildVideos = ({ rawVideos }) => { id: video.edx_video_id, displayName: video.client_video_id, externalUrl: video.course_video_image_url, - dateAdded: video.created, + dateAdded: new Date(video.created), locked: false, thumbnail: video.course_video_image_url, status: video.status, @@ -204,16 +191,19 @@ export const getstatusBadgeVariant = ({ status }) => { } }; -export const videoProps = ({ videos }) => { - const searchSortProps = module.searchAndSortProps(); - const videoList = module.videoListProps({ searchSortProps, videos }); +export const useVideoProps = ({ videos }) => { + const searchSortProps = useSearchAndSortProps(); + const videoList = useVideoListProps({ + searchSortProps, + videos, + }); const { galleryError, galleryProps, inputError, selectBtnProps, } = videoList; - const fileInput = module.fileInputProps(); + const fileInput = { click: useVideoUploadHandler() }; return { galleryError, @@ -226,8 +216,8 @@ export const videoProps = ({ videos }) => { }; export default { - videoProps, + useVideoProps, buildVideos, - handleCancel, - handleVideoUpload, + useCancelHandler, + useVideoUploadHandler, }; diff --git a/src/editors/containers/VideoGallery/hooks.test.js b/src/editors/containers/VideoGallery/hooks.test.js deleted file mode 100644 index 3015ad1f8..000000000 --- a/src/editors/containers/VideoGallery/hooks.test.js +++ /dev/null @@ -1,312 +0,0 @@ -import * as reactRedux from 'react-redux'; -import * as hooks from './hooks'; -import { filterKeys, sortKeys } from './utils'; -import { MockUseState } from '../../../testUtils'; -import { keyStore } from '../../utils'; -import * as appHooks from '../../hooks'; -import { selectors } from '../../data/redux'; -import analyticsEvt from '../../data/constants/analyticsEvt'; - -jest.mock('react', () => ({ - ...jest.requireActual('react'), - useRef: jest.fn(val => ({ current: val })), - useEffect: jest.fn(), - useCallback: (cb, prereqs) => ({ cb, prereqs }), -})); - -jest.mock('react-redux', () => { - const dispatchFn = jest.fn(); - return { - ...jest.requireActual('react-redux'), - dispatch: dispatchFn, - useDispatch: jest.fn(() => dispatchFn), - useSelector: jest.fn(), - }; -}); - -jest.mock('../../data/redux', () => ({ - selectors: { - app: { - returnUrl: 'returnUrl', - analytics: 'analytics', - }, - }, -})); - -jest.mock('../../hooks', () => ({ - ...jest.requireActual('../../hooks'), - navigateCallback: jest.fn((args) => ({ navigateCallback: args })), - navigateTo: jest.fn((args) => ({ navigateTo: args })), -})); - -const state = new MockUseState(hooks); -const hookKeys = keyStore(hooks); -let hook; -const testValue = 'testVALUEVALID'; - -describe('VideoGallery hooks', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - describe('state hooks', () => { - state.testGetter(state.keys.highlighted); - state.testGetter(state.keys.searchString); - state.testGetter(state.keys.showSelectVideoError); - state.testGetter(state.keys.showSizeError); - state.testGetter(state.keys.sortBy); - state.testGetter(state.keys.filertBy); - state.testGetter(state.keys.hideSelectedVideos); - }); - describe('using state', () => { - beforeEach(() => { state.mock(); }); - afterEach(() => { state.restore(); }); - - describe('searchAndSortProps', () => { - beforeEach(() => { - hook = hooks.searchAndSortProps(); - }); - it('returns searchString value, initialized to an empty string', () => { - expect(state.stateVals.searchString).toEqual(hook.searchString); - expect(state.stateVals.searchString).toEqual(''); - }); - it('returns highlighted value, initialized to dateNewest', () => { - expect(state.stateVals.sortBy).toEqual(hook.sortBy); - expect(state.stateVals.sortBy).toEqual(sortKeys.dateNewest); - }); - test('onSearchChange sets searchString with event target value', () => { - hook.onSearchChange({ target: { value: testValue } }); - expect(state.setState.searchString).toHaveBeenCalledWith(testValue); - }); - test('clearSearchString sets search string to empty string', () => { - hook.clearSearchString(); - expect(state.setState.searchString).toHaveBeenCalledWith(''); - }); - test('onSortClick takes a key and returns callback to set sortBY to that key', () => { - hook.onSortClick(testValue); - expect(state.setState.sortBy).not.toHaveBeenCalled(); - hook.onSortClick(testValue)(); - expect(state.setState.sortBy).toHaveBeenCalledWith(testValue); - }); - }); - describe('filterListBySearch', () => { - const matching = [ - 'test', - 'TEst', - 'eeees', - 'essSSSS', - ]; - const notMatching = ['bad', 'other', 'bad stuff']; - const searchString = 'eS'; - test('returns list filtered lowercase by displayName', () => { - const filter = jest.fn(cb => ({ filter: cb })); - hook = hooks.filterListBySearch({ searchString, videoList: { filter } }); - expect(filter).toHaveBeenCalled(); - const [[filterCb]] = filter.mock.calls; - matching.forEach(val => expect(filterCb({ displayName: val })).toEqual(true)); - notMatching.forEach(val => expect(filterCb({ displayName: val })).toEqual(false)); - }); - }); - describe('buildVideos', () => { - const rawVideos = [ - { - edx_video_id: 'id_1', - client_video_id: 'client_id_1', - course_video_image_url: 'course_video_image_url_1', - created: 'created_1', - status: 'status_1', - duration: 1, - transcripts: [], - }, - { - edx_video_id: 'id_2', - client_video_id: 'client_id_2', - course_video_image_url: 'course_video_image_url_2', - created: 'created_2', - status: 'status_2', - duration: 2, - transcripts: [], - }, - ]; - const expectedValues = [ - { - id: 'id_1', - displayName: 'client_id_1', - externalUrl: 'course_video_image_url_1', - dateAdded: 'created_1', - locked: false, - thumbnail: 'course_video_image_url_1', - status: 'status_1', - statusBadgeVariant: null, - duration: 1, - transcripts: [], - }, - { - id: 'id_2', - displayName: 'client_id_2', - externalUrl: 'course_video_image_url_2', - dateAdded: 'created_2', - locked: false, - thumbnail: 'course_video_image_url_2', - status: 'status_2', - statusBadgeVariant: null, - duration: 2, - transcripts: [], - }, - ]; - test('return the expected values', () => { - const values = hooks.buildVideos({ rawVideos }); - expect(values).toEqual(expectedValues); - }); - }); - describe('getstatusBadgeVariant', () => { - test('return the expected values', () => { - let value = hooks.getstatusBadgeVariant({ status: filterKeys.failed }); - expect(value).toEqual('danger'); - value = hooks.getstatusBadgeVariant({ status: filterKeys.uploading }); - expect(value).toEqual('light'); - value = hooks.getstatusBadgeVariant({ status: filterKeys.processing }); - expect(value).toEqual('light'); - value = hooks.getstatusBadgeVariant({ status: filterKeys.videoStatus }); - expect(value).toBeNull(); - value = hooks.getstatusBadgeVariant({ status: filterKeys.ready }); - expect(value).toBeNull(); - }); - }); - describe('videoListProps outputs', () => { - const props = { - searchSortProps: { - searchString: 'Es', - sortBy: sortKeys.dateNewest, - filterBy: filterKeys.videoStatus, - }, - videos: [ - { - displayName: 'sOmEuiMAge', - staTICUrl: '/assets/sOmEuiMAge', - id: 'sOmEuiMAgeURl', - }, - ], - }; - const filterList = (args) => ({ filterList: args }); - const load = () => { - jest.spyOn(hooks, hookKeys.filterList).mockImplementationOnce(filterList); - hook = hooks.videoListProps(props); - }; - beforeEach(() => { - load(); - }); - describe('selectBtnProps', () => { - test('on click, if sets selection', () => { - const highlighted = 'videoId'; - state.mockVal(state.keys.highlighted, highlighted); - load(); - expect(appHooks.navigateTo).not.toHaveBeenCalled(); - hook.selectBtnProps.onClick(); - expect(appHooks.navigateTo).toHaveBeenCalled(); - }); - test('on click, sets showSelectVideoError to true if nothing is highlighted', () => { - state.mockVal(state.keys.highlighted, null); - load(); - hook.selectBtnProps.onClick(); - expect(appHooks.navigateTo).not.toHaveBeenCalled(); - expect(state.setState.showSelectVideoError).toHaveBeenCalledWith(true); - }); - }); - describe('galleryProps', () => { - it('returns highlighted value, initialized to null', () => { - expect(hook.galleryProps.highlighted).toEqual(state.stateVals.highlighted); - expect(state.stateVals.highlighted).toEqual(null); - }); - test('onHighlightChange sets highlighted with event target value', () => { - hook.galleryProps.onHighlightChange({ target: { value: testValue } }); - expect(state.setState.highlighted).toHaveBeenCalledWith(testValue); - }); - test('displayList returns filterListhook called with searchSortProps and videos', () => { - expect(hook.galleryProps.displayList).toEqual(filterList({ - ...props.searchSortProps, - videos: props.videos, - })); - }); - }); - describe('galleryError', () => { - test('show is initialized to false and returns properly', () => { - const show = 'sHOWSelectiRROr'; - expect(hook.galleryError.show).toEqual(false); - state.mockVal(state.keys.showSelectVideoError, show); - hook = hooks.videoListProps(props); - expect(hook.galleryError.show).toEqual(show); - }); - test('set sets showSelectVideoError to true', () => { - hook.galleryError.set(); - expect(state.setState.showSelectVideoError).toHaveBeenCalledWith(true); - }); - test('dismiss sets showSelectVideoError to false', () => { - hook.galleryError.dismiss(); - expect(state.setState.showSelectVideoError).toHaveBeenCalledWith(false); - }); - }); - }); - }); - describe('fileInputHooks', () => { - test('click calls current.click on the ref', () => { - jest.spyOn(hooks, hookKeys.handleVideoUpload).mockImplementationOnce(); - expect(hooks.handleVideoUpload).not.toHaveBeenCalled(); - hook = hooks.fileInputProps(); - expect(hooks.handleVideoUpload).toHaveBeenCalled(); - expect(appHooks.navigateTo).not.toHaveBeenCalled(); - hook.click(); - expect(appHooks.navigateTo).toHaveBeenCalled(); - }); - }); - describe('videoProps', () => { - const videoList = { - galleryProps: 'some gallery props', - selectBtnProps: 'some select btn props', - }; - const searchAndSortProps = { search: 'props' }; - const fileInput = { file: 'input hooks' }; - const videos = { video: { staTICUrl: '/assets/sOmEuiMAge' } }; - const spies = {}; - beforeEach(() => { - spies.videoList = jest.spyOn(hooks, hookKeys.videoListProps) - .mockReturnValueOnce(videoList); - spies.search = jest.spyOn(hooks, hookKeys.searchAndSortProps) - .mockReturnValueOnce(searchAndSortProps); - spies.file = jest.spyOn(hooks, hookKeys.fileInputProps) - .mockReturnValueOnce(fileInput); - hook = hooks.videoProps({ videos }); - }); - it('forwards fileInput as fileInput', () => { - expect(hook.fileInput).toEqual(fileInput); - expect(spies.file.mock.calls.length).toEqual(1); - expect(spies.file).toHaveBeenCalled(); - }); - it('initializes videoList', () => { - expect(spies.videoList.mock.calls.length).toEqual(1); - expect(spies.videoList).toHaveBeenCalledWith({ - searchSortProps: searchAndSortProps, - videos, - }); - }); - it('forwards searchAndSortHooks as searchSortProps', () => { - expect(hook.searchSortProps).toEqual(searchAndSortProps); - expect(spies.search.mock.calls.length).toEqual(1); - expect(spies.search).toHaveBeenCalled(); - }); - it('forwards galleryProps and selectBtnProps from the video list hooks', () => { - expect(hook.galleryProps).toEqual(videoList.galleryProps); - expect(hook.selectBtnProps).toEqual(videoList.selectBtnProps); - }); - }); - describe('handleCancel', () => { - it('calls navigateCallback', () => { - expect(hooks.handleCancel()).toEqual( - appHooks.navigateCallback({ - destination: reactRedux.useSelector(selectors.app.returnUrl), - analyticsEvent: analyticsEvt.videoGalleryCancelClick, - analytics: reactRedux.useSelector(selectors.app.analytics), - }), - ); - }); - }); -}); diff --git a/src/editors/containers/VideoGallery/index.jsx b/src/editors/containers/VideoGallery/index.jsx index 462a7371b..0600c1334 100644 --- a/src/editors/containers/VideoGallery/index.jsx +++ b/src/editors/containers/VideoGallery/index.jsx @@ -1,6 +1,5 @@ import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; +import { useSelector } from 'react-redux'; import { selectors } from '../../data/redux'; import hooks from './hooks'; import SelectionModal from '../../sharedComponents/SelectionModal'; @@ -8,15 +7,19 @@ import { acceptedImgKeys } from './utils'; import messages from './messages'; import { RequestKeys } from '../../data/constants/requests'; -export const VideoGallery = ({ - // redux - rawVideos, - isLoaded, - isFetchError, - isUploadError, -}) => { +export const VideoGallery = () => { + const rawVideos = useSelector(selectors.app.videos); + const isLoaded = useSelector( + (state) => selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }), + ); + const isFetchError = useSelector( + (state) => selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchVideos }), + ); + const isUploadError = useSelector( + (state) => selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadVideo }), + ); const videos = hooks.buildVideos({ rawVideos }); - const handleVideoUpload = hooks.handleVideoUpload(); + const handleVideoUpload = hooks.useVideoUploadHandler(); useEffect(() => { // If no videos exists redirects to the video upload screen @@ -31,8 +34,8 @@ export const VideoGallery = ({ galleryProps, searchSortProps, selectBtnProps, - } = hooks.videoProps({ videos }); - const handleCancel = hooks.handleCancel(); + } = hooks.useVideoProps({ videos }); + const handleCancel = hooks.useCancelHandler(); const modalMessages = { confirmMsg: messages.selectVideoButtonlabel, @@ -43,44 +46,28 @@ export const VideoGallery = ({ }; return ( -
- -
+ ); }; -VideoGallery.propTypes = { - rawVideos: PropTypes.shape({}).isRequired, - isLoaded: PropTypes.bool.isRequired, - isFetchError: PropTypes.bool.isRequired, - isUploadError: PropTypes.bool.isRequired, -}; +VideoGallery.propTypes = {}; -export const mapStateToProps = (state) => ({ - rawVideos: selectors.app.videos(state), - isLoaded: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }), - isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchVideos }), - isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadVideo }), -}); - -export const mapDispatchToProps = {}; - -export default connect(mapStateToProps, mapDispatchToProps)(VideoGallery); +export default VideoGallery; diff --git a/src/editors/containers/VideoGallery/index.test.jsx b/src/editors/containers/VideoGallery/index.test.jsx index ccbab262b..1ac21014d 100644 --- a/src/editors/containers/VideoGallery/index.test.jsx +++ b/src/editors/containers/VideoGallery/index.test.jsx @@ -1,104 +1,192 @@ +import { initializeMockApp } from '@edx/frontend-platform'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { configureStore } from '@reduxjs/toolkit'; +import '@testing-library/jest-dom/extend-expect'; import React from 'react'; -import { shallow, mount } from 'enzyme'; +import { + act, fireEvent, render, screen, +} from '@testing-library/react'; -import SelectionModal from '../../sharedComponents/SelectionModal'; -import hooks from './hooks'; -import * as module from '.'; +import { VideoGallery } from './index'; -jest.mock('../../sharedComponents/SelectionModal', () => 'SelectionModal'); +jest.unmock('react-redux'); +jest.unmock('@edx/frontend-platform/i18n'); +jest.unmock('@edx/paragon'); +jest.unmock('@edx/paragon/icons'); -const mockHandleVideoUploadHook = jest.fn(); - -jest.mock('./hooks', () => ({ - buildVideos: jest.fn(() => []), - videoProps: jest.fn(() => ({ - galleryError: { - show: 'ShoWERror gAlLery', - set: jest.fn(), - dismiss: jest.fn(), - message: { - id: 'Gallery error id', - defaultMessage: 'Gallery error', - description: 'Gallery error', - }, - }, - inputError: { - show: 'ShoWERror inPUT', - set: jest.fn(), - dismiss: jest.fn(), - message: { - id: 'Input error id', - defaultMessage: 'Input error', - description: 'Input error', - }, - }, - fileInput: { - addFile: 'videoHooks.fileInput.addFile', - click: 'videoHooks.fileInput.click', - ref: 'videoHooks.fileInput.ref', - }, - galleryProps: { gallery: 'props' }, - searchSortProps: { search: 'sortProps' }, - selectBtnProps: { select: 'btnProps' }, - })), - handleCancel: jest.fn(), - handleVideoUpload: () => mockHandleVideoUploadHook, -})); - -jest.mock('../../data/redux', () => ({ - selectors: { - requests: { - isLoaded: (state, { requestKey }) => ({ isLoaded: { state, requestKey } }), - isFetchError: (state, { requestKey }) => ({ isFetchError: { state, requestKey } }), - isUploadError: (state, { requestKey }) => ({ isUploadError: { state, requestKey } }), - }, +let store; +const initialVideos = [ + { + edx_video_id: 'id_1', + client_video_id: 'client_id_1', + course_video_image_url: 'course_video_image_url_1', + created: '2022-09-07T04:56:58.726Z', + status: 'Uploading', + duration: 3, + transcripts: [], }, -})); + { + edx_video_id: 'id_2', + client_video_id: 'client_id_2', + course_video_image_url: 'course_video_image_url_2', + created: '2022-11-07T04:56:58.726Z', + status: 'In Progress', + duration: 2, + transcripts: [], + }, { + edx_video_id: 'id_3', + client_video_id: 'client_id_3', + course_video_image_url: 'course_video_image_url_3', + created: '2022-01-07T04:56:58.726Z', + status: 'Ready', + duration: 4, + transcripts: [], + }, +]; -jest.mock('../../hooks', () => ({ - ...jest.requireActual('../../hooks'), - navigateCallback: jest.fn((args) => ({ navigateCallback: args })), -})); +// We are not using any style-based assertions and this function is very slow with jest-dom +window.getComputedStyle = () => ({ + getPropertyValue: () => undefined, +}); describe('VideoGallery', () => { describe('component', () => { - const props = { - rawVideos: { sOmEaSsET: { staTICUrl: '/video/sOmEaSsET' } }, - isLoaded: false, - isFetchError: false, - isUploadError: false, - }; - let el; - const videoProps = hooks.videoProps(); - beforeEach(() => { - el = shallow(); - mockHandleVideoUploadHook.mockReset(); + let oldLocation; + beforeEach(async () => { + store = configureStore({ + reducer: (state, action) => ((action && action.newState) ? action.newState : state), + preloadedState: { + app: { + videos: initialVideos, + learningContextId: 'course-v1:test+test+test', + blockId: 'some-block-id', + }, + requests: { + fetchVideos: { status: 'completed' }, + uploadVideo: { status: 'inactive' }, + }, + }, + }); + + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'test-user', + administrator: true, + roles: [], + }, + }); }); - it('provides confirm action, forwarding selectBtnProps from imgHooks', () => { - expect(el.find(SelectionModal).props().selectBtnProps).toEqual( - expect.objectContaining({ ...hooks.videoProps().selectBtnProps }), + beforeAll(() => { + oldLocation = window.location; + delete window.location; + window.location = { assign: jest.fn() }; + }); + afterAll(() => { + window.location = oldLocation; + }); + + function updateState({ videos = initialVideos, fetchVideos = 'completed', uploadVideos = 'inactive' }) { + store.dispatch({ + type: '', + newState: { + app: { + videos, + learningContextId: 'course-v1:test+test+test', + blockId: 'some-block-id', + }, + requests: { + fetchVideos: { status: fetchVideos }, + uploadVideo: { status: uploadVideos }, + }, + }, + }); + } + + async function renderComponent() { + return render( + + + , ); + } + + it('displays a list of videos', async () => { + await renderComponent(); + initialVideos.forEach(video => ( + expect(screen.getByText(video.client_video_id)).toBeInTheDocument() + )); }); - it('provides file upload button linked to fileInput.click', () => { - expect(el.find(SelectionModal).props().fileInput.click).toEqual( - videoProps.fileInput.click, - ); + it('navigates to video upload page when there are no videos', async () => { + expect(window.location.assign).not.toHaveBeenCalled(); + updateState({ videos: [] }); + await renderComponent(); + expect(window.location.assign).toHaveBeenCalled(); }); - it('provides a SearchSort component with searchSortProps from imgHooks', () => { - expect(el.find(SelectionModal).props().searchSortProps).toEqual(videoProps.searchSortProps); + it.each([ + [/by date added \(newest\)/i, [2, 1, 3]], + [/by date added \(oldest\)/i, [3, 1, 2]], + [/by name \(ascending\)/i, [1, 2, 3]], + [/by name \(descending\)/i, [3, 2, 1]], + [/by duration \(longest\)/i, [3, 1, 2]], + [/by duration \(shortest\)/i, [2, 1, 3]], + ])('videos can be sorted %s', async (sortBy, order) => { + await renderComponent(); + + fireEvent.click(screen.getByRole('button', { + name: /by date added \(newest\)/i, + })); + fireEvent.click(screen.getByRole('link', { + name: sortBy, + })); + const videoElements = screen.getAllByRole('button', { name: /client_id/ }); + order.forEach((clientIdSuffix, idx) => { + expect(videoElements[idx]).toHaveTextContent(`client_id_${clientIdSuffix}`); + }); }); - it('provides a Gallery component with galleryProps from imgHooks', () => { - expect(el.find(SelectionModal).props().galleryProps).toEqual(videoProps.galleryProps); + it.each([ + ['Uploading', 1, [1]], + ['Processing', 1, [2]], + ['Ready', 1, [3]], + ['Failed', 1, [4]], + ])('videos can be filtered by status %s', async (filterBy, length, items) => { + await renderComponent(); + updateState({ + videos: [...initialVideos, { + edx_video_id: 'id_4', + client_video_id: 'client_id_4', + course_video_image_url: 'course_video_image_url_4', + created: '2022-01-07T04:56:58.726Z', + status: 'Failed', + duration: 4, + transcripts: [], + }], + }); + + await act(async () => { + fireEvent.click(screen.getByRole('button', { + name: 'Video status', + })); + }); + await act(async () => { + fireEvent.click(screen.getByRole('checkbox', { + name: filterBy, + })); + }); + + const videoElements = await screen.findAllByRole('button', { name: /client_id/ }); + expect(videoElements).toHaveLength(length); + items.forEach(clientIdx => ( + expect(screen.getByText(`client_id_${clientIdx}`)).toBeInTheDocument() + )); }); - it('provides a FileInput component with fileInput props from imgHooks', () => { - expect(el.find(SelectionModal).props().fileInput).toMatchObject(videoProps.fileInput); - }); - it('handleVideoUpload called if there are no videos', () => { - el = mount(); - expect(mockHandleVideoUploadHook).not.toHaveBeenCalled(); - el.setProps({ rawVideos: {}, isLoaded: true }); - el.mount(); - expect(mockHandleVideoUploadHook).toHaveBeenCalled(); + + it('filters videos by search string', async () => { + await renderComponent(); + fireEvent.change(screen.getByRole('textbox'), { target: { value: 'CLIENT_ID_2' } }); + expect(screen.queryByText('client_id_2')).toBeInTheDocument(); + expect(screen.queryByText('client_id_1')).not.toBeInTheDocument(); + expect(screen.queryByText('client_id_3')).not.toBeInTheDocument(); }); }); }); diff --git a/src/editors/containers/VideoGallery/utils.js b/src/editors/containers/VideoGallery/utils.js index 989a84e80..8a1373948 100644 --- a/src/editors/containers/VideoGallery/utils.js +++ b/src/editors/containers/VideoGallery/utils.js @@ -22,15 +22,14 @@ export const sortMessages = StrictDict({ }); export const filterKeys = StrictDict({ - videoStatus: 'videoStatus', - uploading: 'uploading', - processing: 'processing', - ready: 'ready', - failed: 'failed', + uploading: 'Uploading', + processing: 'In Progress', + ready: 'Ready', + failed: 'Failed', }); export const filterMessages = StrictDict({ - videoStatus: messages[messageKeys.filterByVideoStatusNone], + title: messages[messageKeys.filterByVideoStatusNone], uploading: messages[messageKeys.filterByVideoStatusUploading], processing: messages[messageKeys.filterByVideoStatusProcessing], ready: messages[messageKeys.filterByVideoStatusReady], diff --git a/src/editors/sharedComponents/BaseModal/__snapshots__/index.test.jsx.snap b/src/editors/sharedComponents/BaseModal/__snapshots__/index.test.jsx.snap index aee237271..449a753f4 100644 --- a/src/editors/sharedComponents/BaseModal/__snapshots__/index.test.jsx.snap +++ b/src/editors/sharedComponents/BaseModal/__snapshots__/index.test.jsx.snap @@ -8,6 +8,7 @@ exports[`BaseModal ImageUploadModal template component snapshot 1`] = ` isOpen={true} onClose={[MockFunction props.close]} size="lg" + title="props.title node" variant="default" > diff --git a/src/editors/sharedComponents/SelectionModal/Gallery.jsx b/src/editors/sharedComponents/SelectionModal/Gallery.jsx index 8a543bb94..37e61e990 100644 --- a/src/editors/sharedComponents/SelectionModal/Gallery.jsx +++ b/src/editors/sharedComponents/SelectionModal/Gallery.jsx @@ -7,15 +7,13 @@ import { import { FormattedMessage, - injectIntl, - intlShape, + useIntl, } from '@edx/frontend-platform/i18n'; import messages from './messages'; import GalleryCard from './GalleryCard'; export const Gallery = ({ - show, galleryIsEmpty, searchIsEmpty, displayList, @@ -25,12 +23,8 @@ export const Gallery = ({ showIdsOnCards, height, isLoaded, - // injected - intl, }) => { - if (!show) { - return null; - } + const intl = useIntl(); if (!isLoaded) { return (
({ @@ -18,28 +19,28 @@ describe('TextEditor Image Gallery component', () => { describe('component', () => { const props = { galleryIsEmpty: false, + emptyGalleryLabel: { + id: 'emptyGalleryMsg', + defaultMessage: 'Empty Gallery', + }, searchIsEmpty: false, displayList: [{ id: 1 }, { id: 2 }, { id: 3 }], highlighted: 'props.highlighted', onHighlightChange: jest.fn().mockName('props.onHighlightChange'), - intl: { formatMessage }, isLoaded: true, }; + const shallowWithIntl = (component) => shallow({component}); test('snapshot: not loaded, show spinner', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallowWithIntl()).toMatchSnapshot(); }); test('snapshot: loaded but no images, show empty gallery', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallowWithIntl()).toMatchSnapshot(); }); test('snapshot: loaded but search returns no images, show 0 search result gallery', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallowWithIntl()).toMatchSnapshot(); }); test('snapshot: loaded, show gallery', () => { - expect(shallow()).toMatchSnapshot(); - }); - test('snapshot: not shot gallery', () => { - const wrapper = shallow(); - expect(wrapper.type()).toBeNull(); + expect(shallowWithIntl()).toMatchSnapshot(); }); }); }); diff --git a/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx b/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx index 85217ed1a..0a8c75353 100644 --- a/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx +++ b/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx @@ -16,23 +16,18 @@ export const GalleryCard = ({ asset, }) => ( -
-
+
)}
-
+

{asset.displayName}

{ asset.transcripts && (
@@ -86,7 +81,7 @@ GalleryCard.propTypes = { displayName: PropTypes.string, externalUrl: PropTypes.string, id: PropTypes.string, - dateAdded: PropTypes.number, + dateAdded: PropTypes.oneOfType([PropTypes.number, PropTypes.instanceOf(Date)]), locked: PropTypes.bool, portableUrl: PropTypes.string, thumbnail: PropTypes.string, @@ -94,7 +89,7 @@ GalleryCard.propTypes = { duration: PropTypes.number, status: PropTypes.string, statusBadgeVariant: PropTypes.string, - transcripts: PropTypes.shape([]), + transcripts: PropTypes.arrayOf(PropTypes.string), }).isRequired, }; diff --git a/src/editors/sharedComponents/SelectionModal/MultiSelectFilterDropdown.jsx b/src/editors/sharedComponents/SelectionModal/MultiSelectFilterDropdown.jsx new file mode 100644 index 000000000..b6ec2fd6c --- /dev/null +++ b/src/editors/sharedComponents/SelectionModal/MultiSelectFilterDropdown.jsx @@ -0,0 +1,38 @@ +import React from 'react'; + +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Dropdown, DropdownToggle, Form } from '@edx/paragon'; + +import PropTypes from 'prop-types'; +import { filterKeys, filterMessages } from '../../containers/VideoGallery/utils'; + +const MultiSelectFilterDropdown = ({ + selected, onSelectionChange, +}) => { + const intl = useIntl(); + return ( + + + {intl.formatMessage(filterMessages.title)} + + + {Object.keys(filterKeys).map(key => ( + + {intl.formatMessage(filterMessages[key])} + + ))} + + + ); +}; + +MultiSelectFilterDropdown.propTypes = { + selected: PropTypes.arrayOf(PropTypes.string).isRequired, + onSelectionChange: PropTypes.func.isRequired, +}; +export default MultiSelectFilterDropdown; diff --git a/src/editors/sharedComponents/SelectionModal/SearchSort.jsx b/src/editors/sharedComponents/SelectionModal/SearchSort.jsx index 762255877..d9b00e554 100644 --- a/src/editors/sharedComponents/SelectionModal/SearchSort.jsx +++ b/src/editors/sharedComponents/SelectionModal/SearchSort.jsx @@ -2,16 +2,17 @@ import React from 'react'; import PropTypes from 'prop-types'; import { - ActionRow, Dropdown, Form, Icon, IconButton, + ActionRow, Form, Icon, IconButton, SelectMenu, MenuItem, } from '@edx/paragon'; import { Close, Search } from '@edx/paragon/icons'; import { FormattedMessage, - injectIntl, - intlShape, + useIntl, } from '@edx/frontend-platform/i18n'; import messages from './messages'; +import MultiSelectFilterDropdown from './MultiSelectFilterDropdown'; +import { sortKeys, sortMessages } from '../../containers/VideoGallery/utils'; export const SearchSort = ({ searchString, @@ -19,28 +20,25 @@ export const SearchSort = ({ clearSearchString, sortBy, onSortClick, - sortKeys, - sortMessages, filterBy, onFilterClick, - filterKeys, - filterMessages, showSwitch, switchMessage, onSwitchClick, - // injected - intl, -}) => ( - - - { + const intl = useIntl(); + return ( + + + } - value={searchString} - /> - + value={searchString} + /> + - { !showSwitch && } - - - - - + { !showSwitch && } + {Object.keys(sortKeys).map(key => ( - + - + ))} - - + - { filterKeys && filterMessages && ( - - - - - - {Object.keys(filterKeys).map(key => ( - - - - ))} - - - )} + {onFilterClick && } - { showSwitch && ( - <> - - - - - - - - )} + { showSwitch && ( + <> + + + + + + + + )} - -); + + ); +}; SearchSort.defaultProps = { filterBy: '', onFilterClick: null, - filterKeys: null, - filterMessages: null, showSwitch: false, onSwitchClick: null, }; @@ -117,17 +96,11 @@ SearchSort.propTypes = { clearSearchString: PropTypes.func.isRequired, sortBy: PropTypes.string.isRequired, onSortClick: PropTypes.func.isRequired, - sortKeys: PropTypes.shape({}).isRequired, - sortMessages: PropTypes.shape({}).isRequired, - filterBy: PropTypes.string, + filterBy: PropTypes.arrayOf(PropTypes.string), onFilterClick: PropTypes.func, - filterKeys: PropTypes.shape({}), - filterMessages: PropTypes.shape({}), showSwitch: PropTypes.bool, switchMessage: PropTypes.shape({}).isRequired, onSwitchClick: PropTypes.func, - // injected - intl: intlShape.isRequired, }; -export default injectIntl(SearchSort); +export default SearchSort; diff --git a/src/editors/sharedComponents/SelectionModal/SearchSort.test.jsx b/src/editors/sharedComponents/SelectionModal/SearchSort.test.jsx index ccf8d36c8..74899d3a2 100644 --- a/src/editors/sharedComponents/SelectionModal/SearchSort.test.jsx +++ b/src/editors/sharedComponents/SelectionModal/SearchSort.test.jsx @@ -1,101 +1,89 @@ import React from 'react'; -import { shallow } from 'enzyme'; -import { Dropdown } from '@edx/paragon'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; -import { formatMessage } from '../../../testUtils'; +import '@testing-library/jest-dom'; +import { + act, fireEvent, render, screen, +} from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { sortKeys, sortMessages } from '../ImageUploadModal/SelectImageModal/utils'; -import { filterKeys, filterMessages } from '../../containers/VideoGallery/utils'; +import { filterMessages } from '../../containers/VideoGallery/utils'; import { SearchSort } from './SearchSort'; +import messages from './messages'; + +jest.unmock('react-redux'); +jest.unmock('@edx/frontend-platform/i18n'); +jest.unmock('@edx/paragon'); +jest.unmock('@edx/paragon/icons'); describe('SearchSort component', () => { - describe('snapshots without filterKeys', () => { - const props = { - searchString: 'props.searchString', - onSearchChange: jest.fn().mockName('props.onSearchChange'), - clearSearchString: jest.fn().mockName('props.clearSearchString'), - sortBy: sortKeys.dateOldest, - sortKeys, - sortMessages, - onSortClick: jest.fn().mockName('props.onSortClick'), - intl: { formatMessage }, - }; - test('with search string (close button)', () => { - expect(shallow()).toMatchSnapshot(); - }); - test('without search string (search icon)', () => { - expect(shallow()).toMatchSnapshot(); - }); - test('adds a sort option for each sortKey', () => { - const el = shallow(); - expect(el.find(Dropdown).containsMatchingElement( - , - )).toEqual(true); - expect(el.find(Dropdown).containsMatchingElement( - , - )).toEqual(true); - expect(el.find(Dropdown).containsMatchingElement( - , - )).toEqual(true); - expect(el.find(Dropdown).containsMatchingElement( - , - )).toEqual(true); + const props = { + searchString: '', + onSearchChange: jest.fn() + .mockName('props.onSearchChange'), + clearSearchString: jest.fn() + .mockName('props.clearSearchString'), + sortBy: sortKeys.dateOldest, + sortKeys, + sortMessages, + onSortClick: jest.fn() + .mockName('props.onSortClick'), + switchMessage: { + id: 'test.id', + defaultMessage: 'test message', + }, + onFilterClick: jest.fn(), + showSwitch: true, + }; + + function getComponent(overrideProps = {}) { + return render( + + + , + ); + } + + test('adds a sort option for each sortKey', async () => { + const { getByRole } = getComponent(); + await act(() => { + fireEvent.click(screen.getByRole('button', { + name: /by date added \(oldest\)/i, + })); }); + Object.values(sortMessages) + .forEach(({ defaultMessage }) => { + expect(getByRole('link', { name: defaultMessage })) + .toBeInTheDocument(); + }); }); - describe('snapshots with filterKeys', () => { - const props = { - searchString: 'props.searchString', - onSearchChange: jest.fn().mockName('props.onSearchChange'), - clearSearchString: jest.fn().mockName('props.clearSearchString'), - sortBy: sortKeys.dateOldest, - sortKeys, - sortMessages, - filterKeys, - filterMessages, - showSwitch: true, - onSortClick: jest.fn().mockName('props.onSortClick'), - onFilterClick: jest.fn().mockName('props.onFilterClick'), - intl: { formatMessage }, - }; - test('with search string (close button)', () => { - expect(shallow()).toMatchSnapshot(); + test('adds a sort option for each sortKey', async () => { + const { getByRole } = getComponent(); + await act(() => { + fireEvent.click(screen.getByRole('button', { name: /by date added \(oldest\)/i })); }); - test('without search string (search icon)', () => { - expect(shallow()).toMatchSnapshot(); - }); - test('adds a sort option for each sortKey', () => { - const el = shallow(); - expect(el.find(Dropdown).containsMatchingElement( - , - )).toEqual(true); - expect(el.find(Dropdown).containsMatchingElement( - , - )).toEqual(true); - expect(el.find(Dropdown).containsMatchingElement( - , - )).toEqual(true); - expect(el.find(Dropdown).containsMatchingElement( - , - )).toEqual(true); - }); - test('adds a filter option for each filterKet', () => { - const el = shallow(); - expect(el.find(Dropdown).containsMatchingElement( - , - )).toEqual(true); - expect(el.find(Dropdown).containsMatchingElement( - , - )).toEqual(true); - expect(el.find(Dropdown).containsMatchingElement( - , - )).toEqual(true); - expect(el.find(Dropdown).containsMatchingElement( - , - )).toEqual(true); - expect(el.find(Dropdown).containsMatchingElement( - , - )).toEqual(true); + Object.values(sortMessages) + .forEach(({ defaultMessage }) => { + expect(getByRole('link', { name: defaultMessage })) + .toBeInTheDocument(); + }); + }); + test('adds a filter option for each filterKet', async () => { + const { getByRole } = getComponent(); + await act(() => { + fireEvent.click(screen.getByRole('button', { name: /video status/i })); }); + Object.keys(filterMessages) + .forEach((key) => { + if (key !== 'title') { + expect(getByRole('checkbox', { name: filterMessages[key].defaultMessage })) + .toBeInTheDocument(); + } + }); + }); + test('searchbox should show clear message button when not empty', async () => { + const { queryByRole } = getComponent({ searchString: 'some string' }); + expect(queryByRole('button', { name: messages.clearSearch.defaultMessage })) + .toBeInTheDocument(); }); }); diff --git a/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap b/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap index 029cbbaf2..10cddbd47 100644 --- a/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap +++ b/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap @@ -1,104 +1,297 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TextEditor Image Gallery component component snapshot: loaded but no images, show empty gallery 1`] = ` -
- -
+ + `; exports[`TextEditor Image Gallery component component snapshot: loaded but search returns no images, show 0 search result gallery 1`] = ` -
- -
+ `; exports[`TextEditor Image Gallery component component snapshot: loaded, show gallery 1`] = ` - -
- - - - - -
-
+ + `; exports[`TextEditor Image Gallery component component snapshot: not loaded, show spinner 1`] = ` -
- -
+ `; diff --git a/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap b/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap index 71c91147a..769e70144 100644 --- a/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap +++ b/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap @@ -2,26 +2,18 @@ exports[`GalleryCard component snapshot with duration badge 1`] = `
- - - } - value="props.searchString" - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; - -exports[`SearchSort component snapshots with filterKeys without search string (search icon) 1`] = ` - - - } - value="" - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; - -exports[`SearchSort component snapshots without filterKeys with search string (close button) 1`] = ` - - - - } - value="props.searchString" - /> - - - - - - - - - - - - - - - - - - - - - - -`; - -exports[`SearchSort component snapshots without filterKeys without search string (search icon) 1`] = ` - - - } - value="" - /> - - - - - - - - - - - - - - - - - - - - - - -`; diff --git a/src/editors/sharedComponents/SelectionModal/index.jsx b/src/editors/sharedComponents/SelectionModal/index.jsx index 9ddd77e90..d57c717ed 100644 --- a/src/editors/sharedComponents/SelectionModal/index.jsx +++ b/src/editors/sharedComponents/SelectionModal/index.jsx @@ -5,8 +5,7 @@ import { Button, Stack } from '@edx/paragon'; import { Add } from '@edx/paragon/icons'; import { FormattedMessage, - injectIntl, - intlShape, + useIntl, } from '@edx/frontend-platform/i18n'; import BaseModal from '../BaseModal'; @@ -33,9 +32,8 @@ export const SelectionModal = ({ isLoaded, isFetchError, isUploadError, - // injected - intl, }) => { + const intl = useIntl(); const { confirmMsg, uploadButtonMsg, @@ -54,7 +52,6 @@ export const SelectionModal = ({ const galleryPropsValues = { isLoaded, - show: showGallery, ...galleryProps, }; return ( @@ -109,7 +106,7 @@ export const SelectionModal = ({ - + {showGallery && } @@ -155,8 +152,6 @@ SelectionModal.propTypes = { isLoaded: PropTypes.bool.isRequired, isFetchError: PropTypes.bool.isRequired, isUploadError: PropTypes.bool.isRequired, - // injected - intl: intlShape.isRequired, }; -export default injectIntl(SelectionModal); +export default SelectionModal; diff --git a/src/editors/sharedComponents/SelectionModal/index.test.jsx b/src/editors/sharedComponents/SelectionModal/index.test.jsx index 45a29f3f8..6f716c3ee 100644 --- a/src/editors/sharedComponents/SelectionModal/index.test.jsx +++ b/src/editors/sharedComponents/SelectionModal/index.test.jsx @@ -1,13 +1,14 @@ import React from 'react'; + import { IntlProvider } from '@edx/frontend-platform/i18n'; import { render, screen } from '@testing-library/react'; -import { formatMessage } from '../../../testUtils'; + import SelectionModal from '.'; import '@testing-library/jest-dom'; const props = { - isOpen: jest.fn(), - isClose: jest.fn(), + isOpen: true, + close: jest.fn(), size: 'fullscreen', isFullscreenScroll: false, galleryError: { @@ -35,7 +36,13 @@ const props = { click: 'imgHooks.fileInput.click', ref: 'imgHooks.fileInput.ref', }, - galleryProps: { gallery: 'props' }, + galleryProps: { + gallery: 'props', + emptyGalleryLabel: { + id: 'emptyGalleryMsg', + defaultMessage: 'Empty Gallery', + }, + }, searchSortProps: { search: 'sortProps' }, selectBtnProps: { select: 'btnProps' }, acceptedFiles: { png: '.png' }, @@ -69,7 +76,6 @@ const props = { isLoaded: true, isFetchError: false, isUploadError: false, - intl: { formatMessage }, }; const mockGalleryFn = jest.fn(); @@ -105,7 +111,7 @@ describe('Selection Modal', () => { }); test('rendering correctly with expected Input', async () => { render( - + , ); @@ -118,7 +124,6 @@ describe('Selection Modal', () => { expect.objectContaining({ ...props.galleryProps, isLoaded: props.isLoaded, - show: true, }), ); expect(mockFetchErrorAlertFn).toHaveBeenCalledWith( @@ -142,11 +147,11 @@ describe('Selection Modal', () => { }); test('rendering correctly with errors', () => { render( - + , ); - expect(screen.getByText('Gallery')).toBeInTheDocument(); + expect(screen.queryByText('Gallery')).not.toBeInTheDocument(); expect(screen.getByText('FileInput')).toBeInTheDocument(); expect(screen.getByText('FetchErrorAlert')).toBeInTheDocument(); expect(screen.getByText('UploadErrorAlert')).toBeInTheDocument(); @@ -157,17 +162,10 @@ describe('Selection Modal', () => { message: props.modalMessages.fetchError, }), ); - expect(mockGalleryFn).toHaveBeenCalledWith( - expect.objectContaining({ - ...props.galleryProps, - isLoaded: props.isLoaded, - show: false, - }), - ); }); test('rendering correctly with loading', () => { render( - + , ); @@ -180,7 +178,6 @@ describe('Selection Modal', () => { expect.objectContaining({ ...props.galleryProps, isLoaded: false, - show: true, }), ); }); diff --git a/src/editors/sharedComponents/SelectionModal/messages.js b/src/editors/sharedComponents/SelectionModal/messages.js index 10d18d0c2..2aa74f9e2 100644 --- a/src/editors/sharedComponents/SelectionModal/messages.js +++ b/src/editors/sharedComponents/SelectionModal/messages.js @@ -4,6 +4,11 @@ export const messages = { defaultMessage: 'Search', description: 'Placeholder text for search bar', }, + clearSearch: { + id: 'authoring.selectionmodal.search.clearSearchButton', + defaultMessage: 'Clear search query', + description: 'Button to clear search query', + }, emptySearchLabel: { id: 'authoring.selectionmodal.emptySearchLabel', defaultMessage: 'No search results.',