Feat video source integration (#125)

* feat: video source integration

Co-authored-by: KristinAoki <kaoki@2u.com>
This commit is contained in:
Raymond Zhou
2022-10-12 09:38:44 -07:00
committed by GitHub
parent a7abab1236
commit d2f07045f0
24 changed files with 282 additions and 257 deletions

View File

@@ -60,8 +60,6 @@ export const TitleHeader = ({
TitleHeader.defaultProps = {};
TitleHeader.propTypes = {
isInitialized: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
setTitle: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
};

View File

@@ -1,17 +1,19 @@
// 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]}
<Component
value="hooks.errorsHook.error"
>
<div
className="video-editor"
<EditorContainer
getContent={[Function]}
onClose={[MockFunction props.onClose]}
validateEntry={[MockFunction validateEntry]}
>
<VideoEditorModal
error="errORsHooKErroR"
/>
</div>
</EditorContainer>
<div
className="video-editor"
>
<VideoEditorModal />
</div>
</EditorContainer>
</Component>
`;

View File

@@ -17,30 +17,20 @@ export const hooks = {
const VideoEditorModal = ({
close,
error,
isOpen,
}) => {
const dispatch = useDispatch();
module.hooks.initialize(dispatch);
return (
<VideoSettingsModal {...{ close, error, isOpen }} />
<VideoSettingsModal {...{ close, isOpen }} />
);
// TODO: add logic to show SelectVideoModal if no selection
};
VideoEditorModal.defaultProps = {
error: {
duration: {},
handout: {},
license: {},
thumbnail: {},
transcripts: {},
videoSource: {},
},
};
VideoEditorModal.propTypes = {
close: PropTypes.func.isRequired,
error: PropTypes.node,
isOpen: PropTypes.bool.isRequired,
};
export default VideoEditorModal;

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import messages from './components/messages';
import { ErrorContext } from '../../hooks';
import * as module from './ErrorSummary';
export const hasNoError = (error) => Object.keys(error[0]).length === 0;
export const showAlert = (errors) => !Object.values(errors).every(module.hasNoError);
export const ErrorSummary = () => {
const errors = React.useContext(ErrorContext);
return (
<Alert
icon={Info}
show={module.showAlert(errors)}
variant="danger"
>
<Alert.Heading>
<FormattedMessage {...messages.validateErrorTitle} />
</Alert.Heading>
<p>
<FormattedMessage {...messages.validateErrorBody} />
</p>
</Alert>
);
};
export default ErrorSummary;

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { shallow } from 'enzyme';
import * as module from './ErrorSummary';
describe('ErrorSummary', () => {
const errors = {
widgetWithError: [{ err1: 'mSg', err2: 'msG2' }, jest.fn()],
widgetWithNoError: [{}, jest.fn()],
};
afterEach(() => {
jest.restoreAllMocks();
});
describe('render', () => {
beforeEach(() => {
jest.spyOn(React, 'useContext').mockReturnValueOnce({});
});
test('snapshots: renders as expected when there are no errors', () => {
jest.spyOn(module, 'showAlert').mockReturnValue(false);
expect(shallow(<module.ErrorSummary />)).toMatchSnapshot();
});
test('snapshots: renders as expected when there are errors', () => {
jest.spyOn(module, 'showAlert').mockReturnValue(true);
expect(shallow(<module.ErrorSummary />)).toMatchSnapshot();
});
});
describe('hasNoError', () => {
it('returns true', () => {
expect(module.hasNoError(errors.widgetWithError)).toEqual(false);
});
it('returns false', () => {
expect(module.hasNoError(errors.widgetWithNoError)).toEqual(true);
});
});
describe('showAlert', () => {
it('returns true', () => {
jest.spyOn(module, 'hasNoError').mockReturnValue(false);
expect(module.showAlert(errors)).toEqual(true);
});
it('returns false', () => {
jest.spyOn(module, 'hasNoError').mockReturnValue(true);
expect(module.showAlert(errors)).toEqual(false);
});
});
});

View File

@@ -0,0 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ErrorSummary render snapshots: renders as expected when there are errors 1`] = `
<Alert
show={true}
variant="danger"
>
<Alert.Heading>
<FormattedMessage
defaultMessage="We couldn't add your video."
description="Title of validation error."
id="authoring.videoeditor.validate.error.title"
/>
</Alert.Heading>
<p>
<FormattedMessage
defaultMessage="Please check your entries and try again."
description="Body of validation error."
id="authoring.videoeditor.validate.error.body"
/>
</p>
</Alert>
`;
exports[`ErrorSummary render snapshots: renders as expected when there are no errors 1`] = `
<Alert
show={false}
variant="danger"
>
<Alert.Heading>
<FormattedMessage
defaultMessage="We couldn't add your video."
description="Title of validation error."
id="authoring.videoeditor.validate.error.title"
/>
</Alert.Heading>
<p>
<FormattedMessage
defaultMessage="Please check your entries and try again."
description="Body of validation error."
id="authoring.videoeditor.validate.error.body"
/>
</p>
</Alert>
`;

View File

@@ -1,40 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import messages from './messages';
export const ErrorSummary = ({
error,
}) => (
<Alert
icon={Info}
show={!Object.values(error).every(val => Object.keys(val).length === 0)}
variant="danger"
>
<Alert.Heading>
<FormattedMessage {...messages.validateErrorTitle} />
</Alert.Heading>
<p>
<FormattedMessage {...messages.validateErrorBody} />
</p>
</Alert>
);
ErrorSummary.defaultProps = {
error: {
duration: {},
handout: {},
license: {},
thumbnail: {},
transcripts: {},
videoSource: {},
},
};
ErrorSummary.propTypes = {
error: PropTypes.node,
};
export default ErrorSummary;

View File

@@ -1,17 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { ErrorSummary } from './ErrorSummary';
describe('ErrorSummary', () => {
const props = {
error: 'eRrOr',
};
describe('render', () => {
test('snapshots: renders as expected', () => {
expect(
shallow(<ErrorSummary {...props} />),
).toMatchSnapshot();
});
});
});

View File

@@ -2,7 +2,7 @@
exports[`TranscriptWidget snapshots snapshot: renders ErrorAlert with delete error message 1`] = `
<injectIntl(ShimmedIntlComponent)
isError={false}
isError={true}
subtitle="English"
title="Transcript"
>
@@ -115,7 +115,7 @@ exports[`TranscriptWidget snapshots snapshot: renders ErrorAlert with delete err
exports[`TranscriptWidget snapshots snapshot: renders ErrorAlert with upload error message 1`] = `
<injectIntl(ShimmedIntlComponent)
isError={false}
isError={true}
subtitle="English"
title="Transcript"
>
@@ -228,7 +228,7 @@ exports[`TranscriptWidget snapshots snapshot: renders ErrorAlert with upload err
exports[`TranscriptWidget snapshots snapshots: renders as expected with allowTranscriptDownloads true 1`] = `
<injectIntl(ShimmedIntlComponent)
isError={false}
isError={true}
subtitle="English"
title="Transcript"
>
@@ -341,7 +341,7 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with allowTra
exports[`TranscriptWidget snapshots snapshots: renders as expected with default props 1`] = `
<injectIntl(ShimmedIntlComponent)
isError={false}
isError={true}
subtitle="None"
title="Transcript"
>
@@ -412,7 +412,7 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with default
exports[`TranscriptWidget snapshots snapshots: renders as expected with showTranscriptByDefault true 1`] = `
<injectIntl(ShimmedIntlComponent)
isError={false}
isError={true}
subtitle="English"
title="Transcript"
>
@@ -525,7 +525,7 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with showTran
exports[`TranscriptWidget snapshots snapshots: renders as expected with transcripts 1`] = `
<injectIntl(ShimmedIntlComponent)
isError={false}
isError={true}
subtitle="English"
title="Transcript"
>

View File

@@ -2,14 +2,26 @@ import React from 'react';
import { thunkActions, actions } from '../../../../../../data/redux';
import * as module from './hooks';
import { videoTranscriptLanguages } from '../../../../../../data/constants/video';
import { ErrorContext } from '../../../../hooks';
import messages from './messages';
export const state = {
inDeleteConfirmation: (args) => React.useState(args),
};
export const updateErrors = ({ isUploadError, isDeleteError }) => {
const [error, setError] = React.useContext(ErrorContext).transcripts;
if (isUploadError) {
setError({ ...error, uploadError: messages.uploadTranscriptError.defaultMessage });
}
if (isDeleteError) {
setError({ ...error, deleteError: messages.deleteTranscriptError.defaultMessage });
}
};
export const transcriptLanguages = (transcripts) => {
const languages = [];
if (Object.keys(transcripts).length > 0) {
if (transcripts && Object.keys(transcripts).length > 0) {
Object.keys(transcripts).forEach(transcript => {
languages.push(videoTranscriptLanguages[transcript]);
});

View File

@@ -28,12 +28,12 @@ import ErrorAlert from '../../../../../../sharedComponents/ErrorAlerts/ErrorAler
import CollapsibleFormWidget from '../CollapsibleFormWidget';
import TranscriptListItem from './TranscriptListItem';
import { ErrorContext } from '../../../../hooks';
/**
* Collapsible Form widget controlling video transcripts
*/
export const TranscriptWidget = ({
error,
// redux
transcripts,
allowTranscriptDownloads,
@@ -42,6 +42,7 @@ export const TranscriptWidget = ({
isUploadError,
isDeleteError,
}) => {
const [error] = React.useContext(ErrorContext).transcripts;
const languagesArr = hooks.transcriptLanguages(transcripts);
const fileInput = hooks.fileInput({ onAddFile: hooks.addFileCallback({ dispatch: useDispatch() }) });
const hasTranscripts = hooks.hasTranscripts(transcripts);
@@ -124,10 +125,8 @@ export const TranscriptWidget = ({
};
TranscriptWidget.defaultProps = {
error: {},
};
TranscriptWidget.propTypes = {
error: PropTypes.node,
// redux
transcripts: PropTypes.shape({}).isRequired,
allowTranscriptDownloads: PropTypes.bool.isRequired,

View File

@@ -7,6 +7,11 @@ import { formatMessage } from '../../../../../../../testUtils';
import { actions, selectors } from '../../../../../../data/redux';
import { TranscriptWidget, mapStateToProps, mapDispatchToProps } from '.';
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(() => ({ transcripts: ['error.transcripts', jest.fn().mockName('error.setTranscripts')] })),
}));
jest.mock('../../../../../../data/redux', () => ({
actions: {
video: {

View File

@@ -28,7 +28,6 @@ import CollapsibleFormWidget from '../CollapsibleFormWidget';
* Collapsible Form widget controlling video source as well as fallback sources
*/
export const VideoSourceWidget = ({
// error,
// injected
intl,
// redux
@@ -128,10 +127,8 @@ export const VideoSourceWidget = ({
);
};
VideoSourceWidget.defaultProps = {
// error: {},
};
VideoSourceWidget.propTypes = {
// error: PropTypes.node,
// injected
intl: intlShape.isRequired,
// redux

View File

@@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ErrorSummary render snapshots: renders as expected 1`] = `
<Alert
show={true}
variant="danger"
>
<Alert.Heading>
<FormattedMessage
defaultMessage="We couldn't add your video."
description="Title of validation error."
id="authoring.videoeditor.validate.error.title"
/>
</Alert.Heading>
<p>
<FormattedMessage
defaultMessage="Please check your entries and try again."
description="Body of validation error."
id="authoring.videoeditor.validate.error.body"
/>
</p>
</Alert>
`;

View File

@@ -1,9 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { thunkActions } from '../../../../data/redux';
// import VideoPreview from './components/VideoPreview';
import ErrorSummary from './components/ErrorSummary';
import ErrorSummary from './ErrorSummary';
import DurationWidget from './components/DurationWidget';
import HandoutWidget from './components/HandoutWidget';
import LicenseWidget from './components/LicenseWidget';
@@ -20,39 +19,23 @@ export const hooks = {
},
};
export const VideoSettingsModal = ({
error,
}) => (
export const VideoSettingsModal = () => (
<div className="video-settings-modal row">
<div className="video-preview col col-4">
Video Preview goes here
{/* <VideoPreview /> */}
</div>
<div className="video-controls col col-8">
<ErrorSummary {...{ error }} />
<ErrorSummary />
<h3>Settings</h3>
<VideoSourceWidget error={error.videoSource} />
<ThumbnailWidget error={error.thumbnail} />
<TranscriptWidget error={error.transcripts} />
<DurationWidget error={error.duration} />
<HandoutWidget error={error.handout} />
<LicenseWidget error={error.license} />
<VideoSourceWidget />
<ThumbnailWidget />
<TranscriptWidget />
<DurationWidget />
<HandoutWidget />
<LicenseWidget />
</div>
</div>
);
VideoSettingsModal.defaultProps = {
error: {
duration: {},
handout: {},
license: {},
thumbnail: {},
transcripts: {},
videoSource: {},
},
};
VideoSettingsModal.propTypes = {
error: PropTypes.node,
};
export default VideoSettingsModal;

View File

@@ -1,8 +1,10 @@
import { useState } from 'react';
import { useState, createContext } from 'react';
import { StrictDict } from '../../utils';
import * as module from './hooks';
export const ErrorContext = createContext();
export const state = StrictDict({
durationErrors: (val) => useState(val),
handoutErrors: (val) => useState(val),
@@ -22,59 +24,21 @@ export const errorsHook = () => {
return {
error: {
duration: durationErrors,
handout: handoutErrors,
license: licenseErrors,
thumbnail: thumbnailErrors,
transcripts: transcriptsErrors,
videoSource: videoSourceErrors,
duration: [durationErrors, setDurationErrors],
handout: [handoutErrors, setHandoutErrors],
license: [licenseErrors, setLicenseErrors],
thumbnail: [thumbnailErrors, setThumbnailErrors],
transcripts: [transcriptsErrors, setTranscriptsErrors],
videoSource: [videoSourceErrors, setVideoSourceErrors],
},
validateEntry: () => {
let validated = true;
if (!module.validateDuration({ setDurationErrors })) { validated = false; }
if (!module.validateHandout({ setHandoutErrors })) { validated = false; }
if (!module.validateLicense({ setLicenseErrors })) { validated = false; }
if (!module.validateThumbnail({ setThumbnailErrors })) { validated = false; }
if (!module.validateTranscripts({ setTranscriptsErrors })) { validated = false; }
if (!module.validateVideoSource({ setVideoSourceErrors })) { validated = false; }
return validated;
if (Object.keys(durationErrors).length > 0) { return false; }
if (Object.keys(handoutErrors).length > 0) { return false; }
if (Object.keys(licenseErrors).length > 0) { return false; }
if (Object.keys(thumbnailErrors).length > 0) { return false; }
if (Object.keys(transcriptsErrors).length > 0) { return false; }
if (Object.keys(videoSourceErrors).length > 0) { return false; }
return true;
},
};
};
export const validateDuration = ({ setDurationErrors }) => {
setDurationErrors({
fieldName: 'sample error message',
});
return false;
};
export const validateHandout = ({ setHandoutErrors }) => {
setHandoutErrors({
fieldName: 'sample error message',
});
return false;
};
export const validateLicense = ({ setLicenseErrors }) => {
setLicenseErrors({
fieldName: 'sample error message',
});
return false;
};
export const validateThumbnail = ({ setThumbnailErrors }) => {
setThumbnailErrors({
fieldName: 'sample error message',
});
return false;
};
export const validateTranscripts = ({ setTranscriptsErrors }) => {
setTranscriptsErrors({
fieldName: 'sample error message',
});
return false;
};
export const validateVideoSource = ({ setVideoSourceErrors }) => {
setVideoSourceErrors({
fieldName: 'sample error message',
});
return false;
};

View File

@@ -1,6 +1,5 @@
import { MockUseState } from '../../../testUtils';
import { keyStore } from '../../utils';
import * as module from './hooks';
jest.mock('react', () => ({
@@ -8,7 +7,6 @@ jest.mock('react', () => ({
}));
const state = new MockUseState(module);
const moduleKeys = keyStore(module);
let hook;
@@ -33,38 +31,44 @@ describe('VideoEditorHooks', () => {
state.restore();
});
const mockTrue = () => true;
const mockFalse = () => false;
const fakeDurationError = {
field1: 'field1msg',
field2: 'field2msg',
};
test('error: state values', () => {
expect(module.errorsHook().error).toEqual({
duration: state.stateVals[state.keys.durationErrors],
handout: state.stateVals[state.keys.handoutErrors],
license: state.stateVals[state.keys.licenseErrors],
thumbnail: state.stateVals[state.keys.thumbnailErrors],
transcripts: state.stateVals[state.keys.transcriptsErrors],
videoSource: state.stateVals[state.keys.videoSourceErrors],
});
expect(module.errorsHook().error.duration).toEqual([
state.stateVals[state.keys.durationErrors],
state.setState[state.keys.durationErrors],
]);
expect(module.errorsHook().error.handout).toEqual([
state.stateVals[state.keys.handoutErrors],
state.setState[state.keys.handoutErrors],
]);
expect(module.errorsHook().error.license).toEqual([
state.stateVals[state.keys.licenseErrors],
state.setState[state.keys.licenseErrors],
]);
expect(module.errorsHook().error.thumbnail).toEqual([
state.stateVals[state.keys.thumbnailErrors],
state.setState[state.keys.thumbnailErrors],
]);
expect(module.errorsHook().error.transcripts).toEqual([
state.stateVals[state.keys.transcriptsErrors],
state.setState[state.keys.transcriptsErrors],
]);
expect(module.errorsHook().error.videoSource).toEqual([
state.stateVals[state.keys.videoSourceErrors],
state.setState[state.keys.videoSourceErrors],
]);
});
describe('validateEntry', () => {
beforeEach(() => {
hook = module.errorsHook();
});
test('validateEntry: returns true if all validation calls are true', () => {
jest.spyOn(module, moduleKeys.validateDuration).mockImplementationOnce(mockTrue);
jest.spyOn(module, moduleKeys.validateHandout).mockImplementationOnce(mockTrue);
jest.spyOn(module, moduleKeys.validateLicense).mockImplementationOnce(mockTrue);
jest.spyOn(module, moduleKeys.validateThumbnail).mockImplementationOnce(mockTrue);
jest.spyOn(module, moduleKeys.validateTranscripts).mockImplementationOnce(mockTrue);
jest.spyOn(module, moduleKeys.validateVideoSource).mockImplementationOnce(mockTrue);
hook = module.errorsHook();
expect(hook.validateEntry()).toEqual(true);
});
test('validateEntry: returns false if any validation calls are false', () => {
jest.spyOn(module, moduleKeys.validateDuration).mockImplementationOnce(mockFalse);
jest.spyOn(module, moduleKeys.validateHandout).mockImplementationOnce(mockTrue);
jest.spyOn(module, moduleKeys.validateLicense).mockImplementationOnce(mockTrue);
jest.spyOn(module, moduleKeys.validateThumbnail).mockImplementationOnce(mockTrue);
jest.spyOn(module, moduleKeys.validateTranscripts).mockImplementationOnce(mockTrue);
jest.spyOn(module, moduleKeys.validateVideoSource).mockImplementationOnce(mockTrue);
state.mockVal(state.keys.durationErrors, fakeDurationError);
hook = module.errorsHook();
expect(hook.validateEntry()).toEqual(false);
});
});

View File

@@ -6,7 +6,7 @@ import { selectors } from '../../data/redux';
import EditorContainer from '../EditorContainer';
import VideoEditorModal from './components/VideoEditorModal';
import { errorsHook } from './hooks';
import { ErrorContext, errorsHook } from './hooks';
export const VideoEditor = ({
onClose,
@@ -19,15 +19,17 @@ export const VideoEditor = ({
} = errorsHook();
return (
<EditorContainer
getContent={() => videoSettings}
onClose={onClose}
validateEntry={validateEntry}
>
<div className="video-editor">
<VideoEditorModal {...{ error }} />
</div>
</EditorContainer>
<ErrorContext.Provider value={error}>
<EditorContainer
getContent={() => videoSettings}
onClose={onClose}
validateEntry={validateEntry}
>
<div className="video-editor">
<VideoEditorModal />
</div>
</EditorContainer>
</ErrorContext.Provider>
);
};

View File

@@ -2,13 +2,13 @@ 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', () => ({
ErrorContext: jest.fn(),
errorsHook: jest.fn(() => ({
error: 'hooks.errorsHook.error',
validateEntry: jest.fn().mockName('validateEntry'),
@@ -29,10 +29,6 @@ describe('VideoEditor', () => {
// 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();

View File

@@ -23,7 +23,7 @@ export const loadVideoData = () => (dispatch, getState) => {
videoId,
fallbackVideos,
allowVideoDownloads: rawVideoData.download_video,
transcripts: rawVideoData.transcripts,
transcripts: rawVideoData.transcripts || {},
allowTranscriptDownloads: rawVideoData.download_track,
showTranscriptByDefault: rawVideoData.show_captions,
duration: { // TODO duration is not always sent so they should be calculated.
@@ -47,14 +47,28 @@ export const determineVideoSource = ({
youtubeId,
html5Sources,
}) => {
// videoSource should be the edx_video_id (if present), or the youtube url (if present), or the first fallback url.
// in that order.
// if we are falling back to the first fallback url, remove it from the list of fallback urls for display
const videoSource = edxVideoId || youtubeId || html5Sources[0] || '';
// videoSource should be the edx_video_id, the youtube url or the first fallback url in that order.
// If we are falling back to the first fallback url, remove it from the list of fallback urls for display.
const youtubeUrl = `https://youtu.be/${youtubeId}`;
const videoId = edxVideoId || '';
const fallbackVideos = (!edxVideoId && !youtubeId)
? html5Sources.slice(1)
: html5Sources;
let videoSource = '';
let fallbackVideos = [];
if (edxVideoId) {
[videoSource, fallbackVideos] = [edxVideoId, html5Sources];
// videoSource = edxVideoId;
// fallbackVideos = html5Sources;
} else if (youtubeId) {
[videoSource, fallbackVideos] = [youtubeUrl, html5Sources];
// videoSource = youtubeUrl;
// fallbackVideos = html5Sources;
} else if (Array.isArray(html5Sources) && html5Sources[0]) {
[videoSource, fallbackVideos] = [html5Sources[0], html5Sources.slice(1)];
// videoSource = html5Sources[0];
// fallbackVideos = html5Sources.slice(1);
}
if (fallbackVideos.length === 0) {
fallbackVideos = ['', ''];
}
return {
videoSource,
videoId,

View File

@@ -107,6 +107,7 @@ describe('video thunkActions', () => {
describe('determineVideoSource', () => {
const edxVideoId = 'EDxviDEoiD';
const youtubeId = 'yOuTuBEiD';
const youtubeUrl = `https://youtu.be/${youtubeId}`;
const html5Sources = ['htmLOne', 'hTMlTwo', 'htMLthrEE'];
describe('when there is an edx video id, youtube id and html5 sources', () => {
it('returns the edx video id for video source and html5 sources for fallback videos', () => {
@@ -122,13 +123,13 @@ describe('video thunkActions', () => {
});
});
describe('when there is no edx video id', () => {
it('returns the youtube id for video source and html5 sources for fallback videos', () => {
it('returns the youtube url for video source and html5 sources for fallback videos', () => {
expect(thunkActions.determineVideoSource({
edxVideoId: '',
youtubeId,
html5Sources,
})).toEqual({
videoSource: youtubeId,
videoSource: youtubeUrl,
videoId: '',
fallbackVideos: html5Sources,
});
@@ -146,7 +147,7 @@ describe('video thunkActions', () => {
fallbackVideos: ['hTMlTwo', 'htMLthrEE'],
});
});
it('returns the html5 source for video source and an empty array for fallback videos', () => {
it('returns the html5 source for video source and an array with 2 empty values for fallback videos', () => {
expect(thunkActions.determineVideoSource({
edxVideoId: '',
youtubeId: '',
@@ -154,12 +155,12 @@ describe('video thunkActions', () => {
})).toEqual({
videoSource: 'htmlOne',
videoId: '',
fallbackVideos: [],
fallbackVideos: ['', ''],
});
});
});
describe('when there is no edx video id, no youtube id and no html5 sources', () => {
it('returns an empty string for video source and an empty array for fallback videos', () => {
it('returns an empty string for video source and an array with 2 empty values for fallback videos', () => {
expect(thunkActions.determineVideoSource({
edxVideoId: '',
youtubeId: '',
@@ -167,7 +168,7 @@ describe('video thunkActions', () => {
})).toEqual({
videoSource: '',
videoId: '',
fallbackVideos: [],
fallbackVideos: ['', ''],
});
});
});

View File

@@ -30,6 +30,9 @@ export const simpleSelectors = [
export const openLanguages = createSelector(
[module.simpleSelectors.transcripts],
(transcripts) => {
if (!transcripts) {
return videoTranscriptLanguages;
}
const open = Object.entries(videoTranscriptLanguages).filter(
([lang]) => !Object.keys(transcripts).includes(lang),
);
@@ -47,7 +50,19 @@ export const getTranscriptDownloadUrl = createSelector(
);
export const videoSettings = createSelector(
Object.values(module.simpleSelectors),
[
module.simpleSelectors.videoSource,
module.simpleSelectors.fallbackVideos,
module.simpleSelectors.allowVideoDownloads,
module.simpleSelectors.thumbnail,
module.simpleSelectors.transcripts,
module.simpleSelectors.allowTranscriptDownloads,
module.simpleSelectors.duration,
module.simpleSelectors.showTranscriptByDefault,
module.simpleSelectors.handout,
module.simpleSelectors.licenseType,
module.simpleSelectors.licenseDetails,
],
(
videoSource,
fallbackVideos,
@@ -78,8 +93,8 @@ export const videoSettings = createSelector(
);
export default {
...simpleSelectors,
openLanguages,
getTranscriptDownloadUrl,
...simpleSelectors,
videoSettings,
};

View File

@@ -145,11 +145,11 @@ export const processVideoIds = ({ videoSource, fallbackVideos }) => {
edxVideoId = videoSource;
} else if (module.parseYoutubeId(videoSource)) {
youtubeId = module.parseYoutubeId(videoSource);
} else {
} else if (videoSource) {
html5Sources.push(videoSource);
}
fallbackVideos.forEach((src) => html5Sources.push(src));
fallbackVideos.forEach((src) => (src ? html5Sources.push(src) : null));
return {
edxVideoId,

View File

@@ -23,7 +23,7 @@ export const fetchBlockById = ({ blockId, studioEndpointUrl }) => mockPromise({
sub: '',
track: '',
transcripts: {
en: 'my-transcript-url',
en: { filename: 'my-transcript-url' },
},
xml_attributes: {
source: '',
@@ -54,7 +54,7 @@ export const fetchStudioView = ({ blockId, studioEndpointUrl }) => mockPromise({
sub: '',
track: '',
transcripts: {
en: 'my-transcript-url',
en: { filename: 'my-transcript-url' },
},
xml_attributes: {
source: '',