diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/__snapshots__/index.test.jsx.snap b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/__snapshots__/index.test.jsx.snap index 53fdb4043..a4c282e9a 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/__snapshots__/index.test.jsx.snap +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/__snapshots__/index.test.jsx.snap @@ -3,7 +3,7 @@ exports[`DurationWidget render snapshots: renders as expected with default props 1`] = ` - Full video length + Total: 00:00:00 `; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.js index 02707b0b6..679364f9f 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.js +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.js @@ -1,41 +1,37 @@ import { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { actions, selectors } from '../../../../../../data/redux'; import messages from '../messages'; import * as module from './hooks'; const durationMatcher = /^(\d{0,2}):?(\d{0,2})?:?(\d{0,2})?$/i; -export const durationWidget = ({ dispatch }) => { - const reduxStartStopTimes = useSelector(selectors.video.duration); - const setReduxStartStopTimes = (val) => dispatch(actions.video.updateField({ duration: val })); - const initialState = module.durationString(reduxStartStopTimes); - const [unsavedStartStopTimes, setUnsavedStartStopTimes] = useState(initialState); +export const durationWidget = ({ duration, updateField }) => { + const setDuration = (val) => updateField({ duration: val }); + const initialState = module.durationString(duration); + const [unsavedDuration, setUnsavedDuration] = useState(initialState); useEffect(() => { - setUnsavedStartStopTimes(module.durationString(reduxStartStopTimes)); - }, [reduxStartStopTimes]); + setUnsavedDuration(module.durationString(duration)); + }, [duration]); return { - reduxStartStopTimes, - unsavedStartStopTimes, + unsavedDuration, onBlur: (index) => ( (e) => module.updateDuration({ - reduxStartStopTimes, - setReduxStartStopTimes, - unsavedStartStopTimes, - setUnsavedStartStopTimes, + duration, + setDuration, + unsavedDuration, + setUnsavedDuration, index, durationString: e.target.value, }) ), onChange: (index) => ( - (e) => setUnsavedStartStopTimes(module.onDurationChange(unsavedStartStopTimes, index, e.target.value)) + (e) => setUnsavedDuration(module.onDurationChange(unsavedDuration, index, e.target.value)) ), onKeyDown: (index) => ( - (e) => setUnsavedStartStopTimes(module.onDurationKeyDown(unsavedStartStopTimes, index, e)) + (e) => setUnsavedDuration(module.onDurationKeyDown(unsavedDuration, index, e)) ), getTotalLabel: ({ duration, subtitle, intl }) => { if (!duration.stopTime) { @@ -86,23 +82,23 @@ export const durationStringFromValue = (value) => { }; /** - * updateDuration({ reduxStartStopTimes, unsavedStartStopTimes, setUnsavedStartStopTimes, setReduxStartStopTimes }) - * Returns a memoized callback based on inputs that updates unsavedStartStopTimes value and form value - * if the new string is valid (reduxStartStopTimes stores a number, unsavedStartStopTimes stores a string). - * If the duration string is invalid, resets the unsavedStartStopTimes value to the latest good value. - * @param {object} reduxStartStopTimes - redux-stored durations in milliseconds - * @param {object} unsavedStartStopTimes - hook-stored duration in 'hh:mm:ss' format - * @param {func} setReduxStartStopTimes - set form value - * @param {func} setUnsavedStartStopTimes - set unsavedStartStopTimes object + * updateDuration({ duration, unsavedDuration, setUnsavedDuration, setDuration }) + * Returns a memoized callback based on inputs that updates unsavedDuration value and form value + * if the new string is valid (duration stores a number, unsavedDuration stores a string). + * If the duration string is invalid, resets the unsavedDuration value to the latest good value. + * @param {object} duration - redux-stored durations in milliseconds + * @param {object} unsavedDuration - hook-stored duration in 'hh:mm:ss' format + * @param {func} setDuration - set form value + * @param {func} setUnsavedDuration - set unsavedDuration object * @param {string} index - startTime or stopTime - * @return {func} - callback to update duration unsavedStartStopTimesly and in redux + * @return {func} - callback to update duration unsavedDurationly and in redux * updateDuration(args)(index, durationString) */ export const updateDuration = ({ - reduxStartStopTimes, - unsavedStartStopTimes, - setReduxStartStopTimes, - setUnsavedStartStopTimes, + duration, + unsavedDuration, + setDuration, + setUnsavedDuration, index, inputString, }) => { @@ -117,17 +113,17 @@ export const updateDuration = ({ newValue = 1000; } // stopTime must be at least 1 second after startTime, except 0 means no custom stopTime - if (index === 'stopTime' && newValue > 0 && newValue < (reduxStartStopTimes.startTime + 1000)) { - newValue = reduxStartStopTimes.startTime + 1000; + if (index === 'stopTime' && newValue > 0 && newValue < (duration.startTime + 1000)) { + newValue = duration.startTime + 1000; } // startTime must be at least 1 second before stopTime, except when stopTime is less than a second // (stopTime should only be less than a second if it's zero, but we're being paranoid) - if (index === 'startTime' && reduxStartStopTimes.stopTime >= 1000 && newValue > (reduxStartStopTimes.stopTime - 1000)) { - newValue = reduxStartStopTimes.stopTime - 1000; + if (index === 'startTime' && duration.stopTime >= 1000 && newValue > (duration.stopTime - 1000)) { + newValue = duration.stopTime - 1000; } newDurationString = module.durationStringFromValue(newValue); - setUnsavedStartStopTimes({ ...unsavedStartStopTimes, [index]: newDurationString }); - setReduxStartStopTimes({ ...reduxStartStopTimes, [index]: newValue }); + setUnsavedDuration({ ...unsavedDuration, [index]: newDurationString }); + setDuration({ ...duration, [index]: newValue }); }; /** diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.test.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.test.js index 2707497b8..976ff3a80 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.test.js +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.test.js @@ -1,40 +1,20 @@ import React from 'react'; -import { useSelector } from 'react-redux'; -import { selectors } from '../../../../../../data/redux'; import * as hooks from './hooks'; import messages from '../messages'; jest.mock('react', () => { const updateState = jest.fn(); - const dispatchFn = jest.fn(); return { ...jest.requireActual('react'), updateState, useState: jest.fn(val => ([{ state: val }, (newVal) => updateState({ val, newVal })])), useCallback: (cb, prereqs) => ({ useCallback: { cb, prereqs } }), useEffect: jest.fn(), - useSelector: jest.fn(), - dispatch: dispatchFn, - useDispatch: jest.fn(() => dispatchFn), }; }); -jest.mock('../../../../../../data/redux', () => ({ - actions: { - video: { - updateField: (val) => ({ updateField: val }), - }, - }, - selectors: { - video: { - duration: (state) => ({ duration: state }), - }, - }, -})); - let hook; -const dispatch = jest.fn(val => ({ dispatch: val })); const intl = { formatMessage: jest.fn(val => val), }; @@ -65,35 +45,35 @@ describe('Video Settings DurationWidget hooks', () => { jest.restoreAllMocks(); }); describe('durationWidget', () => { - let reduxStartStopTimes; + const duration = { + startTime: '00:00:00', + stopTime: '00:00:10', + }; + const updateField = jest.fn(); beforeEach(() => { - hook = hooks.durationWidget({ dispatch }); - reduxStartStopTimes = useSelector(selectors.video.duration); + hook = hooks.durationWidget({ duration, updateField }); }); describe('behavior', () => { describe('initialization', () => { - test('useEffect memoized on reduxStartStopTimes', () => { - hooks.durationWidget({ dispatch }); + test('useEffect memoized on duration', () => { + hooks.durationWidget({ duration, updateField }); expect(React.useEffect).toHaveBeenCalled(); - expect(React.useEffect.mock.calls[0][1]).toEqual([reduxStartStopTimes]); + expect(React.useEffect.mock.calls[0][1]).toEqual([duration]); }); - test('calls setUnsavedStartStopTimes with durationString(reduxStartStopTimes)', () => { - hooks.durationWidget({ dispatch }); + test('calls setUnsavedDuration with durationString(duration)', () => { + hooks.durationWidget({ duration, updateField }); React.useEffect.mock.calls[0][0](); expect(React.updateState).toHaveBeenCalled(); }); }); }); describe('returns', () => { - hook = hooks.durationWidget({ dispatch }); + hook = hooks.durationWidget({ duration, updateField }); afterEach(() => { jest.restoreAllMocks(); }); - describe('reduxStartStopTimes, with redux duration value', () => { - expect(hook.reduxStartStopTimes).toEqual(useSelector(selectors.video.duration)); - }); - describe('unsavedStartStopTimes, defaulted to reduxStartStopTimes', () => { - expect(hook.unsavedStartStopTimes).toEqual({ state: hooks.durationString(hook.reduxStartStopTimes) }); + describe('unsavedDuration, defaulted to duration', () => { + expect(hook.unsavedDuration).toEqual({ state: hooks.durationString(duration) }); }); describe('onBlur, calls updateDuration', () => { jest.spyOn(hooks, 'updateDuration').mockImplementation(jest.fn()); @@ -182,78 +162,78 @@ describe('Video Settings DurationWidget hooks', () => { beforeEach(() => { hook = hooks.updateDuration; props = { - reduxStartStopTimes: { startTime: 23000, stopTime: 600000 }, - unsavedStartStopTimes: { startTime: '00:00:23', stopTime: '00:10:00' }, - setReduxStartStopTimes: jest.fn(), - setUnsavedStartStopTimes: jest.fn(), + duration: { startTime: 23000, stopTime: 600000 }, + unsavedDuration: { startTime: '00:00:23', stopTime: '00:10:00' }, + setDuration: jest.fn(), + setUnsavedDuration: jest.fn(), index: 'startTime', inputString: '01:23:45', }; }); describe('if the passed durationString is valid', () => { - it('sets the unsavedStartStopTimes to updated strings and reduxStartStopTimes to new timestamp value', () => { + it('sets the unsavedDuration to updated strings and duration to new timestamp value', () => { hook({ ...props, index: testValidIndex, inputString: testValidDuration, }); - expect(props.setUnsavedStartStopTimes).toHaveBeenCalledWith({ - ...props.unsavedStartStopTimes, + expect(props.setUnsavedDuration).toHaveBeenCalledWith({ + ...props.unsavedDuration, [testValidIndex]: testValidDuration, }); - expect(props.setReduxStartStopTimes).toHaveBeenCalledWith({ - ...props.reduxStartStopTimes, + expect(props.setDuration).toHaveBeenCalledWith({ + ...props.duration, [testValidIndex]: testValidValue, }); }); }); describe('if the passed durationString is not valid', () => { - it('updates unsavedStartStopTimes values to 0 (the default)', () => { + it('updates unsavedDuration values to 0 (the default)', () => { hook({ ...props, index: testValidIndex, inputString: testInvalidDuration, }); - expect(props.setUnsavedStartStopTimes).toHaveBeenCalledWith({ - ...props.unsavedStartStopTimes, + expect(props.setUnsavedDuration).toHaveBeenCalledWith({ + ...props.unsavedDuration, [testValidIndex]: testValidDuration, }); - expect(props.setReduxStartStopTimes).toHaveBeenCalledWith({ - ...props.reduxStartStopTimes, + expect(props.setDuration).toHaveBeenCalledWith({ + ...props.duration, [testValidIndex]: testValidValue, }); }); }); describe('if the passed startTime is after (or equal to) the stored non-zero stopTime', () => { - it('updates unsavedStartStopTimes startTime values to 1 second before stopTime', () => { + it('updates unsavedDuration startTime values to 1 second before stopTime', () => { hook({ ...props, index: testValidIndex, inputString: '00:10:00', }); - expect(props.setUnsavedStartStopTimes).toHaveBeenCalledWith({ - ...props.unsavedStartStopTimes, + expect(props.setUnsavedDuration).toHaveBeenCalledWith({ + ...props.unsavedDuration, [testValidIndex]: '00:09:59', }); - expect(props.setReduxStartStopTimes).toHaveBeenCalledWith({ - ...props.reduxStartStopTimes, + expect(props.setDuration).toHaveBeenCalledWith({ + ...props.duration, [testValidIndex]: 599000, }); }); }); describe('if the passed stopTime is before (or equal to) the stored startTime', () => { - it('updates unsavedStartStopTimes stopTime values to 1 second after startTime', () => { + it('updates unsavedDuration stopTime values to 1 second after startTime', () => { hook({ ...props, index: testStopIndex, inputString: '00:00:22', }); - expect(props.setUnsavedStartStopTimes).toHaveBeenCalledWith({ - ...props.unsavedStartStopTimes, + expect(props.setUnsavedDuration).toHaveBeenCalledWith({ + ...props.unsavedDuration, [testStopIndex]: '00:00:24', }); - expect(props.setReduxStartStopTimes).toHaveBeenCalledWith({ - ...props.reduxStartStopTimes, + expect(props.setDuration).toHaveBeenCalledWith({ + ...props.duration, [testStopIndex]: 24000, }); }); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.jsx index 85f2c5e79..ed0ff2e26 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.jsx @@ -1,9 +1,11 @@ import React from 'react'; -import { useDispatch } from 'react-redux'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; import { Col, Form } from '@edx/paragon'; import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; +import { actions, selectors } from '../../../../../../data/redux'; import { keyStore } from '../../../../../../utils'; import CollapsibleFormWidget from '../CollapsibleFormWidget'; import hooks from './hooks'; @@ -14,28 +16,28 @@ import messages from '../messages'; * Also displays the total run time of the video. */ export const DurationWidget = ({ + // redux + duration, + updateField, // injected intl, }) => { - const dispatch = useDispatch(); - const { - reduxStartStopTimes, - unsavedStartStopTimes, + unsavedDuration, onBlur, onChange, onKeyDown, getTotalLabel, - } = hooks.durationWidget({ dispatch }); + } = hooks.durationWidget({ duration, updateField }); - const timeKeys = keyStore(reduxStartStopTimes); + const timeKeys = keyStore(duration); return ( @@ -60,7 +62,7 @@ export const DurationWidget = ({ onBlur={onBlur(timeKeys.stopTime)} onChange={onChange(timeKeys.stopTime)} onKeyDown={onKeyDown(timeKeys.stopTime)} - value={unsavedStartStopTimes.stopTime} + value={unsavedDuration.stopTime} /> @@ -69,7 +71,7 @@ export const DurationWidget = ({
{getTotalLabel({ - duration: reduxStartStopTimes, + duration: duration, subtitle: false, intl, })} @@ -79,8 +81,19 @@ export const DurationWidget = ({ }; DurationWidget.propTypes = { + // redux + duration: PropTypes.objectOf(PropTypes.number).isRequired, + updateField: PropTypes.func.isRequired, // injected intl: intlShape.isRequired, }; -export default injectIntl(DurationWidget); +export const mapStateToProps = (state) => ({ + duration: selectors.video.duration(state), +}); + +export const mapDispatchToProps = { + updateField: actions.video.updateField, +}; + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(DurationWidget)); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.test.jsx index ab6a1da01..ddb9e8df9 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.test.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.test.jsx @@ -1,14 +1,30 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { actions, selectors } from '../../../../../../data/redux'; import { formatMessage } from '../../../../../../../testUtils'; -import { DurationWidget } from '.'; +import { DurationWidget, mapStateToProps, mapDispatchToProps } from '.'; + +jest.mock('../../../../../../data/redux', () => ({ + actions: { + video: { + updateField: jest.fn().mockName('actions.video.updateField'), + }, + }, + selectors: { + video: { + duration: jest.fn(state => ({ duration: state })), + }, + }, +})); describe('DurationWidget', () => { const props = { - isError: false, - subtitle: 'SuBTItle', - title: 'tiTLE', + duration: { + startTime: '00:00:00', + stopTime: '00:00:10', + }, + updateField: jest.fn().mockName('updateField'), // inject intl: { formatMessage }, }; @@ -19,4 +35,17 @@ describe('DurationWidget', () => { ).toMatchSnapshot(); }); }); + describe('mapStateToProps', () => { + const testState = { A: 'pple', B: 'anana', C: 'ucumber' }; + test('duration from video.duration', () => { + expect( + mapStateToProps(testState).duration, + ).toEqual(selectors.video.duration(testState)); + }); + }); + describe('mapDispatchToProps', () => { + test('updateField from actions.video.updateField', () => { + expect(mapDispatchToProps.updateField).toEqual(actions.video.updateField); + }); + }); }); diff --git a/src/editors/data/services/cms/mockApi.js b/src/editors/data/services/cms/mockApi.js index 0b1bf414d..94be91054 100644 --- a/src/editors/data/services/cms/mockApi.js +++ b/src/editors/data/services/cms/mockApi.js @@ -286,5 +286,3 @@ export const fetchStudioView = ({ blockId, studioEndpointUrl }) => { }, }); }; - -export const checkTranscriptsForImport = () => mockPromise({});