Merge pull request #368 from open-craft/kshitij/fix-video-sort-filter

fix: Video Gallery filters and sorting
This commit is contained in:
kenclary
2023-08-23 06:43:56 -07:00
committed by GitHub
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.',