@@ -0,0 +1,17 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VideoEditor snapshots renders as expected with default behavior 1`] = `
|
||||
<EditorContainer
|
||||
getContent={[Function]}
|
||||
onClose={[MockFunction props.onClose]}
|
||||
validateEntry={[MockFunction validateEntry]}
|
||||
>
|
||||
<div
|
||||
className="video-editor"
|
||||
>
|
||||
<VideoEditorModal
|
||||
error="errORsHooKErroR"
|
||||
/>
|
||||
</div>
|
||||
</EditorContainer>
|
||||
`;
|
||||
@@ -1,21 +1,26 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { selectors } from '../../data/redux';
|
||||
|
||||
import EditorContainer from '../EditorContainer';
|
||||
import VideoEditorModal from './components/VideoEditorModal';
|
||||
import * as hooks from './hooks';
|
||||
import { errorsHook } from './hooks';
|
||||
|
||||
export default function VideoEditor({
|
||||
export const VideoEditor = ({
|
||||
onClose,
|
||||
}) {
|
||||
// redux
|
||||
videoSettings,
|
||||
}) => {
|
||||
const {
|
||||
error,
|
||||
validateEntry,
|
||||
} = hooks.errorsHook();
|
||||
} = errorsHook();
|
||||
|
||||
return (
|
||||
<EditorContainer
|
||||
getContent={() => ({})}
|
||||
getContent={() => videoSettings}
|
||||
onClose={onClose}
|
||||
validateEntry={validateEntry}
|
||||
>
|
||||
@@ -24,11 +29,43 @@ export default function VideoEditor({
|
||||
</div>
|
||||
</EditorContainer>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
VideoEditor.defaultProps = {
|
||||
onClose: null,
|
||||
videoSettings: null,
|
||||
};
|
||||
VideoEditor.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
// redux
|
||||
videoSettings: PropTypes.shape({
|
||||
videoSource: PropTypes.string,
|
||||
fallbackVideos: PropTypes.arrayOf(PropTypes.string),
|
||||
allowVideoDownloads: PropTypes.bool,
|
||||
thumbnail: PropTypes.string,
|
||||
transcripts: PropTypes.objectOf(PropTypes.string),
|
||||
allowTranscriptDownloads: PropTypes.bool,
|
||||
duration: PropTypes.shape({
|
||||
startTime: PropTypes.number,
|
||||
stopTime: PropTypes.number,
|
||||
total: PropTypes.number,
|
||||
}),
|
||||
showTranscriptByDefult: PropTypes.bool,
|
||||
handout: PropTypes.string,
|
||||
licenseType: PropTypes.string,
|
||||
licenseDetails: PropTypes.shape({
|
||||
attribution: PropTypes.bool,
|
||||
noncommercial: PropTypes.bool,
|
||||
noDerivatives: PropTypes.bool,
|
||||
shareAlike: PropTypes.bool,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
videoSettings: selectors.video.videoSettings(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(VideoEditor);
|
||||
|
||||
56
src/editors/containers/VideoEditor/index.test.jsx
Normal file
56
src/editors/containers/VideoEditor/index.test.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { selectors } from '../../data/redux';
|
||||
import { errorsHook } from './hooks';
|
||||
import { VideoEditor, mapStateToProps, mapDispatchToProps } from '.';
|
||||
|
||||
jest.mock('../EditorContainer', () => 'EditorContainer');
|
||||
jest.mock('./components/VideoEditorModal', () => 'VideoEditorModal');
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
errorsHook: jest.fn(() => ({
|
||||
error: 'hooks.errorsHook.error',
|
||||
validateEntry: jest.fn().mockName('validateEntry'),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../../data/redux', () => ({
|
||||
selectors: {
|
||||
video: {
|
||||
videoSettings: state => ({ videoSettings: { state } }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('VideoEditor', () => {
|
||||
const props = {
|
||||
onClose: jest.fn().mockName('props.onClose'),
|
||||
// redux
|
||||
videoSettings: 'vIdEOsETtings',
|
||||
};
|
||||
errorsHook.mockReturnValue({
|
||||
error: 'errORsHooKErroR',
|
||||
validateEntry: jest.fn().mockName('validateEntry'),
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
test('renders as expected with default behavior', () => {
|
||||
expect(shallow(<VideoEditor {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { some: 'testState' };
|
||||
test('loads videoSettings from videoSettings selector', () => {
|
||||
expect(mapStateToProps(testState).videoSettings).toEqual(
|
||||
selectors.video.videoSettings(testState),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('is empty', () => {
|
||||
expect(mapDispatchToProps).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -46,8 +46,40 @@ export const getTranscriptDownloadUrl = createSelector(
|
||||
}),
|
||||
);
|
||||
|
||||
export const videoSettings = createSelector(
|
||||
Object.values(module.simpleSelectors),
|
||||
(
|
||||
videoSource,
|
||||
fallbackVideos,
|
||||
allowVideoDownloads,
|
||||
thumbnail,
|
||||
transcripts,
|
||||
allowTranscriptDownloads,
|
||||
duration,
|
||||
showTranscriptByDefault,
|
||||
handout,
|
||||
licenseType,
|
||||
licenseDetails,
|
||||
) => (
|
||||
{
|
||||
videoSource,
|
||||
fallbackVideos,
|
||||
allowVideoDownloads,
|
||||
thumbnail,
|
||||
transcripts,
|
||||
allowTranscriptDownloads,
|
||||
duration,
|
||||
showTranscriptByDefault,
|
||||
handout,
|
||||
licenseType,
|
||||
licenseDetails,
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
export default {
|
||||
openLanguages,
|
||||
getTranscriptDownloadUrl,
|
||||
...simpleSelectors,
|
||||
videoSettings,
|
||||
};
|
||||
|
||||
@@ -68,13 +68,43 @@ export const apiMethods = {
|
||||
if (blockType === 'html') {
|
||||
return {
|
||||
category: blockType,
|
||||
couseKey: learningContextId,
|
||||
courseKey: learningContextId,
|
||||
data: content,
|
||||
has_changes: true,
|
||||
id: blockId,
|
||||
metadata: { display_name: title },
|
||||
};
|
||||
}
|
||||
if (blockType === 'video') {
|
||||
const {
|
||||
html5Sources,
|
||||
edxVideoId,
|
||||
youtubeId,
|
||||
} = module.processVideoIds({
|
||||
videoSource: content.videoSource,
|
||||
fallbackVideos: content.fallbackVideos,
|
||||
});
|
||||
return {
|
||||
category: blockType,
|
||||
courseKey: learningContextId,
|
||||
display_name: title,
|
||||
id: blockId,
|
||||
metadata: {
|
||||
display_name: title,
|
||||
download_video: content.allowVideoDownloads,
|
||||
edx_video_id: edxVideoId,
|
||||
html5_sources: html5Sources,
|
||||
youtube_id_1_0: youtubeId,
|
||||
download_track: content.allowTranscriptDownloads,
|
||||
track: '', // TODO Downloadable Transcript URL. Backend expects a file name, for example: "something.srt"
|
||||
show_captions: content.showTranscriptByDefault,
|
||||
handout: content.handout,
|
||||
start_time: content.duration.startTime,
|
||||
end_time: content.duration.stopTime,
|
||||
license: module.processLicense(content.licenseType, content.licenseDetails),
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new TypeError(`No Block in V2 Editors named /"${blockType}/", Cannot Save Content.`);
|
||||
},
|
||||
saveBlock: ({
|
||||
@@ -106,6 +136,56 @@ export const loadImages = (rawImages) => camelizeKeys(rawImages).reduce(
|
||||
{},
|
||||
);
|
||||
|
||||
export const processVideoIds = ({ videoSource, fallbackVideos }) => {
|
||||
let edxVideoId = '';
|
||||
let youtubeId = '';
|
||||
const html5Sources = [];
|
||||
|
||||
if (module.isEdxVideo(videoSource)) {
|
||||
edxVideoId = videoSource;
|
||||
} else if (module.parseYoutubeId(videoSource)) {
|
||||
youtubeId = module.parseYoutubeId(videoSource);
|
||||
} else {
|
||||
html5Sources.push(videoSource);
|
||||
}
|
||||
|
||||
fallbackVideos.forEach((src) => html5Sources.push(src));
|
||||
|
||||
return {
|
||||
edxVideoId,
|
||||
html5Sources,
|
||||
youtubeId,
|
||||
};
|
||||
};
|
||||
|
||||
export const isEdxVideo = (src) => {
|
||||
const uuid4Regex = new RegExp(/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/);
|
||||
if (src.match(uuid4Regex)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const parseYoutubeId = (src) => {
|
||||
const youtubeRegex = new RegExp(/^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w-]+\?v=|embed\/|v\/)?)([\w-]+)(\S+)?$/);
|
||||
if (!src.match(youtubeRegex)) {
|
||||
return null;
|
||||
}
|
||||
return src.match(youtubeRegex)[5];
|
||||
};
|
||||
|
||||
export const processLicense = (licenseType, licenseDetails) => {
|
||||
if (licenseType === 'all-rights-reserved') {
|
||||
return 'all-rights-reserved';
|
||||
}
|
||||
return 'creative-commons: ver=4.0'.concat(
|
||||
(licenseDetails.attribution ? ' BY' : ''),
|
||||
(licenseDetails.noncommercial ? ' NC' : ''),
|
||||
(licenseDetails.noDerivatives ? ' ND' : ''),
|
||||
(licenseDetails.shareAlike ? ' SA' : ''),
|
||||
);
|
||||
};
|
||||
|
||||
export const checkMockApi = (key) => {
|
||||
if (process.env.REACT_APP_DEVGALLERY) {
|
||||
return mockApi[key];
|
||||
|
||||
@@ -32,12 +32,14 @@ const { camelize } = utils;
|
||||
const { apiMethods } = api;
|
||||
|
||||
const blockId = 'coursev1:2uX@4345432';
|
||||
const content = 'Im baby palo santo ugh celiac fashion axe. La croix lo-fi venmo whatever. Beard man braid migas single-origin coffee forage ramps.';
|
||||
const learningContextId = 'demo2uX';
|
||||
const studioEndpointUrl = 'hortus.coa';
|
||||
const title = 'remember this needs to go into metadata to save';
|
||||
|
||||
describe('cms api', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('apiMethods', () => {
|
||||
describe('fetchBlockId', () => {
|
||||
it('should call get with url.blocks', () => {
|
||||
@@ -69,6 +71,7 @@ describe('cms api', () => {
|
||||
|
||||
describe('normalizeContent', () => {
|
||||
test('return value for blockType: html', () => {
|
||||
const content = 'Im baby palo santo ugh celiac fashion axe. La croix lo-fi venmo whatever. Beard man braid migas single-origin coffee forage ramps.';
|
||||
expect(apiMethods.normalizeContent({
|
||||
blockId,
|
||||
blockType: 'html',
|
||||
@@ -77,13 +80,68 @@ describe('cms api', () => {
|
||||
title,
|
||||
})).toEqual({
|
||||
category: 'html',
|
||||
couseKey: learningContextId,
|
||||
courseKey: learningContextId,
|
||||
data: content,
|
||||
has_changes: true,
|
||||
id: blockId,
|
||||
metadata: { display_name: title },
|
||||
});
|
||||
});
|
||||
test('return value for blockType: video', () => {
|
||||
const content = {
|
||||
videoSource: 'viDeOSouRCE',
|
||||
fallbackVideos: 'FalLBacKVidEOs',
|
||||
allowVideoDownloads: 'alLOwViDeodownLOads',
|
||||
thumbnail: 'THUmbNaIL',
|
||||
transcripts: 'traNScRiPts',
|
||||
allowTranscriptDownloads: 'aLloWTRaNScriPtdoWnlOADS',
|
||||
duration: {
|
||||
startTime: 'StArTTime',
|
||||
stopTime: 'sToPTiME',
|
||||
},
|
||||
showTranscriptByDefault: 'ShOWtrANscriPTBYDeFAulT',
|
||||
handout: 'HAnDOuT',
|
||||
licenseType: 'LiCeNsETYpe',
|
||||
licenseDetails: 'liCENSeDetAIls',
|
||||
};
|
||||
const html5Sources = 'hTML5souRCES';
|
||||
const edxVideoId = 'eDXviDEOid';
|
||||
const youtubeId = 'yOUtUBeid';
|
||||
const license = 'LiCEnsE';
|
||||
jest.spyOn(api, 'processVideoIds').mockReturnValue({
|
||||
html5Sources,
|
||||
edxVideoId,
|
||||
youtubeId,
|
||||
});
|
||||
jest.spyOn(api, 'processLicense').mockReturnValue(license);
|
||||
expect(apiMethods.normalizeContent({
|
||||
blockId,
|
||||
blockType: 'video',
|
||||
content,
|
||||
learningContextId,
|
||||
title,
|
||||
})).toEqual({
|
||||
category: 'video',
|
||||
courseKey: learningContextId,
|
||||
display_name: title,
|
||||
id: blockId,
|
||||
metadata: {
|
||||
display_name: title,
|
||||
download_video: content.allowVideoDownloads,
|
||||
edx_video_id: edxVideoId,
|
||||
html5_sources: html5Sources,
|
||||
youtube_id_1_0: youtubeId,
|
||||
download_track: content.allowTranscriptDownloads,
|
||||
track: '',
|
||||
show_captions: content.showTranscriptByDefault,
|
||||
handout: content.handout,
|
||||
start_time: content.duration.startTime,
|
||||
end_time: content.duration.stopTime,
|
||||
license,
|
||||
},
|
||||
});
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
test('throw error for invalid blockType', () => {
|
||||
expect(() => { apiMethods.normalizeContent({ blockType: 'somethingINVALID' }); })
|
||||
.toThrow(TypeError);
|
||||
@@ -91,6 +149,7 @@ describe('cms api', () => {
|
||||
});
|
||||
|
||||
describe('saveBlock', () => {
|
||||
const content = 'Im baby palo santo ugh celiac fashion axe. La croix lo-fi venmo whatever. Beard man braid migas single-origin coffee forage ramps.';
|
||||
it('should call post with urls.block and normalizeContent', () => {
|
||||
apiMethods.saveBlock({
|
||||
blockId,
|
||||
@@ -201,4 +260,151 @@ describe('cms api', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('processVideoIds', () => {
|
||||
const edxVideoId = 'eDXviDEoid';
|
||||
const youtubeId = 'yOuTuBeID';
|
||||
const html5Sources = [
|
||||
'sOuRce1',
|
||||
'sourCE2',
|
||||
];
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
describe('if the videoSource is an edx video id', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(api, 'isEdxVideo').mockReturnValue(true);
|
||||
jest.spyOn(api, 'parseYoutubeId').mockReturnValue(null);
|
||||
});
|
||||
it('returns edxVideoId when there are no fallbackVideos', () => {
|
||||
expect(api.processVideoIds({
|
||||
videoSource: edxVideoId,
|
||||
fallbackVideos: [],
|
||||
})).toEqual({
|
||||
edxVideoId,
|
||||
html5Sources: [],
|
||||
youtubeId: '',
|
||||
});
|
||||
});
|
||||
it('returns edxVideoId and html5Sources when there are fallbackVideos', () => {
|
||||
expect(api.processVideoIds({
|
||||
videoSource: edxVideoId,
|
||||
fallbackVideos: html5Sources,
|
||||
})).toEqual({
|
||||
edxVideoId,
|
||||
html5Sources,
|
||||
youtubeId: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('if the videoSource is a youtube url', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(api, 'isEdxVideo').mockReturnValue(false);
|
||||
jest.spyOn(api, 'parseYoutubeId').mockReturnValue(youtubeId);
|
||||
});
|
||||
it('returns youtubeId when there are no fallbackVideos', () => {
|
||||
expect(api.processVideoIds({
|
||||
videoSource: edxVideoId,
|
||||
fallbackVideos: [],
|
||||
})).toEqual({
|
||||
edxVideoId: '',
|
||||
html5Sources: [],
|
||||
youtubeId,
|
||||
});
|
||||
});
|
||||
it('returns youtubeId and html5Sources when there are fallbackVideos', () => {
|
||||
expect(api.processVideoIds({
|
||||
videoSource: edxVideoId,
|
||||
fallbackVideos: html5Sources,
|
||||
})).toEqual({
|
||||
edxVideoId: '',
|
||||
html5Sources,
|
||||
youtubeId,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('if the videoSource is an html5 source', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(api, 'isEdxVideo').mockReturnValue(false);
|
||||
jest.spyOn(api, 'parseYoutubeId').mockReturnValue(null);
|
||||
});
|
||||
it('returns html5Sources when there are no fallbackVideos', () => {
|
||||
expect(api.processVideoIds({
|
||||
videoSource: html5Sources[0],
|
||||
fallbackVideos: [],
|
||||
})).toEqual({
|
||||
edxVideoId: '',
|
||||
html5Sources: [html5Sources[0]],
|
||||
youtubeId: '',
|
||||
});
|
||||
});
|
||||
it('returns html5Sources when there are fallbackVideos', () => {
|
||||
expect(api.processVideoIds({
|
||||
videoSource: html5Sources[0],
|
||||
fallbackVideos: [html5Sources[1]],
|
||||
})).toEqual({
|
||||
edxVideoId: '',
|
||||
html5Sources,
|
||||
youtubeId: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('isEdxVideo', () => {
|
||||
it('returns true if id is in uuid4 format', () => {
|
||||
const id = 'c2afd8c8-3329-4dfc-95be-4ee6d986c3e5';
|
||||
expect(api.isEdxVideo(id)).toEqual(true);
|
||||
});
|
||||
it('returns false if id is not in uuid4 format', () => {
|
||||
const id = 'someB-ad-Id';
|
||||
expect(api.isEdxVideo(id)).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('parseYoutubeId', () => {
|
||||
it('returns the youtube id in an url', () => {
|
||||
const id = '3_yD_cEKoCk';
|
||||
const testURLs = [
|
||||
'https://www.youtube.com/watch?v=3_yD_cEKoCk&feature=featured',
|
||||
'https://www.youtube.com/watch?v=3_yD_cEKoCk',
|
||||
'http://www.youtube.com/watch?v=3_yD_cEKoCk',
|
||||
'//www.youtube.com/watch?v=3_yD_cEKoCk',
|
||||
'www.youtube.com/watch?v=3_yD_cEKoCk',
|
||||
'https://youtube.com/watch?v=3_yD_cEKoCk',
|
||||
'http://youtube.com/watch?v=3_yD_cEKoCk',
|
||||
'//youtube.com/watch?v=3_yD_cEKoCk',
|
||||
'youtube.com/watch?v=3_yD_cEKoCk',
|
||||
'https://m.youtube.com/watch?v=3_yD_cEKoCk',
|
||||
'http://m.youtube.com/watch?v=3_yD_cEKoCk',
|
||||
'//m.youtube.com/watch?v=3_yD_cEKoCk',
|
||||
'm.youtube.com/watch?v=3_yD_cEKoCk',
|
||||
'https://www.youtube.com/v/3_yD_cEKoCk?fs=1&hl=en_US',
|
||||
'http://www.youtube.com/v/3_yD_cEKoCk?fs=1&hl=en_US',
|
||||
'//www.youtube.com/v/3_yD_cEKoCk?fs=1&hl=en_US',
|
||||
'www.youtube.com/v/3_yD_cEKoCk?fs=1&hl=en_US',
|
||||
'youtube.com/v/3_yD_cEKoCk?fs=1&hl=en_US',
|
||||
'https://www.youtube.com/embed/3_yD_cEKoCk?autoplay=1',
|
||||
'https://www.youtube.com/embed/3_yD_cEKoCk',
|
||||
'http://www.youtube.com/embed/3_yD_cEKoCk',
|
||||
'//www.youtube.com/embed/3_yD_cEKoCk',
|
||||
'www.youtube.com/embed/3_yD_cEKoCk',
|
||||
'https://youtube.com/embed/3_yD_cEKoCk',
|
||||
'http://youtube.com/embed/3_yD_cEKoCk',
|
||||
'//youtube.com/embed/3_yD_cEKoCk',
|
||||
'youtube.com/embed/3_yD_cEKoCk',
|
||||
'https://youtu.be/3_yD_cEKoCk?t=120',
|
||||
'https://youtu.be/3_yD_cEKoCk',
|
||||
'http://youtu.be/3_yD_cEKoCk',
|
||||
'//youtu.be/3_yD_cEKoCk',
|
||||
'youtu.be/3_yD_cEKoCk',
|
||||
];
|
||||
testURLs.forEach((url) => {
|
||||
expect(api.parseYoutubeId(url)).toEqual(id);
|
||||
});
|
||||
});
|
||||
it('returns null if the url is not a youtube url', () => {
|
||||
const badURL = 'https://someothersite.com/3_yD_cEKoCk';
|
||||
expect(api.parseYoutubeId(badURL)).toEqual(null);
|
||||
});
|
||||
});
|
||||
// TODO FOR LICENSE
|
||||
describe('processLicense', () => {});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user