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.
This commit is contained in:
Kshitij Sobti
2023-08-07 18:18:51 +05:30
parent e9c0f6cc82
commit fb7caffdd5
20 changed files with 762 additions and 1305 deletions

View File

@@ -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),

View File

@@ -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,
};

View File

@@ -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),
}),
);
});
});
});

View File

@@ -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 (
<div>
<SelectionModal
{...{
isOpen: true,
close: handleCancel,
size: 'fullscreen',
isFullscreenScroll: false,
galleryError,
inputError,
fileInput,
galleryProps,
searchSortProps,
selectBtnProps,
acceptedFiles: acceptedImgKeys,
modalMessages,
isLoaded,
isUploadError,
isFetchError,
}}
/>
</div>
<SelectionModal
{...{
isOpen: true,
close: handleCancel,
size: 'fullscreen',
isFullscreenScroll: false,
galleryError,
inputError,
fileInput,
galleryProps,
searchSortProps,
selectBtnProps,
acceptedFiles: acceptedImgKeys,
modalMessages,
isLoaded,
isUploadError,
isFetchError,
}}
/>
);
};
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;

View File

@@ -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(<module.VideoGallery {...props} />);
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(
<AppProvider store={store}>
<VideoGallery />
</AppProvider>,
);
}
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(<module.VideoGallery {...props} />);
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();
});
});
});

View File

@@ -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],

View File

@@ -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"
>
<ModalDialog.Header

View File

@@ -29,6 +29,7 @@ export const BaseModal = ({
hasCloseButton
isFullscreenOnMobile
isFullscreenScroll={isFullscreenScroll}
title={title}
>
<ModalDialog.Header style={{ zIndex: 1, boxShadow: '2px 2px 5px rgba(0, 0, 0, 0.3)' }}>
<ModalDialog.Title>

View File

@@ -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 (
<div style={{
@@ -96,8 +90,6 @@ Gallery.propTypes = {
emptyGalleryLabel: PropTypes.shape({}).isRequired,
showIdsOnCards: PropTypes.bool,
height: PropTypes.string,
// injected
intl: intlShape.isRequired,
};
export default injectIntl(Gallery);
export default Gallery;

View File

@@ -1,7 +1,8 @@
import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from '../../../testUtils';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { Gallery } from './Gallery';
jest.mock('../../data/redux', () => ({
@@ -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(<IntlProvider locale="en">{component}</IntlProvider>);
test('snapshot: not loaded, show spinner', () => {
expect(shallow(<Gallery {...props} isLoaded={false} />)).toMatchSnapshot();
expect(shallowWithIntl(<Gallery {...props} isLoaded={false} />)).toMatchSnapshot();
});
test('snapshot: loaded but no images, show empty gallery', () => {
expect(shallow(<Gallery {...props} galleryIsEmpty />)).toMatchSnapshot();
expect(shallowWithIntl(<Gallery {...props} galleryIsEmpty />)).toMatchSnapshot();
});
test('snapshot: loaded but search returns no images, show 0 search result gallery', () => {
expect(shallow(<Gallery {...props} searchIsEmpty />)).toMatchSnapshot();
expect(shallowWithIntl(<Gallery {...props} searchIsEmpty />)).toMatchSnapshot();
});
test('snapshot: loaded, show gallery', () => {
expect(shallow(<Gallery {...props} />)).toMatchSnapshot();
});
test('snapshot: not shot gallery', () => {
const wrapper = shallow(<Gallery {...props} show={false} />);
expect(wrapper.type()).toBeNull();
expect(shallowWithIntl(<Gallery {...props} />)).toMatchSnapshot();
});
});
});

View File

@@ -16,23 +16,18 @@ export const GalleryCard = ({
asset,
}) => (
<SelectableBox
className="card bg-white"
className="card bg-white shadow-none border-0 py-0"
key={asset.externalUrl}
type="radio"
value={asset.id}
style={{
padding: '10px 20px',
border: 'none',
boxShadow: 'none',
}}
>
<div className="card-div d-flex flex-row flex-nowrap">
<div style={{
position: 'relative',
width: '200px',
height: '100px',
margin: '18px 0 0 0',
}}
<div className="card-div d-flex flex-row flex-nowrap align-items-center">
<div
className="position-relative"
style={{
width: '200px',
height: '100px',
}}
>
<Image
style={{ border: 'none', width: '200px', height: '100px' }}
@@ -57,7 +52,7 @@ export const GalleryCard = ({
</Badge>
)}
</div>
<div className="card-text p-3" style={{ marginTop: '10px' }}>
<div className="card-text px-3 py-2" style={{ marginTop: '10px' }}>
<h3 className="text-primary-500">{asset.displayName}</h3>
{ asset.transcripts && (
<div style={{ margin: '0 0 5px 0' }}>
@@ -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,
};

View File

@@ -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 (
<Dropdown autoClose={false}>
<DropdownToggle variant="outline" id="gallery-filter">
{intl.formatMessage(filterMessages.title)}
</DropdownToggle>
<Dropdown.Menu renderOnMount className="p-2">
{Object.keys(filterKeys).map(key => (
<Dropdown.Item
key={key}
as={Form.Checkbox}
checked={selected.includes(key)}
onChange={onSelectionChange(key)}
>
<span className="p-1">{intl.formatMessage(filterMessages[key])}</span>
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
);
};
MultiSelectFilterDropdown.propTypes = {
selected: PropTypes.arrayOf(PropTypes.string).isRequired,
onSelectionChange: PropTypes.func.isRequired,
};
export default MultiSelectFilterDropdown;

View File

@@ -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,
}) => (
<ActionRow>
<Form.Group style={{ margin: 0 }}>
<Form.Control
autoFocus
onChange={onSearchChange}
placeholder={intl.formatMessage(messages.searchPlaceholder)}
trailingElement={
}) => {
const intl = useIntl();
return (
<ActionRow>
<Form.Group style={{ margin: 0 }}>
<Form.Control
autoFocus
onChange={onSearchChange}
placeholder={intl.formatMessage(messages.searchPlaceholder)}
trailingElement={
searchString
? (
<IconButton
alt={intl.formatMessage(messages.clearSearch)}
iconAs={Icon}
invertColors
isActive
@@ -51,62 +49,43 @@ export const SearchSort = ({
)
: <Icon src={Search} />
}
value={searchString}
/>
</Form.Group>
value={searchString}
/>
</Form.Group>
{ !showSwitch && <ActionRow.Spacer /> }
<Dropdown>
<Dropdown.Toggle className="text-gray-700" id="gallery-sort-button" variant="tertiary">
<FormattedMessage {...sortMessages[sortBy]} />
</Dropdown.Toggle>
<Dropdown.Menu>
{ !showSwitch && <ActionRow.Spacer /> }
<SelectMenu variant="link">
{Object.keys(sortKeys).map(key => (
<Dropdown.Item key={key} onClick={onSortClick(key)}>
<MenuItem key={key} onClick={onSortClick(key)} defaultSelected={key === sortBy}>
<FormattedMessage {...sortMessages[key]} />
</Dropdown.Item>
</MenuItem>
))}
</Dropdown.Menu>
</Dropdown>
</SelectMenu>
{ filterKeys && filterMessages && (
<Dropdown>
<Dropdown.Toggle className="text-gray-700" id="gallery-filter-button" variant="tertiary">
<FormattedMessage {...filterMessages[filterBy]} />
</Dropdown.Toggle>
<Dropdown.Menu>
{Object.keys(filterKeys).map(key => (
<Dropdown.Item key={key} onClick={onFilterClick(key)}>
<FormattedMessage {...filterMessages[key]} />
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
)}
{onFilterClick && <MultiSelectFilterDropdown selected={filterBy} onSelectionChange={onFilterClick} />}
{ showSwitch && (
<>
<ActionRow.Spacer />
<Form.SwitchSet
name="switch"
onChange={onSwitchClick}
isInline
>
<Form.Switch className="text-gray-700" value="switch-value" floatLabelLeft>
<FormattedMessage {...switchMessage} />
</Form.Switch>
</Form.SwitchSet>
</>
)}
{ showSwitch && (
<>
<ActionRow.Spacer />
<Form.SwitchSet
name="switch"
onChange={onSwitchClick}
isInline
>
<Form.Switch className="text-gray-700" value="switch-value" floatLabelLeft>
<FormattedMessage {...switchMessage} />
</Form.Switch>
</Form.SwitchSet>
</>
)}
</ActionRow>
);
</ActionRow>
);
};
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;

View File

@@ -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(<SearchSort {...props} />)).toMatchSnapshot();
});
test('without search string (search icon)', () => {
expect(shallow(<SearchSort {...props} searchString="" />)).toMatchSnapshot();
});
test('adds a sort option for each sortKey', () => {
const el = shallow(<SearchSort {...props} />);
expect(el.find(Dropdown).containsMatchingElement(
<FormattedMessage {...sortMessages.dateNewest} />,
)).toEqual(true);
expect(el.find(Dropdown).containsMatchingElement(
<FormattedMessage {...sortMessages.dateOldest} />,
)).toEqual(true);
expect(el.find(Dropdown).containsMatchingElement(
<FormattedMessage {...sortMessages.nameAscending} />,
)).toEqual(true);
expect(el.find(Dropdown).containsMatchingElement(
<FormattedMessage {...sortMessages.nameDescending} />,
)).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(
<IntlProvider locale="en">
<SearchSort {...props} {...overrideProps} />
</IntlProvider>,
);
}
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(<SearchSort {...props} />)).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(<SearchSort {...props} searchString="" />)).toMatchSnapshot();
});
test('adds a sort option for each sortKey', () => {
const el = shallow(<SearchSort {...props} />);
expect(el.find(Dropdown).containsMatchingElement(
<FormattedMessage {...sortMessages.dateNewest} />,
)).toEqual(true);
expect(el.find(Dropdown).containsMatchingElement(
<FormattedMessage {...sortMessages.dateOldest} />,
)).toEqual(true);
expect(el.find(Dropdown).containsMatchingElement(
<FormattedMessage {...sortMessages.nameAscending} />,
)).toEqual(true);
expect(el.find(Dropdown).containsMatchingElement(
<FormattedMessage {...sortMessages.nameDescending} />,
)).toEqual(true);
});
test('adds a filter option for each filterKet', () => {
const el = shallow(<SearchSort {...props} />);
expect(el.find(Dropdown).containsMatchingElement(
<FormattedMessage {...filterMessages.videoStatus} />,
)).toEqual(true);
expect(el.find(Dropdown).containsMatchingElement(
<FormattedMessage {...filterMessages.uploading} />,
)).toEqual(true);
expect(el.find(Dropdown).containsMatchingElement(
<FormattedMessage {...filterMessages.processing} />,
)).toEqual(true);
expect(el.find(Dropdown).containsMatchingElement(
<FormattedMessage {...filterMessages.ready} />,
)).toEqual(true);
expect(el.find(Dropdown).containsMatchingElement(
<FormattedMessage {...filterMessages.failed} />,
)).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();
});
});

View File

@@ -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`] = `
<div
className="gallery p-4 bg-light-400"
style={
<ContextProvider
value={
Object {
"height": "375px",
"margin": "0 -1.5rem",
"$t": [Function],
"defaultFormats": Object {},
"defaultLocale": "en",
"defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function],
"formatDateTimeRange": [Function],
"formatDateToParts": [Function],
"formatDisplayName": [Function],
"formatList": [Function],
"formatListToParts": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatNumberToParts": [Function],
"formatPlural": [Function],
"formatRelativeTime": [Function],
"formatTime": [Function],
"formatTimeToParts": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getDisplayNames": [Function],
"getListFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralRules": [Function],
"getRelativeTimeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"onError": [Function],
"onWarn": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": undefined,
"wrapRichTextChunksInFragment": undefined,
}
}
>
<FormattedMessage />
</div>
<Gallery
displayList={
Array [
Object {
"id": 1,
},
Object {
"id": 2,
},
Object {
"id": 3,
},
]
}
emptyGalleryLabel={
Object {
"defaultMessage": "Empty Gallery",
"id": "emptyGalleryMsg",
}
}
galleryIsEmpty={true}
height="375px"
highlighted="props.highlighted"
isLoaded={true}
onHighlightChange={[MockFunction props.onHighlightChange]}
searchIsEmpty={false}
show={true}
showIdsOnCards={false}
/>
</ContextProvider>
`;
exports[`TextEditor Image Gallery component component snapshot: loaded but search returns no images, show 0 search result gallery 1`] = `
<div
className="gallery p-4 bg-light-400"
style={
<ContextProvider
value={
Object {
"height": "375px",
"margin": "0 -1.5rem",
"$t": [Function],
"defaultFormats": Object {},
"defaultLocale": "en",
"defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function],
"formatDateTimeRange": [Function],
"formatDateToParts": [Function],
"formatDisplayName": [Function],
"formatList": [Function],
"formatListToParts": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatNumberToParts": [Function],
"formatPlural": [Function],
"formatRelativeTime": [Function],
"formatTime": [Function],
"formatTimeToParts": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getDisplayNames": [Function],
"getListFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralRules": [Function],
"getRelativeTimeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"onError": [Function],
"onWarn": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": undefined,
"wrapRichTextChunksInFragment": undefined,
}
}
>
<FormattedMessage
defaultMessage="No search results."
description="Label for when search returns nothing."
id="authoring.selectionmodal.emptySearchLabel"
<Gallery
displayList={
Array [
Object {
"id": 1,
},
Object {
"id": 2,
},
Object {
"id": 3,
},
]
}
emptyGalleryLabel={
Object {
"defaultMessage": "Empty Gallery",
"id": "emptyGalleryMsg",
}
}
galleryIsEmpty={false}
height="375px"
highlighted="props.highlighted"
isLoaded={true}
onHighlightChange={[MockFunction props.onHighlightChange]}
searchIsEmpty={true}
show={true}
showIdsOnCards={false}
/>
</div>
</ContextProvider>
`;
exports[`TextEditor Image Gallery component component snapshot: loaded, show gallery 1`] = `
<Scrollable
className="gallery bg-light-400"
style={
<ContextProvider
value={
Object {
"height": "375px",
"margin": "0 -1.5rem",
"$t": [Function],
"defaultFormats": Object {},
"defaultLocale": "en",
"defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function],
"formatDateTimeRange": [Function],
"formatDateToParts": [Function],
"formatDisplayName": [Function],
"formatList": [Function],
"formatListToParts": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatNumberToParts": [Function],
"formatPlural": [Function],
"formatRelativeTime": [Function],
"formatTime": [Function],
"formatTimeToParts": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getDisplayNames": [Function],
"getListFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralRules": [Function],
"getRelativeTimeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"onError": [Function],
"onWarn": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": undefined,
"wrapRichTextChunksInFragment": undefined,
}
}
>
<div
className="p-4"
>
<SelectableBox.Set
columns={1}
name="images"
onChange={[MockFunction props.onHighlightChange]}
type="radio"
value="props.highlighted"
>
<GalleryCard
asset={
Object {
"id": 1,
}
}
key="1"
showId={false}
/>
<GalleryCard
asset={
Object {
"id": 2,
}
}
key="2"
showId={false}
/>
<GalleryCard
asset={
Object {
"id": 3,
}
}
key="3"
showId={false}
/>
</SelectableBox.Set>
</div>
</Scrollable>
<Gallery
displayList={
Array [
Object {
"id": 1,
},
Object {
"id": 2,
},
Object {
"id": 3,
},
]
}
emptyGalleryLabel={
Object {
"defaultMessage": "Empty Gallery",
"id": "emptyGalleryMsg",
}
}
galleryIsEmpty={false}
height="375px"
highlighted="props.highlighted"
isLoaded={true}
onHighlightChange={[MockFunction props.onHighlightChange]}
searchIsEmpty={false}
show={true}
showIdsOnCards={false}
/>
</ContextProvider>
`;
exports[`TextEditor Image Gallery component component snapshot: not loaded, show spinner 1`] = `
<div
style={
<ContextProvider
value={
Object {
"left": "50%",
"position": "absolute",
"top": "50%",
"transform": "translate(-50%, -50%)",
"$t": [Function],
"defaultFormats": Object {},
"defaultLocale": "en",
"defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function],
"formatDateTimeRange": [Function],
"formatDateToParts": [Function],
"formatDisplayName": [Function],
"formatList": [Function],
"formatListToParts": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatNumberToParts": [Function],
"formatPlural": [Function],
"formatRelativeTime": [Function],
"formatTime": [Function],
"formatTimeToParts": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getDisplayNames": [Function],
"getListFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralRules": [Function],
"getRelativeTimeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"onError": [Function],
"onWarn": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": undefined,
"wrapRichTextChunksInFragment": undefined,
}
}
>
<Spinner
animation="border"
className="mie-3"
screenReaderText="loading..."
<Gallery
displayList={
Array [
Object {
"id": 1,
},
Object {
"id": 2,
},
Object {
"id": 3,
},
]
}
emptyGalleryLabel={
Object {
"defaultMessage": "Empty Gallery",
"id": "emptyGalleryMsg",
}
}
galleryIsEmpty={false}
height="375px"
highlighted="props.highlighted"
isLoaded={false}
onHighlightChange={[MockFunction props.onHighlightChange]}
searchIsEmpty={false}
show={true}
showIdsOnCards={false}
/>
</div>
</ContextProvider>
`;

View File

@@ -2,26 +2,18 @@
exports[`GalleryCard component snapshot with duration badge 1`] = `
<SelectableBox
className="card bg-white"
className="card bg-white shadow-none border-0 py-0"
key="props.img.externalUrl"
style={
Object {
"border": "none",
"boxShadow": "none",
"padding": "10px 20px",
}
}
type="radio"
>
<div
className="card-div d-flex flex-row flex-nowrap"
className="card-div d-flex flex-row flex-nowrap align-items-center"
>
<div
className="position-relative"
style={
Object {
"height": "100px",
"margin": "18px 0 0 0",
"position": "relative",
"width": "200px",
}
}
@@ -51,7 +43,7 @@ exports[`GalleryCard component snapshot with duration badge 1`] = `
</Component>
</div>
<div
className="card-text p-3"
className="card-text px-3 py-2"
style={
Object {
"marginTop": "10px",
@@ -94,26 +86,18 @@ exports[`GalleryCard component snapshot with duration badge 1`] = `
exports[`GalleryCard component snapshot with duration transcripts 1`] = `
<SelectableBox
className="card bg-white"
className="card bg-white shadow-none border-0 py-0"
key="props.img.externalUrl"
style={
Object {
"border": "none",
"boxShadow": "none",
"padding": "10px 20px",
}
}
type="radio"
>
<div
className="card-div d-flex flex-row flex-nowrap"
className="card-div d-flex flex-row flex-nowrap align-items-center"
>
<div
className="position-relative"
style={
Object {
"height": "100px",
"margin": "18px 0 0 0",
"position": "relative",
"width": "200px",
}
}
@@ -130,7 +114,7 @@ exports[`GalleryCard component snapshot with duration transcripts 1`] = `
/>
</div>
<div
className="card-text p-3"
className="card-text px-3 py-2"
style={
Object {
"marginTop": "10px",
@@ -188,26 +172,18 @@ exports[`GalleryCard component snapshot with duration transcripts 1`] = `
exports[`GalleryCard component snapshot with status badge 1`] = `
<SelectableBox
className="card bg-white"
className="card bg-white shadow-none border-0 py-0"
key="props.img.externalUrl"
style={
Object {
"border": "none",
"boxShadow": "none",
"padding": "10px 20px",
}
}
type="radio"
>
<div
className="card-div d-flex flex-row flex-nowrap"
className="card-div d-flex flex-row flex-nowrap align-items-center"
>
<div
className="position-relative"
style={
Object {
"height": "100px",
"margin": "18px 0 0 0",
"position": "relative",
"width": "200px",
}
}
@@ -236,7 +212,7 @@ exports[`GalleryCard component snapshot with status badge 1`] = `
</Component>
</div>
<div
className="card-text p-3"
className="card-text px-3 py-2"
style={
Object {
"marginTop": "10px",
@@ -279,26 +255,18 @@ exports[`GalleryCard component snapshot with status badge 1`] = `
exports[`GalleryCard component snapshot: dateAdded=12345 1`] = `
<SelectableBox
className="card bg-white"
className="card bg-white shadow-none border-0 py-0"
key="props.img.externalUrl"
style={
Object {
"border": "none",
"boxShadow": "none",
"padding": "10px 20px",
}
}
type="radio"
>
<div
className="card-div d-flex flex-row flex-nowrap"
className="card-div d-flex flex-row flex-nowrap align-items-center"
>
<div
className="position-relative"
style={
Object {
"height": "100px",
"margin": "18px 0 0 0",
"position": "relative",
"width": "200px",
}
}
@@ -315,7 +283,7 @@ exports[`GalleryCard component snapshot: dateAdded=12345 1`] = `
/>
</div>
<div
className="card-text p-3"
className="card-text px-3 py-2"
style={
Object {
"marginTop": "10px",

View File

@@ -1,445 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SearchSort component snapshots with filterKeys with search string (close button) 1`] = `
<ActionRow>
<Form.Group
style={
Object {
"margin": 0,
}
}
>
<Form.Control
autoFocus={true}
onChange={[MockFunction props.onSearchChange]}
placeholder="Search"
trailingElement={
<IconButton
iconAs="Icon"
invertColors={true}
isActive={true}
onClick={[MockFunction props.clearSearchString]}
size="sm"
src={[MockFunction icons.Close]}
/>
}
value="props.searchString"
/>
</Form.Group>
<Dropdown>
<Dropdown.Toggle
className="text-gray-700"
id="gallery-sort-button"
variant="tertiary"
>
<FormattedMessage
defaultMessage="By date added (oldest)"
description="Dropdown label for sorting by date (oldest)"
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
/>
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item
key="dateNewest"
>
<FormattedMessage
defaultMessage="By date added (newest)"
description="Dropdown label for sorting by date (newest)"
id="authoring.texteditor.selectimagemodal.sort.datenewest.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="dateOldest"
>
<FormattedMessage
defaultMessage="By date added (oldest)"
description="Dropdown label for sorting by date (oldest)"
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="nameAscending"
>
<FormattedMessage
defaultMessage="By name (ascending)"
description="Dropdown label for sorting by name (ascending)"
id="authoring.texteditor.selectimagemodal.sort.nameascending.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="nameDescending"
>
<FormattedMessage
defaultMessage="By name (descending)"
description="Dropdown label for sorting by name (descending)"
id="authoring.texteditor.selectimagemodal.sort.namedescending.label"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<Dropdown>
<Dropdown.Toggle
className="text-gray-700"
id="gallery-filter-button"
variant="tertiary"
>
<FormattedMessage />
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item
key="videoStatus"
>
<FormattedMessage
defaultMessage="Video status"
description="Dropdown label for filter by video status (none)"
id="authoring.selectvideomodal.filter.videostatusnone.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="uploading"
>
<FormattedMessage
defaultMessage="Uploading"
description="Dropdown label for filter by video status (uploading)"
id="authoring.selectvideomodal.filter.videostatusuploading.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="processing"
>
<FormattedMessage
defaultMessage="Processing"
description="Dropdown label for filter by video status (processing)"
id="authoring.selectvideomodal.filter.videostatusprocessing.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="ready"
>
<FormattedMessage
defaultMessage="Ready"
description="Dropdown label for filter by video status (ready)"
id="authoring.selectvideomodal.filter.videostatusready.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="failed"
>
<FormattedMessage
defaultMessage="Failed"
description="Dropdown label for filter by video status (failed)"
id="authoring.selectvideomodal.filter.videostatusfailed.label"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<ActionRow.Spacer />
<Component
isInline={true}
name="switch"
onChange={null}
>
<Component
className="text-gray-700"
floatLabelLeft={true}
value="switch-value"
>
<FormattedMessage />
</Component>
</Component>
</ActionRow>
`;
exports[`SearchSort component snapshots with filterKeys without search string (search icon) 1`] = `
<ActionRow>
<Form.Group
style={
Object {
"margin": 0,
}
}
>
<Form.Control
autoFocus={true}
onChange={[MockFunction props.onSearchChange]}
placeholder="Search"
trailingElement={<Icon />}
value=""
/>
</Form.Group>
<Dropdown>
<Dropdown.Toggle
className="text-gray-700"
id="gallery-sort-button"
variant="tertiary"
>
<FormattedMessage
defaultMessage="By date added (oldest)"
description="Dropdown label for sorting by date (oldest)"
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
/>
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item
key="dateNewest"
>
<FormattedMessage
defaultMessage="By date added (newest)"
description="Dropdown label for sorting by date (newest)"
id="authoring.texteditor.selectimagemodal.sort.datenewest.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="dateOldest"
>
<FormattedMessage
defaultMessage="By date added (oldest)"
description="Dropdown label for sorting by date (oldest)"
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="nameAscending"
>
<FormattedMessage
defaultMessage="By name (ascending)"
description="Dropdown label for sorting by name (ascending)"
id="authoring.texteditor.selectimagemodal.sort.nameascending.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="nameDescending"
>
<FormattedMessage
defaultMessage="By name (descending)"
description="Dropdown label for sorting by name (descending)"
id="authoring.texteditor.selectimagemodal.sort.namedescending.label"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<Dropdown>
<Dropdown.Toggle
className="text-gray-700"
id="gallery-filter-button"
variant="tertiary"
>
<FormattedMessage />
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item
key="videoStatus"
>
<FormattedMessage
defaultMessage="Video status"
description="Dropdown label for filter by video status (none)"
id="authoring.selectvideomodal.filter.videostatusnone.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="uploading"
>
<FormattedMessage
defaultMessage="Uploading"
description="Dropdown label for filter by video status (uploading)"
id="authoring.selectvideomodal.filter.videostatusuploading.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="processing"
>
<FormattedMessage
defaultMessage="Processing"
description="Dropdown label for filter by video status (processing)"
id="authoring.selectvideomodal.filter.videostatusprocessing.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="ready"
>
<FormattedMessage
defaultMessage="Ready"
description="Dropdown label for filter by video status (ready)"
id="authoring.selectvideomodal.filter.videostatusready.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="failed"
>
<FormattedMessage
defaultMessage="Failed"
description="Dropdown label for filter by video status (failed)"
id="authoring.selectvideomodal.filter.videostatusfailed.label"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<ActionRow.Spacer />
<Component
isInline={true}
name="switch"
onChange={null}
>
<Component
className="text-gray-700"
floatLabelLeft={true}
value="switch-value"
>
<FormattedMessage />
</Component>
</Component>
</ActionRow>
`;
exports[`SearchSort component snapshots without filterKeys with search string (close button) 1`] = `
<ActionRow>
<Form.Group
style={
Object {
"margin": 0,
}
}
>
<Form.Control
autoFocus={true}
onChange={[MockFunction props.onSearchChange]}
placeholder="Search"
trailingElement={
<IconButton
iconAs="Icon"
invertColors={true}
isActive={true}
onClick={[MockFunction props.clearSearchString]}
size="sm"
src={[MockFunction icons.Close]}
/>
}
value="props.searchString"
/>
</Form.Group>
<ActionRow.Spacer />
<Dropdown>
<Dropdown.Toggle
className="text-gray-700"
id="gallery-sort-button"
variant="tertiary"
>
<FormattedMessage
defaultMessage="By date added (oldest)"
description="Dropdown label for sorting by date (oldest)"
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
/>
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item
key="dateNewest"
>
<FormattedMessage
defaultMessage="By date added (newest)"
description="Dropdown label for sorting by date (newest)"
id="authoring.texteditor.selectimagemodal.sort.datenewest.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="dateOldest"
>
<FormattedMessage
defaultMessage="By date added (oldest)"
description="Dropdown label for sorting by date (oldest)"
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="nameAscending"
>
<FormattedMessage
defaultMessage="By name (ascending)"
description="Dropdown label for sorting by name (ascending)"
id="authoring.texteditor.selectimagemodal.sort.nameascending.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="nameDescending"
>
<FormattedMessage
defaultMessage="By name (descending)"
description="Dropdown label for sorting by name (descending)"
id="authoring.texteditor.selectimagemodal.sort.namedescending.label"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</ActionRow>
`;
exports[`SearchSort component snapshots without filterKeys without search string (search icon) 1`] = `
<ActionRow>
<Form.Group
style={
Object {
"margin": 0,
}
}
>
<Form.Control
autoFocus={true}
onChange={[MockFunction props.onSearchChange]}
placeholder="Search"
trailingElement={<Icon />}
value=""
/>
</Form.Group>
<ActionRow.Spacer />
<Dropdown>
<Dropdown.Toggle
className="text-gray-700"
id="gallery-sort-button"
variant="tertiary"
>
<FormattedMessage
defaultMessage="By date added (oldest)"
description="Dropdown label for sorting by date (oldest)"
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
/>
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item
key="dateNewest"
>
<FormattedMessage
defaultMessage="By date added (newest)"
description="Dropdown label for sorting by date (newest)"
id="authoring.texteditor.selectimagemodal.sort.datenewest.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="dateOldest"
>
<FormattedMessage
defaultMessage="By date added (oldest)"
description="Dropdown label for sorting by date (oldest)"
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="nameAscending"
>
<FormattedMessage
defaultMessage="By name (ascending)"
description="Dropdown label for sorting by name (ascending)"
id="authoring.texteditor.selectimagemodal.sort.nameascending.label"
/>
</Dropdown.Item>
<Dropdown.Item
key="nameDescending"
>
<FormattedMessage
defaultMessage="By name (descending)"
description="Dropdown label for sorting by name (descending)"
id="authoring.texteditor.selectimagemodal.sort.namedescending.label"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</ActionRow>
`;

View File

@@ -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 = ({
<FormattedMessage {...galleryError.message} />
</ErrorAlert>
<Stack gap={2}>
<Gallery {...galleryPropsValues} />
{showGallery && <Gallery {...galleryPropsValues} />}
<FileInput fileInput={fileInput} acceptedFiles={Object.values(acceptedFiles).join()} />
</Stack>
</BaseModal>
@@ -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;

View File

@@ -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(
<IntlProvider>
<IntlProvider locale="en">
<SelectionModal {...props} />
</IntlProvider>,
);
@@ -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(
<IntlProvider>
<IntlProvider locale="en">
<SelectionModal {...props} isFetchError />
</IntlProvider>,
);
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(
<IntlProvider>
<IntlProvider locale="en">
<SelectionModal {...props} isLoaded={false} />
</IntlProvider>,
);
@@ -180,7 +178,6 @@ describe('Selection Modal', () => {
expect.objectContaining({
...props.galleryProps,
isLoaded: false,
show: true,
}),
);
});

View File

@@ -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.',