feat: add alert to notify video id changes (#295)

This commit is contained in:
Kristin Aoki
2023-04-06 12:34:07 -04:00
committed by GitHub
parent 82cfa9897c
commit 4cfc5a6ea6
6 changed files with 142 additions and 22 deletions

View File

@@ -5,6 +5,17 @@ exports[`VideoSourceWidget snapshots snapshots: renders as expected with default
fontSize="x-small"
title="Video source"
>
<ErrorAlert
dismissError={[Function]}
hideHeading={true}
isError={false}
>
<FormattedMessage
defaultMessage="The Video ID field has changed, please check the Video URL and fallback URL values and update them if necessary."
description="Body message for the alert that appears when the video id has been changed."
id="authoring.videoeditor.videoIdChangeAlert.message"
/>
</ErrorAlert>
<Form.Group>
<div
className="border-primary-100 border-bottom pb-4"
@@ -15,7 +26,7 @@ exports[`VideoSourceWidget snapshots snapshots: renders as expected with default
onChange={[MockFunction]}
value=""
/>
<Component
<Form.Control.Feedback
className="text-primary-300 mb-4"
>
<FormattedMessage
@@ -23,14 +34,14 @@ exports[`VideoSourceWidget snapshots snapshots: renders as expected with default
description="Feedback for video ID field"
id="authoring.videoeditor.videoSource.videoId.feedback"
/>
</Component>
</Form.Control.Feedback>
<Form.Control
floatingLabel="Video URL"
onBlur={[Function]}
onChange={[MockFunction]}
value=""
/>
<Component
<Form.Control.Feedback
className="text-primary-300"
>
<FormattedMessage
@@ -39,7 +50,7 @@ exports[`VideoSourceWidget snapshots snapshots: renders as expected with default
description="Feedback for video URL field"
id="authoring.videoeditor.videoSource.videoUrl.feedback"
/>
</Component>
</Form.Control.Feedback>
</div>
<div
className="mt-4"
@@ -147,6 +158,17 @@ exports[`VideoSourceWidget snapshots snapshots: renders as expected with videoSh
fontSize="x-small"
title="Video source"
>
<ErrorAlert
dismissError={[Function]}
hideHeading={true}
isError={false}
>
<FormattedMessage
defaultMessage="The Video ID field has changed, please check the Video URL and fallback URL values and update them if necessary."
description="Body message for the alert that appears when the video id has been changed."
id="authoring.videoeditor.videoIdChangeAlert.message"
/>
</ErrorAlert>
<Form.Group>
<div
className="border-primary-100 border-bottom pb-4"
@@ -157,7 +179,7 @@ exports[`VideoSourceWidget snapshots snapshots: renders as expected with videoSh
onChange={[MockFunction]}
value=""
/>
<Component
<Form.Control.Feedback
className="text-primary-300 mb-4"
>
<FormattedMessage
@@ -165,14 +187,14 @@ exports[`VideoSourceWidget snapshots snapshots: renders as expected with videoSh
description="Feedback for video ID field"
id="authoring.videoeditor.videoSource.videoId.feedback"
/>
</Component>
</Form.Control.Feedback>
<Form.Control
floatingLabel="Video URL"
onBlur={[Function]}
onChange={[MockFunction]}
value=""
/>
<Component
<Form.Control.Feedback
className="text-primary-300"
>
<FormattedMessage
@@ -181,7 +203,7 @@ exports[`VideoSourceWidget snapshots snapshots: renders as expected with videoSh
description="Feedback for video URL field"
id="authoring.videoeditor.videoSource.videoUrl.feedback"
/>
</Component>
</Form.Control.Feedback>
</div>
<div
className="mt-4"

View File

@@ -1,8 +1,13 @@
import React from 'react';
import { actions } from '../../../../../../data/redux';
import { parseYoutubeId } from '../../../../../../data/services/cms/api';
import * as requests from '../../../../../../data/redux/thunkActions/requests';
export const sourceHooks = ({ dispatch }) => ({
export const state = {
showVideoIdChangeAlert: (args) => React.useState(args),
};
export const sourceHooks = ({ dispatch, previousVideoId, setAlert }) => ({
updateVideoURL: (e, videoId) => {
const videoUrl = e.target.value;
dispatch(actions.video.updateField({ videoSource: videoUrl }));
@@ -22,7 +27,13 @@ export const sourceHooks = ({ dispatch }) => ({
}));
}
},
updateVideoId: (e) => dispatch(actions.video.updateField({ videoId: e.target.value })),
updateVideoId: (e) => {
const updatedVideoId = e.target.value;
if (previousVideoId !== updatedVideoId && updatedVideoId) {
setAlert();
}
dispatch(actions.video.updateField({ videoId: updatedVideoId }));
},
});
export const fallbackHooks = ({ fallbackVideos, dispatch }) => ({
@@ -33,7 +44,19 @@ export const fallbackHooks = ({ fallbackVideos, dispatch }) => ({
},
});
export const videoIdChangeAlert = () => {
const [showVideoIdChangeAlert, setShowVideoIdChangeAlert] = state.showVideoIdChangeAlert(false);
return {
videoIdChangeAlert: {
show: showVideoIdChangeAlert,
set: () => setShowVideoIdChangeAlert(true),
dismiss: () => setShowVideoIdChangeAlert(false),
},
};
};
export default {
videoIdChangeAlert,
sourceHooks,
fallbackHooks,
};

View File

@@ -1,8 +1,16 @@
import { dispatch } from 'react-redux';
import { actions } from '../../../../../../data/redux';
import { MockUseState } from '../../../../../../../testUtils';
import * as requests from '../../../../../../data/redux/thunkActions/requests';
import * as hooks from './hooks';
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 {
@@ -25,16 +33,24 @@ jest.mock('../../../../../../data/redux/thunkActions/requests', () => ({
checkTranscriptsForImport: jest.fn(),
}));
const state = new MockUseState(hooks);
const youtubeId = 'yOuTuBEiD';
const youtubeUrl = `https://youtu.be/${youtubeId}`;
describe('VideoEditorHandout hooks', () => {
let hook;
describe('state hooks', () => {
state.testGetter(state.keys.showVideoIdChangeAlert);
});
describe('sourceHooks', () => {
const e = { target: { value: 'soMEvALuE' } };
beforeEach(() => {
hook = hooks.sourceHooks({ dispatch });
hook = hooks.sourceHooks({
dispatch,
previousVideoId: 'soMEvALuE',
setAlert: jest.fn(),
});
});
afterEach(() => {
jest.clearAllMocks();
@@ -82,7 +98,23 @@ describe('VideoEditorHandout hooks', () => {
});
describe('updateVideoId', () => {
it('dispatches updateField action with new videoId', () => {
hook.updateVideoId(e);
hook.updateVideoId({ target: { value: 'newVideoId' } });
expect(dispatch).toHaveBeenCalledWith(
actions.video.updateField({
videoId: e.target.value,
}),
);
});
it('dispatches updateField action with empty string', () => {
hook.updateVideoId({ target: { value: '' } });
expect(dispatch).toHaveBeenCalledWith(
actions.video.updateField({
videoId: e.target.value,
}),
);
});
it('dispatches updateField action with previousVideoId', () => {
hook.updateVideoId({ target: { value: 'soMEvALuE' } });
expect(dispatch).toHaveBeenCalledWith(
actions.video.updateField({
videoId: e.target.value,
@@ -120,4 +152,23 @@ describe('VideoEditorHandout hooks', () => {
});
});
});
describe('videoIdChangeAlert', () => {
beforeEach(() => {
state.mock();
});
afterEach(() => {
state.restore();
});
test('showVideoIdChangeAlert: state values', () => {
expect(hooks.videoIdChangeAlert().videoIdChangeAlert.show).toEqual(false);
});
test('showVideoIdChangeAlert setters: set', () => {
hooks.videoIdChangeAlert().videoIdChangeAlert.set();
expect(state.setState[state.keys.showVideoIdChangeAlert]).toHaveBeenCalledWith(true);
});
test('showVideoIdChangeAlert setters: dismiss', () => {
hooks.videoIdChangeAlert().videoIdChangeAlert.dismiss();
expect(state.setState[state.keys.showVideoIdChangeAlert]).toHaveBeenCalledWith(false);
});
});
});

View File

@@ -10,7 +10,6 @@ import {
Button,
Tooltip,
OverlayTrigger,
FormControlFeedback,
} from '@edx/paragon';
import { DeleteOutline, InfoOutline, Add } from '@edx/paragon/icons';
import {
@@ -24,6 +23,7 @@ import * as hooks from './hooks';
import messages from './messages';
import { selectors } from '../../../../../../data/redux';
import { ErrorAlert } from '../../../../../../sharedComponents/ErrorAlerts/ErrorAlert';
import CollapsibleFormWidget from '../CollapsibleFormWidget';
/**
@@ -52,7 +52,12 @@ export const VideoSourceWidget = ({
[widgetHooks.selectorKeys.allowVideoSharing]: widgetHooks.genericWidget,
},
});
const { updateVideoId, updateVideoURL } = hooks.sourceHooks({ dispatch });
const { videoIdChangeAlert } = hooks.videoIdChangeAlert();
const { updateVideoId, updateVideoURL } = hooks.sourceHooks({
dispatch,
previousVideoId: videoId.formValue,
setAlert: videoIdChangeAlert.set,
});
const {
addFallbackVideo,
deleteFallbackVideo,
@@ -63,6 +68,13 @@ export const VideoSourceWidget = ({
fontSize="x-small"
title={intl.formatMessage(messages.titleLabel)}
>
<ErrorAlert
dismissError={videoIdChangeAlert.dismiss}
hideHeading
isError={videoIdChangeAlert.show}
>
<FormattedMessage {...messages.videoIdChangeAlert} />
</ErrorAlert>
<Form.Group>
<div className="border-primary-100 border-bottom pb-4">
<Form.Control
@@ -71,18 +83,18 @@ export const VideoSourceWidget = ({
onBlur={updateVideoId}
value={videoId.local}
/>
<FormControlFeedback className="text-primary-300 mb-4">
<Form.Control.Feedback className="text-primary-300 mb-4">
<FormattedMessage {...messages.videoIdFeedback} />
</FormControlFeedback>
</Form.Control.Feedback>
<Form.Control
floatingLabel={intl.formatMessage(messages.videoUrlLabel)}
onChange={source.onChange}
onBlur={(e) => updateVideoURL(e, videoId.local)}
value={source.local}
/>
<FormControlFeedback className="text-primary-300">
<Form.Control.Feedback className="text-primary-300">
<FormattedMessage {...messages.videoUrlFeedback} />
</FormControlFeedback>
</Form.Control.Feedback>
</div>
<div className="mt-4">
<FormattedMessage {...messages.fallbackVideoTitle} />

View File

@@ -32,6 +32,13 @@ jest.mock('../hooks', () => ({
}));
jest.mock('./hooks', () => ({
videoIdChangeAlert: jest.fn().mockReturnValue({
videoIdChangeAlert: {
set: (args) => ({ set: args }),
show: false,
dismiss: (args) => ({ dismiss: args }),
},
}),
sourceHooks: jest.fn().mockReturnValue({
updateVideoId: (args) => ({ updateVideoId: args }),
updateVideoURL: jest.fn().mockName('updateVideoURL'),
@@ -81,20 +88,20 @@ describe('VideoSourceWidget', () => {
let el;
let hook;
beforeEach(() => {
hook = hooks.sourceHooks({ dispatch, previousVideoId: 'someVideoId', setAlert: jest.fn() });
el = shallow(<VideoSourceWidget {...props} />);
hook = hooks.sourceHooks({ dispatch });
});
test('updateVideoId is tied to id field onBlur', () => {
const expected = hook.updateVideoId;
expect(el
// eslint-disable-next-line
.children().at(0).children().at(0).children().at(0)
.children().at(1).children().at(0).children().at(0)
.props().onBlur).toEqual(expected);
});
test('updateVideoURL is tied to url field onBlur', () => {
const { onBlur } = el
// eslint-disable-next-line
.children().at(0).children().at(0).children().at(2).props();
.children().at(1).children().at(0).children().at(2).props();
onBlur('onBlur event');
expect(hook.updateVideoURL).toHaveBeenCalledWith('onBlur event', '');
});

View File

@@ -28,6 +28,11 @@ const messages = defineMessages({
to an .mp4, .ogg, or .webm video file hosted elsewhere on the internet.`,
description: 'Feedback for video URL field',
},
videoIdChangeAlert: {
id: 'authoring.videoeditor.videoIdChangeAlert.message',
defaultMessage: 'The Video ID field has changed, please check the Video URL and fallback URL values and update them if necessary.',
description: 'Body message for the alert that appears when the video id has been changed.',
},
fallbackVideoTitle: {
id: 'authoring.videoeditor.videoSource.fallbackVideo.title',
defaultMessage: 'Fallback videos',