From fb7caffdd5ea4b39ff878440fa99656f1b209453 Mon Sep 17 00:00:00 2001 From: Kshitij Sobti Date: Mon, 7 Aug 2023 18:18:51 +0530 Subject: [PATCH] feat: Allow selecting my multiple filters in video gallery The sort and filter UI of the video gallery was not working, this fixes that issue, and also adds a new UI for filering videos that allows filtering videos to include more than one status. It also fixes the hooks related to VideoGallery to avoid potential bugs in the future and updates tests to use react testing library instead of enzyme. It also reduces the padding in gallery page. --- .../containers/EditorContainer/hooks.js | 7 +- src/editors/containers/VideoGallery/hooks.js | 114 ++--- .../containers/VideoGallery/hooks.test.js | 312 ------------ src/editors/containers/VideoGallery/index.jsx | 85 ++-- .../containers/VideoGallery/index.test.jsx | 260 ++++++---- src/editors/containers/VideoGallery/utils.js | 11 +- .../__snapshots__/index.test.jsx.snap | 1 + .../sharedComponents/BaseModal/index.jsx | 1 + .../SelectionModal/Gallery.jsx | 14 +- .../SelectionModal/Gallery.test.jsx | 21 +- .../SelectionModal/GalleryCard.jsx | 27 +- .../MultiSelectFilterDropdown.jsx | 38 ++ .../SelectionModal/SearchSort.jsx | 113 ++--- .../SelectionModal/SearchSort.test.jsx | 164 +++---- .../__snapshots__/Gallery.test.jsx.snap | 339 ++++++++++--- .../__snapshots__/GalleryCard.test.jsx.snap | 64 +-- .../__snapshots__/SearchSort.test.jsx.snap | 445 ------------------ .../sharedComponents/SelectionModal/index.jsx | 13 +- .../SelectionModal/index.test.jsx | 33 +- .../SelectionModal/messages.js | 5 + 20 files changed, 762 insertions(+), 1305 deletions(-) delete mode 100644 src/editors/containers/VideoGallery/hooks.test.js create mode 100644 src/editors/sharedComponents/SelectionModal/MultiSelectFilterDropdown.jsx delete mode 100644 src/editors/sharedComponents/SelectionModal/__snapshots__/SearchSort.test.jsx.snap 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.',