test: replacing snapshot tests with RTL tests part 15 (#2248)

This commit is contained in:
jacobo-dominguez-wgu
2025-07-07 17:44:01 -06:00
committed by GitHub
parent e8e5a3c4ce
commit c4f565bf76
21 changed files with 505 additions and 1231 deletions

View File

@@ -1,43 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SolutionWidget render snapshot: renders correct default 1`] = `
<div
className="tinyMceWidget mt-4 text-primary-500"
>
<div
className="h4 mb-3"
>
<FormattedMessage
defaultMessage="Explanation"
description="Explanation Title"
id="authoring.problemEditor.explanationwidget.explanationWidgetTitle"
/>
</div>
<div
className="small mb-3"
>
<FormattedMessage
defaultMessage="Provide an explanation for the correct answer"
description="Description of the solution widget"
id="authoring.problemEditor.explanationwidget.solutionDescriptionText"
/>
</div>
<TinyMceWidget
disabled={false}
editorContentHtml="This is my solution"
editorRef={null}
editorType="solution"
enableImageUpload={true}
id="solution"
images={{}}
isLibrary={false}
learningContextId="course+org+run"
lmsEndpointUrl=""
minHeight={150}
onChange={[Function]}
placeholder="Enter your explanation"
setEditorRef={[MockFunction prepareEditorRef.setEditorRef]}
studioEndpointUrl=""
/>
</div>
`;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { selectors } from '../../../../../data/redux';
@@ -16,9 +16,8 @@ const ExplanationWidget = ({
images,
isLibrary,
blockId,
// injected
intl,
}) => {
const intl = useIntl();
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
const initialContent = settings?.solutionExplanation || '';
const newContent = replaceStaticWithAsset({
@@ -66,8 +65,6 @@ ExplanationWidget.propTypes = {
images: PropTypes.shape({}).isRequired,
isLibrary: PropTypes.bool.isRequired,
blockId: PropTypes.string.isRequired,
// injected
intl: intlShape.isRequired,
};
export const mapStateToProps = (state) => ({
settings: selectors.problem.settings(state),
@@ -78,4 +75,4 @@ export const mapStateToProps = (state) => ({
});
export const ExplanationWidgetInternal = ExplanationWidget; // For testing only
export default injectIntl(connect(mapStateToProps)(ExplanationWidget));
export default connect(mapStateToProps)(ExplanationWidget);

View File

@@ -1,70 +0,0 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from '../../../../../testUtils';
import { selectors } from '../../../../../data/redux';
import { ExplanationWidgetInternal as ExplanationWidget, mapStateToProps } from '.';
jest.mock('../../../../../data/redux', () => ({
__esModule: true,
default: jest.fn(),
selectors: {
problem: {
settings: jest.fn(state => ({ question: state })),
},
app: {
learningContextId: jest.fn(state => ({ learningContextId: state })),
images: jest.fn(state => ({ images: state })),
isLibrary: jest.fn(state => ({ isLibrary: state })),
blockId: jest.fn(state => ({ blockId: state })),
},
},
thunkActions: {
video: {
importTranscript: jest.fn(),
},
},
}));
jest.mock('../../../../../sharedComponents/TinyMceWidget/hooks', () => ({
...jest.requireActual('../../../../../sharedComponents/TinyMceWidget/hooks'),
prepareEditorRef: jest.fn(() => ({
refReady: true,
setEditorRef: jest.fn().mockName('prepareEditorRef.setEditorRef'),
})),
}));
describe('SolutionWidget', () => {
const props = {
settings: { solutionExplanation: 'This is my solution' },
learningContextId: 'course+org+run',
images: {},
isLibrary: false,
// injected
intl: { formatMessage },
};
describe('render', () => {
test('snapshot: renders correct default', () => {
expect(shallow(<ExplanationWidget {...props} />).snapshot).toMatchSnapshot();
});
});
describe('mapStateToProps', () => {
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
test('settings from problem.settings', () => {
expect(mapStateToProps(testState).settings).toEqual(selectors.problem.settings(testState));
});
test('learningContextId from app.learningContextId', () => {
expect(mapStateToProps(testState).learningContextId).toEqual(selectors.app.learningContextId(testState));
});
test('images from app.images', () => {
expect(
mapStateToProps(testState).images,
).toEqual(selectors.app.images(testState));
});
test('isLibrary from app.isLibrary', () => {
expect(
mapStateToProps(testState).isLibrary,
).toEqual(selectors.app.isLibrary(testState));
});
});
});

View File

@@ -0,0 +1,56 @@
import React from 'react';
import { render, screen, initializeMocks } from '@src/testUtils';
import ExplanationWidget from '.';
jest.mock('../../../../../data/redux', () => ({
__esModule: true,
default: jest.fn(),
selectors: {
problem: {
settings: jest.fn(state => ({ question: state })),
},
app: {
learningContextId: jest.fn(state => ({ learningContextId: state })),
images: jest.fn(state => ({ images: state })),
isLibrary: jest.fn(state => ({ isLibrary: state })),
blockId: jest.fn(state => ({ blockId: state })),
},
},
thunkActions: {
video: {
importTranscript: jest.fn(),
},
},
}));
jest.mock('../../../../../sharedComponents/TinyMceWidget/hooks', () => ({
...jest.requireActual('../../../../../sharedComponents/TinyMceWidget/hooks'),
prepareEditorRef: jest.fn(() => ({
refReady: true,
setEditorRef: jest.fn().mockName('prepareEditorRef.setEditorRef'),
})),
}));
jest.mock('../../../../../sharedComponents/TinyMceWidget', () => ({
__esModule: true,
default: () => <div>TinyMceWidget</div>,
}));
describe('SolutionWidget', () => {
const props = {
settings: { solutionExplanation: 'This is my solution' },
learningContextId: 'course+org+run',
images: {},
isLibrary: false,
blockId: 'block-v1:Org+TS100+24+type@html+block@12345',
};
beforeEach(() => {
initializeMocks();
});
test('renders correct default', () => {
render(<ExplanationWidget {...props} />);
expect(screen.getByText('Explanation')).toBeInTheDocument();
expect(screen.getByText('Provide an explanation for the correct answer')).toBeInTheDocument();
expect(screen.getByText('TinyMceWidget')).toBeInTheDocument();
});
});

View File

@@ -1,35 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RandomizationCard snapshot snapshot: renders general feedback setting card 1`] = `
<SettingsOption
className=""
extraSections={[]}
hasExpandableTextArea={false}
none={false}
summary={
{
"defaultMessage": "sUmmary",
}
}
title="General Feedback"
>
<div
className="halfSpacedMessage"
>
<span>
<FormattedMessage
defaultMessage="Enter the feedback to appear when a student submits a wrong answer. This will be overridden if you add answer-specific feedback."
description="description for general feedback input, clariying useage"
id="authoring.problemeditor.settings.generalFeedbackInputDescription"
/>
</span>
</div>
<Form.Group>
<Form.Control
floatingLabel="Enter General Feedback"
onChange={[MockFunction randomizationCardHooks.handleChange]}
value="sOmE_vAlUE"
/>
</Form.Group>
</SettingsOption>
`;

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Form } from '@openedx/paragon';
import PropTypes from 'prop-types';
import SettingsOption from '../../SettingsOption';
@@ -9,9 +9,8 @@ import { generalFeedbackHooks } from './hooks';
export const GeneralFeedbackCard = ({
generalFeedback,
updateSettings,
// inject
intl,
}) => {
const intl = useIntl();
const { summary, handleChange } = generalFeedbackHooks(generalFeedback, updateSettings);
return (
<SettingsOption
@@ -38,7 +37,6 @@ export const GeneralFeedbackCard = ({
GeneralFeedbackCard.propTypes = {
generalFeedback: PropTypes.string.isRequired,
updateSettings: PropTypes.func.isRequired,
intl: intlShape.isRequired,
};
export default injectIntl(GeneralFeedbackCard);
export default GeneralFeedbackCard;

View File

@@ -1,38 +0,0 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from '../../../../../../../testUtils';
import { GeneralFeedbackCard } from './index';
import { generalFeedbackHooks } from './hooks';
jest.mock('./hooks', () => ({
generalFeedbackHooks: jest.fn(),
}));
describe('RandomizationCard', () => {
const props = {
generalFeedback: 'sOmE_vAlUE',
updateSettings: jest.fn().mockName('args.updateSettings'),
intl: { formatMessage },
};
const randomizationCardHooksProps = {
summary: { message: { defaultMessage: 'sUmmary' } },
handleChange: jest.fn().mockName('randomizationCardHooks.handleChange'),
};
generalFeedbackHooks.mockReturnValue(randomizationCardHooksProps);
describe('behavior', () => {
it(' calls generalFeedbackHooks with props when initialized', () => {
shallow(<GeneralFeedbackCard {...props} />);
expect(generalFeedbackHooks).toHaveBeenCalledWith(props.generalFeedback, props.updateSettings);
});
});
describe('snapshot', () => {
test('snapshot: renders general feedback setting card', () => {
expect(shallow(<GeneralFeedbackCard {...props} />).snapshot).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,44 @@
import React from 'react';
import {
render, screen, initializeMocks, fireEvent,
} from '@src/testUtils';
import { GeneralFeedbackCard } from './index';
import * as hooks from './hooks';
describe('GeneralFeedbackCard', () => {
const props = {
generalFeedback: 'sOmE_vAlUE',
updateSettings: jest.fn().mockName('args.updateSettings'),
};
const randomizationCardHooksProps = {
summary: { message: { defaultMessage: 'sUmmary' } },
handleChange: jest.fn().mockName('randomizationCardHooks.handleChange'),
};
beforeEach(() => {
initializeMocks();
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('behavior', () => {
it('calls generalFeedbackHooks with props when initialized', () => {
jest.spyOn(hooks, 'generalFeedbackHooks').mockImplementation(() => randomizationCardHooksProps);
render(<GeneralFeedbackCard {...props} />);
expect(hooks.generalFeedbackHooks).toHaveBeenCalledWith(props.generalFeedback, props.updateSettings);
});
});
describe('render', () => {
test('renders general feedback setting card', () => {
render(<GeneralFeedbackCard {...props} />);
expect(screen.getByText('General Feedback')).toBeInTheDocument();
expect(screen.getByText('sOmE_vAlUE')).toBeInTheDocument();
fireEvent.click(screen.getByText('General Feedback'));
expect(screen.getByText('Enter General Feedback')).toBeInTheDocument();
});
});
});

View File

@@ -1,171 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LicenseWidget snapshots snapshots: renders as expected with default props 1`] = `
<CollapsibleFormWidget
fontSize=""
subtitle={
<div>
<LicenseBlurb
details={{}}
license="all-rights-reserved"
/>
<div
className="x-small mt-2"
>
<FormattedMessage
defaultMessage="This license currently set at the course level"
description="Helper text for license type when using course license"
id="authoring.videoeditor.license.courseLevelDescription.helperText"
/>
</div>
</div>
}
title="License"
>
<Stack
gap={4}
>
<Fragment>
<[object Object]
level="course"
license="all-rights-reserved"
/>
<injectIntl(ShimmedIntlComponent)
details={{}}
level="course"
license="all-rights-reserved"
/>
<LicenseDisplay
details={{}}
license="all-rights-reserved"
licenseDescription={
<FormattedMessage
defaultMessage="Licenses set at the course level appear at the bottom of courseware pages within your course."
description="Message explaining where course level licenses are set"
id="authoring.videoeditor.license.courseLicenseDescription.message"
/>
}
/>
</Fragment>
<Fragment>
<div
className="border-primary-100 border-bottom my-2"
/>
<Button
className="text-primary-500 font-weight-bold justify-content-start pl-0"
onClick={[Function]}
size="sm"
variant="link"
>
<FormattedMessage
defaultMessage="Add a license for this video"
description="Label for add license button"
id="authoring.videoeditor.license.add.label"
/>
</Button>
</Fragment>
</Stack>
</CollapsibleFormWidget>
`;
exports[`LicenseWidget snapshots snapshots: renders as expected with isLibrary true 1`] = `
<CollapsibleFormWidget
fontSize=""
subtitle={
<div>
<LicenseBlurb
details={{}}
license="all-rights-reserved"
/>
<div
className="x-small mt-2"
>
<FormattedMessage
defaultMessage="This license currently set at the library level"
description="Helper text for license type when using library license"
id="authoring.videoeditor.license.libraryLevelDescription.helperText"
/>
</div>
</div>
}
title="License"
>
<Stack
gap={4}
>
<Fragment>
<[object Object]
level="library"
license="all-rights-reserved"
/>
<injectIntl(ShimmedIntlComponent)
details={{}}
level="library"
license="all-rights-reserved"
/>
<LicenseDisplay
details={{}}
license="all-rights-reserved"
licenseDescription={
<FormattedMessage
defaultMessage="Licenses set at the library level appear at the specific library video."
description="Message explaining where library level licenses are set"
id="authoring.videoeditor.license.libraryLicenseDescription.message"
/>
}
/>
</Fragment>
</Stack>
</CollapsibleFormWidget>
`;
exports[`LicenseWidget snapshots snapshots: renders as expected with licenseType defined 1`] = `
<CollapsibleFormWidget
fontSize=""
subtitle={
<div>
<LicenseBlurb
details={{}}
license="all-rights-reserved"
/>
<div
className="x-small mt-2"
>
<FormattedMessage
defaultMessage="This license is set specifically for this video"
description="Helper text for license type when choosing for a spcific video"
id="authoring.videoeditor.license.defaultLevelDescription.helperText"
/>
</div>
</div>
}
title="License"
>
<Stack
gap={4}
>
<Fragment>
<[object Object]
level="block"
license="all-rights-reserved"
/>
<injectIntl(ShimmedIntlComponent)
details={{}}
level="block"
license="all-rights-reserved"
/>
<LicenseDisplay
details={{}}
license="all-rights-reserved"
licenseDescription={
<FormattedMessage
defaultMessage="When a video has a different license than the course as a whole, learners see the license at the bottom right of the video player."
description="Message explaining where video specific licenses are seen by users"
id="authoring.videoeditor.license.defaultLicenseDescription.message"
/>
}
/>
</Fragment>
</Stack>
</CollapsibleFormWidget>
`;

View File

@@ -1,112 +0,0 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from '../../../../../../testUtils';
import { actions, selectors } from '../../../../../../data/redux';
import { LicenseWidgetInternal as LicenseWidget, mapStateToProps, mapDispatchToProps } from '.';
jest.mock('react', () => {
const updateState = jest.fn();
return {
...jest.requireActual('react'),
updateState,
useContext: jest.fn(() => ({ license: ['error.license', jest.fn().mockName('error.setLicense')] })),
};
});
jest.mock('../../../../../../data/redux', () => ({
actions: {
video: {
updateField: jest.fn().mockName('actions.video.updateField'),
},
},
selectors: {
app: {
isLibrary: jest.fn(state => ({ isLibrary: state })),
},
video: {
licenseType: jest.fn(state => ({ licenseType: state })),
licenseDetails: jest.fn(state => ({ licenseDetails: state })),
courseLicenseType: jest.fn(state => ({ courseLicenseType: state })),
courseLicenseDetails: jest.fn(state => ({ courseLicenseDetails: state })),
},
},
}));
describe('LicenseWidget', () => {
const props = {
error: {},
subtitle: 'SuBTItle',
title: 'tiTLE',
intl: { formatMessage },
isLibrary: false,
licenseType: null,
licenseDetails: {},
courseLicenseType: 'all-rights-reserved',
courseLicenseDetails: {},
updateField: jest.fn().mockName('args.updateField'),
};
describe('snapshots', () => {
// determineLicense.mockReturnValue({
// license: false,
// details: jest.fn().mockName('modal.openModal'),
// level: 'course',
// });
// determineText.mockReturnValue({
// isSourceCodeOpen: false,
// openSourceCodeModal: jest.fn().mockName('modal.openModal'),
// closeSourceCodeModal: jest.fn().mockName('modal.closeModal'),
// });
test('snapshots: renders as expected with default props', () => {
expect(
shallow(<LicenseWidget {...props} />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with isLibrary true', () => {
expect(
shallow(<LicenseWidget {...props} isLibrary licenseType="all-rights-reserved" />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with licenseType defined', () => {
expect(
shallow(<LicenseWidget {...props} licenseType="all-rights-reserved" />).snapshot,
).toMatchSnapshot();
});
});
describe('mapStateToProps', () => {
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
test('isLibrary from app.isLibrary', () => {
expect(
mapStateToProps(testState).isLibrary,
).toEqual(selectors.app.isLibrary(testState));
});
test('licenseType from video.licenseType', () => {
expect(
mapStateToProps(testState).licenseType,
).toEqual(selectors.video.licenseType(testState));
});
test('licenseDetails from video.licenseDetails', () => {
expect(
mapStateToProps(testState).licenseDetails,
).toEqual(selectors.video.licenseDetails(testState));
});
test('courseLicenseType from video.courseLicenseType', () => {
expect(
mapStateToProps(testState).courseLicenseType,
).toEqual(selectors.video.courseLicenseType(testState));
});
test('courseLicenseDetails from video.courseLicenseDetails', () => {
expect(
mapStateToProps(testState).courseLicenseDetails,
).toEqual(selectors.video.courseLicenseDetails(testState));
});
});
describe('mapDispatchToProps', () => {
const dispatch = jest.fn();
test('updateField from actions.video.updateField', () => {
expect(mapDispatchToProps.updateField).toEqual(dispatch(actions.video.updateField));
});
});
});

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { render, screen, initializeMocks } from '@src/testUtils';
import { formatMessage } from '../../../../../../testUtils';
import { LicenseWidgetInternal as LicenseWidget } from '.';
jest.mock('../../../../../../data/redux', () => ({
actions: {
video: {
updateField: jest.fn().mockName('actions.video.updateField'),
},
},
selectors: {
app: {
isLibrary: jest.fn(state => ({ isLibrary: state })),
},
video: {
licenseType: jest.fn(state => ({ licenseType: state })),
licenseDetails: jest.fn(state => ({ licenseDetails: state })),
courseLicenseType: jest.fn(state => ({ courseLicenseType: state })),
courseLicenseDetails: jest.fn(state => ({ courseLicenseDetails: state })),
},
},
}));
describe('LicenseWidget', () => {
const props = {
intl: { formatMessage },
isLibrary: false,
licenseType: '',
licenseDetails: {},
courseLicenseType: 'all-rights-reserved',
courseLicenseDetails: {},
updateField: jest.fn().mockName('args.updateField'),
};
beforeEach(() => {
initializeMocks();
});
test('renders as expected with default props', () => {
render(<LicenseWidget {...props} />);
expect(screen.getByText('License')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Add a license for this video' })).toBeInTheDocument();
});
test('renders as expected with isLibrary true', () => {
render(<LicenseWidget {...props} isLibrary licenseType="all-rights-reserved" />);
expect(screen.getByText('License')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Add a license for this video' })).not.toBeInTheDocument();
});
test('renders as expected with licenseType defined', () => {
render(<LicenseWidget {...props} licenseType="all-rights-reserved" />);
expect(screen.getByText('License')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Add a license for this video' })).not.toBeInTheDocument();
});
});

View File

@@ -1,156 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ThumbnailWidget snapshots snapshots: renders as expected where thumbnail uploads are allowed 1`] = `null`;
exports[`ThumbnailWidget snapshots snapshots: renders as expected where videoId is valid 1`] = `
<CollapsibleFormWidget
fontSize="x-small"
isError={true}
subtitle="Yes"
title="Thumbnail"
>
<ErrorAlert
dismissError={[Function]}
hideHeading={true}
isError={false}
>
<FormattedMessage
defaultMessage="The file size for thumbnails must be larger than 2 KB or less than 2 MB. Please resize your image and try again."
description=" Message presented to user when file size of image is less than 2 KB or larger than 2 MB"
id="authoring.videoeditor.thumbnail.error.fileSizeError"
/>
</ErrorAlert>
<Stack
direction="horizontal"
gap={3}
>
<Image
alt="Image used as thumbnail for video"
className="w-75"
fluid={true}
src="sOMeUrl"
thumbnail={true}
/>
<IconButtonWithTooltip
iconAs="Icon"
onClick={[Function]}
tooltipContent="Delete"
tooltipPlacement="top"
/>
</Stack>
</CollapsibleFormWidget>
`;
exports[`ThumbnailWidget snapshots snapshots: renders as expected where videoId is valid and no thumbnail 1`] = `
<CollapsibleFormWidget
fontSize="x-small"
isError={true}
subtitle="None"
title="Thumbnail"
>
<ErrorAlert
dismissError={[Function]}
hideHeading={true}
isError={false}
>
<FormattedMessage
defaultMessage="The file size for thumbnails must be larger than 2 KB or less than 2 MB. Please resize your image and try again."
description=" Message presented to user when file size of image is less than 2 KB or larger than 2 MB"
id="authoring.videoeditor.thumbnail.error.fileSizeError"
/>
</ErrorAlert>
<Stack
gap={4}
>
<div
className="text-center"
>
<FormattedMessage
defaultMessage="Upload an image for learners to see before playing the video."
description="Message for adding thumbnail"
id="authoring.videoeditor.thumbnail.upload.message"
/>
<div
className="text-primary-300"
>
<FormattedMessage
defaultMessage="Images must have an aspect ratio of 16:9 (1280x720 px recommended)"
description="Message for thumbnail aspectRequirements"
id="authoring.videoeditor.thumbnail.upload.aspectRequirements"
/>
</div>
</div>
<FileInput
acceptedFiles=".gif,.jpg,.jpeg,.png,.bmp,.bmp2"
fileInput={
{
"addFile": [Function],
"click": [Function],
"ref": {
"current": undefined,
},
}
}
/>
<Button
className="text-primary-500 font-weight-bold justify-content-start pl-0"
disabled={false}
onClick={[Function]}
size="sm"
variant="link"
>
<FormattedMessage
defaultMessage="Upload thumbnail"
description="Label for upload button"
id="authoring.videoeditor.thumbnail.upload.label"
/>
</Button>
</Stack>
</CollapsibleFormWidget>
`;
exports[`ThumbnailWidget snapshots snapshots: renders as expected with a thumbnail provided 1`] = `
<CollapsibleFormWidget
fontSize="x-small"
isError={true}
subtitle="Yes"
title="Thumbnail"
>
<ErrorAlert
dismissError={[Function]}
hideHeading={true}
isError={false}
>
<FormattedMessage
defaultMessage="The file size for thumbnails must be larger than 2 KB or less than 2 MB. Please resize your image and try again."
description=" Message presented to user when file size of image is less than 2 KB or larger than 2 MB"
id="authoring.videoeditor.thumbnail.error.fileSizeError"
/>
</ErrorAlert>
<Alert
variant="light"
>
<FormattedMessage
defaultMessage="Select a video from your library to enable this feature (applies only to courses that run on the edx.org site)."
description="Message for unavailable thumbnail widget"
id="authoring.videoeditor.thumbnail.unavailable.message"
/>
</Alert>
<Stack
direction="horizontal"
gap={3}
>
<Image
alt="Image used as thumbnail for video"
className="w-75"
fluid={true}
src="sOMeUrl"
thumbnail={true}
/>
</Stack>
</CollapsibleFormWidget>
`;
exports[`ThumbnailWidget snapshots snapshots: renders as expected with default props 1`] = `null`;
exports[`ThumbnailWidget snapshots snapshots: renders as expected with isLibrary true 1`] = `null`;

View File

@@ -1,106 +0,0 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from '../../../../../../testUtils';
import { selectors } from '../../../../../../data/redux';
import { ThumbnailWidgetInternal as ThumbnailWidget, mapStateToProps, mapDispatchToProps } from '.';
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(() => ({ thumbnail: ['error.thumbnail', jest.fn().mockName('error.setThumbnail')] })),
}));
jest.mock('../../../../../../data/redux', () => ({
actions: {
video: {
updateField: jest.fn().mockName('actions.video.updateField'),
},
},
selectors: {
video: {
allowThumbnailUpload: jest.fn(state => ({ allowThumbnailUpload: state })),
thumbnail: jest.fn(state => ({ thumbnail: state })),
videoId: jest.fn(state => ({ videoId: state })),
},
app: {
isLibrary: jest.fn(state => ({ isLibrary: state })),
},
},
}));
jest.mock('../../../../../../data/services/cms/api', () => ({
isEdxVideo: (args) => (args),
}));
describe('ThumbnailWidget', () => {
const props = {
error: {},
title: 'tiTLE',
intl: { formatMessage },
isLibrary: false,
allowThumbnailUpload: false,
thumbnail: null,
videoId: '',
updateField: jest.fn().mockName('args.updateField'),
};
describe('snapshots', () => {
test('snapshots: renders as expected with default props', () => {
expect(
shallow(<ThumbnailWidget {...props} />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with isLibrary true', () => {
expect(
shallow(<ThumbnailWidget {...props} isLibrary />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with a thumbnail provided', () => {
expect(
shallow(<ThumbnailWidget {...props} thumbnail="sOMeUrl" videoId="sOMeViDEoID" />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected where thumbnail uploads are allowed', () => {
expect(
shallow(<ThumbnailWidget {...props} thumbnail="sOMeUrl" allowThumbnailUpload />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected where videoId is valid', () => {
expect(
shallow(<ThumbnailWidget {...props} thumbnail="sOMeUrl" allowThumbnailUpload videoId="sOMeViDEoID" />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected where videoId is valid and no thumbnail', () => {
expect(
shallow(<ThumbnailWidget {...props} allowThumbnailUpload videoId="sOMeViDEoID" />).snapshot,
).toMatchSnapshot();
});
});
describe('mapStateToProps', () => {
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
test('isLibrary from app.isLibrary', () => {
expect(
mapStateToProps(testState).isLibrary,
).toEqual(selectors.app.isLibrary(testState));
});
test('allowThumbnailUpload from video.allowThumbnailUpload', () => {
expect(
mapStateToProps(testState).allowThumbnailUpload,
).toEqual(selectors.video.allowThumbnailUpload(testState));
});
test('thumbnail from video.thumbnail', () => {
expect(
mapStateToProps(testState).thumbnail,
).toEqual(selectors.video.thumbnail(testState));
});
test('videoId from video.videoId', () => {
expect(
mapStateToProps(testState).videoId,
).toEqual(selectors.video.videoId(testState));
});
});
describe('mapDispatchToProps', () => {
test('mapDispatchToProps to equal an empty object', () => {
expect(mapDispatchToProps).toEqual({});
});
});
});

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { render, screen, initializeMocks } from '@src/testUtils';
import { formatMessage } from '../../../../../../testUtils';
import { ThumbnailWidgetInternal as ThumbnailWidget } from '.';
jest.mock('../../../../../../data/redux', () => ({
actions: {
video: {
updateField: jest.fn().mockName('actions.video.updateField'),
},
},
selectors: {
video: {
allowThumbnailUpload: jest.fn(state => ({ allowThumbnailUpload: state })),
thumbnail: jest.fn(state => ({ thumbnail: state })),
videoId: jest.fn(state => ({ videoId: state })),
},
app: {
isLibrary: jest.fn(state => ({ isLibrary: state })),
},
},
}));
jest.mock('../../../../../../data/services/cms/api', () => ({
isEdxVideo: (args) => (args),
}));
describe('ThumbnailWidget', () => {
const props = {
error: {},
title: 'tiTLE',
intl: { formatMessage },
isLibrary: false,
allowThumbnailUpload: false,
thumbnail: '',
videoId: '',
updateField: jest.fn().mockName('args.updateField'),
};
beforeEach(() => {
initializeMocks();
});
describe('snapshots', () => {
test('snapshots: renders as expected with default props', () => {
const { container } = render(<ThumbnailWidget {...props} />);
const reduxWrapper = container.getRootNode();
expect(reduxWrapper.textContent).toBe(null);
});
test('snapshots: renders as expected with isLibrary true', () => {
const { container } = render(<ThumbnailWidget {...props} isLibrary />);
const reduxWrapper = container.getRootNode();
expect(reduxWrapper.textContent).toBe(null);
});
test('snapshots: renders as expected with a thumbnail provided', () => {
render(<ThumbnailWidget {...props} thumbnail="sOMeUrl" videoId="sOMeViDEoID" />);
expect(screen.getByRole('img', { name: 'Image used as thumbnail for video' })).toBeInTheDocument();
});
test('snapshots: renders as expected where thumbnail uploads are allowed', () => {
const { container } = render(<ThumbnailWidget {...props} thumbnail="sOMeUrl" videoId="sOMeViDEoID" allowThumbnailUpload />);
const reduxWrapper = container.getRootNode();
expect(reduxWrapper.textContent).toBe(null);
});
test('snapshots: renders as expected where videoId is valid', () => {
render(<ThumbnailWidget {...props} thumbnail="sOMeUrl" allowThumbnailUpload videoId="sOMeViDEoID" />);
expect(screen.getByRole('img', { name: 'Image used as thumbnail for video' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Thumbnail' })).toBeInTheDocument();
});
test('snapshots: renders as expected where videoId is valid and no thumbnail', () => {
render(<ThumbnailWidget {...props} allowThumbnailUpload videoId="sOMeViDEoID" />);
expect(screen.getByRole('button', { name: 'Thumbnail' })).toBeInTheDocument();
expect(screen.queryByRole('img', { name: 'Image used as thumbnail for video' })).not.toBeInTheDocument();
});
});
});

View File

@@ -1,317 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VideoSourceWidget snapshots snapshots: renders as expected with default props 1`] = `
<CollapsibleFormWidget
fontSize="x-small"
subtitle={null}
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>
<div
className="border-primary-100 border-bottom pb-4"
>
<Form.Group>
<Form.Control
floatingLabel="Video ID"
onBlur={[Function]}
onChange={[MockFunction]}
value=""
/>
<Form.Control.Feedback
className="text-primary-300 mb-4"
>
<FormattedMessage
defaultMessage="If you were assigned a video ID by edX, enter the ID here."
description="Feedback for video ID field"
id="authoring.videoeditor.videoSource.videoId.feedback"
/>
</Form.Control.Feedback>
</Form.Group>
<Form.Group>
<Form.Control
floatingLabel="Video URL"
onBlur={[Function]}
onChange={[MockFunction]}
value=""
/>
<Form.Control.Feedback
className="text-primary-300"
>
<FormattedMessage
defaultMessage="The URL for your video. This can be a YouTube URL, or a link
to an .mp4, .ogg, or .webm video file hosted elsewhere on the internet."
description="Feedback for video URL field"
id="authoring.videoeditor.videoSource.videoUrl.feedback"
/>
</Form.Control.Feedback>
</Form.Group>
</div>
<div
className="mt-4"
>
<FormattedMessage
defaultMessage="Fallback videos"
description="Title for the fallback videos section"
id="authoring.videoeditor.videoSource.fallbackVideo.title"
/>
</div>
<div
className="mt-3"
>
<FormattedMessage
defaultMessage="To be sure all learners can access the video, it is recommended to provide additional videos in both .mp4 and
.webm formats. The first listed video compatible with the learner's device will play."
description="Test explaining reason for fallback videos"
id="authoring.videoeditor.videoSource.fallbackVideo.message"
/>
</div>
<Form.Row
className="mt-3.5 mx-0 flex-nowrap"
>
<Form.Group>
<Form.Control
floatingLabel="Video URL"
/>
<IconButtonWithTooltip
alt="Delete"
iconAs="Icon"
key="top-delete-somEUrL"
onClick={[Function]}
tooltipContent="Delete"
tooltipPlacement="top"
/>
</Form.Group>
</Form.Row>
<ActionRow
className="mt-4.5"
>
<Form.Group>
<Form.Checkbox
checked={false}
className="decorative-control-label"
onChange={[MockFunction]}
>
<div
className="small text-gray-700"
>
<FormattedMessage
defaultMessage="Allow video downloads"
description="Label for allow video downloads checkbox"
id="authoring.videoeditor.videoSource.allowDownloadCheckboxLabel"
/>
</div>
</Form.Checkbox>
<OverlayTrigger
key="top"
overlay={
<Tooltip
id="tooltip-top"
>
<FormattedMessage
defaultMessage="Allow learners to download versions of this video in
different formats if they cannot use the edX video player or do not have
access to YouTube."
description="Message for allow video downloads checkbox"
id="authoring.videoeditor.videoSource.allowDownloadTooltipMessage"
/>
</Tooltip>
}
placement="top"
>
<Icon
style={
{
"height": "16px",
"width": "16px",
}
}
/>
</OverlayTrigger>
</Form.Group>
<ActionRow.Spacer />
</ActionRow>
<div
className="my-4 border-primary-100 border-bottom"
/>
<Button
className="text-primary-500 font-weight-bold pl-0"
onClick={[Function]}
size="sm"
variant="link"
>
<FormattedMessage
defaultMessage="Add a video URL"
description="Label for add a video URL button"
id="authoring.videoeditor.videoSource.fallbackVideo.addButtonLabel"
/>
</Button>
</CollapsibleFormWidget>
`;
exports[`VideoSourceWidget snapshots snapshots: renders as expected with videoSharingEnabledForCourse=true 1`] = `
<CollapsibleFormWidget
fontSize="x-small"
subtitle={null}
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>
<div
className="border-primary-100 border-bottom pb-4"
>
<Form.Group>
<Form.Control
floatingLabel="Video ID"
onBlur={[Function]}
onChange={[MockFunction]}
value=""
/>
<Form.Control.Feedback
className="text-primary-300 mb-4"
>
<FormattedMessage
defaultMessage="If you were assigned a video ID by edX, enter the ID here."
description="Feedback for video ID field"
id="authoring.videoeditor.videoSource.videoId.feedback"
/>
</Form.Control.Feedback>
</Form.Group>
<Form.Group>
<Form.Control
floatingLabel="Video URL"
onBlur={[Function]}
onChange={[MockFunction]}
value=""
/>
<Form.Control.Feedback
className="text-primary-300"
>
<FormattedMessage
defaultMessage="The URL for your video. This can be a YouTube URL, or a link
to an .mp4, .ogg, or .webm video file hosted elsewhere on the internet."
description="Feedback for video URL field"
id="authoring.videoeditor.videoSource.videoUrl.feedback"
/>
</Form.Control.Feedback>
</Form.Group>
</div>
<div
className="mt-4"
>
<FormattedMessage
defaultMessage="Fallback videos"
description="Title for the fallback videos section"
id="authoring.videoeditor.videoSource.fallbackVideo.title"
/>
</div>
<div
className="mt-3"
>
<FormattedMessage
defaultMessage="To be sure all learners can access the video, it is recommended to provide additional videos in both .mp4 and
.webm formats. The first listed video compatible with the learner's device will play."
description="Test explaining reason for fallback videos"
id="authoring.videoeditor.videoSource.fallbackVideo.message"
/>
</div>
<Form.Row
className="mt-3.5 mx-0 flex-nowrap"
>
<Form.Group>
<Form.Control
floatingLabel="Video URL"
/>
<IconButtonWithTooltip
alt="Delete"
iconAs="Icon"
key="top-delete-somEUrL"
onClick={[Function]}
tooltipContent="Delete"
tooltipPlacement="top"
/>
</Form.Group>
</Form.Row>
<ActionRow
className="mt-4.5"
>
<Form.Group>
<Form.Checkbox
checked={false}
className="decorative-control-label"
onChange={[MockFunction]}
>
<div
className="small text-gray-700"
>
<FormattedMessage
defaultMessage="Allow video downloads"
description="Label for allow video downloads checkbox"
id="authoring.videoeditor.videoSource.allowDownloadCheckboxLabel"
/>
</div>
</Form.Checkbox>
<OverlayTrigger
key="top"
overlay={
<Tooltip
id="tooltip-top"
>
<FormattedMessage
defaultMessage="Allow learners to download versions of this video in
different formats if they cannot use the edX video player or do not have
access to YouTube."
description="Message for allow video downloads checkbox"
id="authoring.videoeditor.videoSource.allowDownloadTooltipMessage"
/>
</Tooltip>
}
placement="top"
>
<Icon
style={
{
"height": "16px",
"width": "16px",
}
}
/>
</OverlayTrigger>
</Form.Group>
<ActionRow.Spacer />
</ActionRow>
<div
className="my-4 border-primary-100 border-bottom"
/>
<Button
className="text-primary-500 font-weight-bold pl-0"
onClick={[Function]}
size="sm"
variant="link"
>
<FormattedMessage
defaultMessage="Add a video URL"
description="Label for add a video URL button"
id="authoring.videoeditor.videoSource.fallbackVideo.addButtonLabel"
/>
</Button>
</CollapsibleFormWidget>
`;

View File

@@ -13,8 +13,7 @@ import {
import { DeleteOutline, InfoOutline, Add } from '@openedx/paragon/icons';
import {
FormattedMessage,
injectIntl,
intlShape,
useIntl,
} from '@edx/frontend-platform/i18n';
import * as widgetHooks from '../hooks';
@@ -27,10 +26,8 @@ import CollapsibleFormWidget from '../CollapsibleFormWidget';
/**
* Collapsible Form widget controlling video source as well as fallback sources
*/
const VideoSourceWidget = ({
// injected
intl,
}) => {
const VideoSourceWidget = () => {
const intl = useIntl();
const dispatch = useDispatch();
const {
videoId,
@@ -160,10 +157,5 @@ const VideoSourceWidget = ({
</CollapsibleFormWidget>
);
};
VideoSourceWidget.propTypes = {
// injected
intl: intlShape.isRequired,
};
export const VideoSourceWidgetInternal = VideoSourceWidget; // For testing only
export default injectIntl(VideoSourceWidget);
export default VideoSourceWidget;

View File

@@ -1,133 +0,0 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { dispatch } from 'react-redux';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen, fireEvent } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from '../../../../../../testUtils';
import { VideoSourceWidgetInternal as VideoSourceWidget } from '.';
import * as hooks from './hooks';
import messages from './messages';
jest.mock('react-redux', () => {
const dispatchFn = jest.fn();
return {
...jest.requireActual('react-redux'),
dispatch: dispatchFn,
useDispatch: jest.fn(() => dispatchFn),
};
});
jest.mock('../hooks', () => ({
selectorKeys: ['soMEkEy'],
widgetValues: jest.fn().mockReturnValue({
videoId: { onChange: jest.fn(), onBlur: jest.fn(), local: '' },
videoSource: { onChange: jest.fn(), onBlur: jest.fn(), local: '' },
fallbackVideos: {
formValue: ['somEUrL'],
onChange: jest.fn(),
onBlur: jest.fn(),
local: '',
},
allowVideoDownloads: { local: false, onCheckedChange: jest.fn() },
allowVideoSharing: { local: false, onCheckedChange: jest.fn() },
}),
}));
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'),
}),
fallbackHooks: jest.fn().mockReturnValue({
addFallbackVideo: jest.fn().mockName('addFallbackVideo'),
deleteFallbackVideo: jest.fn().mockName('deleteFallbackVideo'),
}),
}));
jest.mock('../../../../../../data/redux', () => ({
selectors: {
video: {
allow: jest.fn(state => ({ allowTranscriptImport: state })),
},
requests: {
isFailed: jest.fn(state => ({ isFailed: state })),
},
},
}));
describe('VideoSourceWidget', () => {
const props = {
// inject
intl: { formatMessage },
// redux
videoSharingEnabledForCourse: false,
};
describe('snapshots', () => {
describe('snapshots: renders as expected with', () => {
it('default props', () => {
expect(
shallow(<VideoSourceWidget {...props} />).snapshot,
).toMatchSnapshot();
});
it('videoSharingEnabledForCourse=true', () => {
const newProps = { ...props, videoSharingEnabledForCourse: true };
expect(
shallow(<VideoSourceWidget {...newProps} />).snapshot,
).toMatchSnapshot();
});
});
});
describe('behavior inspection', () => {
let el;
let hook;
beforeEach(() => {
hook = hooks.sourceHooks({ dispatch, previousVideoId: 'someVideoId', setAlert: jest.fn() });
el = shallow(<VideoSourceWidget {...props} />);
});
test('updateVideoId is tied to id field onBlur', () => {
const expected = hook.updateVideoId;
expect(el
// eslint-disable-next-line
.shallowWrapper.props.children[1].props.children[0].props.children[0]
.props.onBlur).toEqual(expected);
});
test('updateVideoURL is tied to url field onBlur', () => {
const control = el.shallowWrapper.props.children[1].props.children[1].props.children[0];
expect(control.props.floatingLabel).toEqual('Video URL');
control.props.onBlur('onBlur event');
expect(hook.updateVideoURL).toHaveBeenCalledWith('onBlur event', '');
});
});
describe('VideoSourceWidget', () => {
it('calls addFallbackVideo when the add button is clicked', () => {
// eslint-disable-next-line global-require
const { fallbackHooks } = require('./hooks');
const { addFallbackVideo } = fallbackHooks();
render(
<IntlProvider locale="en" messages={messages}>
<VideoSourceWidget intl={{ formatMessage: ({ defaultMessage }) => defaultMessage }} />
</IntlProvider>,
);
// Find the button by its text (from messages.addButtonLabel)
const addButton = screen.getByRole('button');
fireEvent.click(addButton);
expect(addFallbackVideo).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,248 @@
import React from 'react';
import {
render, screen, fireEvent, initializeMocks,
} from '@src/testUtils';
import VideoSourceWidget from '.';
import * as hooks from './hooks';
import * as widgetHooks from '../hooks';
describe('VideoSourceWidget', () => {
let widgetValuesSpy;
let videoIdChangeAlertSpy;
let sourceHooksSpy;
let fallbackHooksSpy;
beforeEach(() => {
jest.resetModules();
widgetValuesSpy = jest.spyOn(widgetHooks, 'widgetValues');
videoIdChangeAlertSpy = jest.spyOn(hooks, 'videoIdChangeAlert');
sourceHooksSpy = jest.spyOn(hooks, 'sourceHooks');
fallbackHooksSpy = jest.spyOn(hooks, 'fallbackHooks');
initializeMocks();
});
it('renders all main fields and labels', () => {
widgetValuesSpy.mockReturnValue({
videoId: {
onChange: jest.fn(), onBlur: jest.fn(), local: '', formValue: '',
},
videoSource: {
onChange: jest.fn(), onBlur: jest.fn(), local: '', formValue: '',
},
fallbackVideos: {
formValue: [], onChange: jest.fn(), onBlur: jest.fn(), local: [],
},
allowVideoDownloads: { local: false, onCheckedChange: jest.fn() },
});
videoIdChangeAlertSpy.mockReturnValue({
videoIdChangeAlert: { set: jest.fn(), show: false, dismiss: jest.fn() },
});
sourceHooksSpy.mockReturnValue({
updateVideoId: jest.fn(),
updateVideoURL: jest.fn(),
});
fallbackHooksSpy.mockReturnValue({
addFallbackVideo: jest.fn(),
deleteFallbackVideo: jest.fn(),
});
render(<VideoSourceWidget />);
expect(screen.getByText('Video source')).toBeInTheDocument();
expect(screen.getByLabelText('Video ID')).toBeInTheDocument();
expect(screen.getByLabelText('Video URL')).toBeInTheDocument();
expect(screen.getByText('Allow video downloads')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /add/i })).toBeInTheDocument();
});
it('calls updateVideoId on videoId field blur', () => {
const updateVideoId = jest.fn();
widgetValuesSpy.mockReturnValue({
videoId: {
onChange: jest.fn(), onBlur: jest.fn(), local: '', formValue: '',
},
videoSource: {
onChange: jest.fn(), onBlur: jest.fn(), local: '', formValue: '',
},
fallbackVideos: {
formValue: [], onChange: jest.fn(), onBlur: jest.fn(), local: [],
},
allowVideoDownloads: { local: false, onCheckedChange: jest.fn() },
});
videoIdChangeAlertSpy.mockReturnValue({
videoIdChangeAlert: { set: jest.fn(), show: false, dismiss: jest.fn() },
});
sourceHooksSpy.mockReturnValue({
updateVideoId,
updateVideoURL: jest.fn(),
});
fallbackHooksSpy.mockReturnValue({
addFallbackVideo: jest.fn(),
deleteFallbackVideo: jest.fn(),
});
render(<VideoSourceWidget />);
const videoIdInput = screen.getByLabelText('Video ID');
fireEvent.blur(videoIdInput);
expect(updateVideoId).toHaveBeenCalled();
});
it('calls updateVideoURL on videoSource field blur', () => {
const updateVideoURL = jest.fn();
widgetValuesSpy.mockReturnValue({
videoId: {
onChange: jest.fn(), onBlur: jest.fn(), local: '', formValue: '',
},
videoSource: {
onChange: jest.fn(), onBlur: jest.fn(), local: '', formValue: '',
},
fallbackVideos: {
formValue: [], onChange: jest.fn(), onBlur: jest.fn(), local: [],
},
allowVideoDownloads: { local: false, onCheckedChange: jest.fn() },
});
videoIdChangeAlertSpy.mockReturnValue({
videoIdChangeAlert: { set: jest.fn(), show: false, dismiss: jest.fn() },
});
sourceHooksSpy.mockReturnValue({
updateVideoId: jest.fn(),
updateVideoURL,
});
fallbackHooksSpy.mockReturnValue({
addFallbackVideo: jest.fn(),
deleteFallbackVideo: jest.fn(),
});
render(<VideoSourceWidget />);
const videoUrlInput = screen.getByLabelText('Video URL');
fireEvent.blur(videoUrlInput);
expect(updateVideoURL).toHaveBeenCalled();
});
it('renders fallback video fields and calls deleteFallbackVideo on delete', () => {
const deleteFallbackVideo = jest.fn();
widgetValuesSpy.mockReturnValue({
videoId: {
onChange: jest.fn(), onBlur: jest.fn(), local: '', formValue: '',
},
videoSource: {
onChange: jest.fn(), onBlur: jest.fn(), local: '', formValue: '',
},
fallbackVideos: {
formValue: ['url1', 'url2'],
onChange: () => jest.fn(),
onBlur: () => jest.fn(),
local: ['url1', 'url2'],
},
allowVideoDownloads: { local: false, onCheckedChange: jest.fn() },
});
videoIdChangeAlertSpy.mockReturnValue({
videoIdChangeAlert: { set: jest.fn(), show: false, dismiss: jest.fn() },
});
sourceHooksSpy.mockReturnValue({
updateVideoId: jest.fn(),
updateVideoURL: jest.fn(),
});
fallbackHooksSpy.mockReturnValue({
addFallbackVideo: jest.fn(),
deleteFallbackVideo,
});
render(<VideoSourceWidget />);
expect(screen.getAllByText('Video URL').length).toBe(3); // 1 main + 2 fallback
const deleteButtons = screen.getAllByRole('button', { name: /delete/i });
fireEvent.click(deleteButtons[0]);
expect(deleteFallbackVideo).toHaveBeenCalledWith('url1');
});
it('calls addFallbackVideo when add button is clicked', () => {
const addFallbackVideo = jest.fn();
widgetValuesSpy.mockReturnValue({
videoId: {
onChange: jest.fn(), onBlur: jest.fn(), local: '', formValue: '',
},
videoSource: {
onChange: jest.fn(), onBlur: jest.fn(), local: '', formValue: '',
},
fallbackVideos: {
formValue: [], onChange: jest.fn(), onBlur: jest.fn(), local: [],
},
allowVideoDownloads: { local: false, onCheckedChange: jest.fn() },
});
videoIdChangeAlertSpy.mockReturnValue({
videoIdChangeAlert: { set: jest.fn(), show: false, dismiss: jest.fn() },
});
sourceHooksSpy.mockReturnValue({
updateVideoId: jest.fn(),
updateVideoURL: jest.fn(),
});
fallbackHooksSpy.mockReturnValue({
addFallbackVideo,
deleteFallbackVideo: jest.fn(),
});
render(<VideoSourceWidget />);
const addButton = screen.getByRole('button', { name: /add/i });
fireEvent.click(addButton);
expect(addFallbackVideo).toHaveBeenCalled();
});
it('calls allowDownload.onCheckedChange when checkbox is clicked', () => {
const onCheckedChange = jest.fn();
widgetValuesSpy.mockReturnValue({
videoId: {
onChange: jest.fn(), onBlur: jest.fn(), local: '', formValue: '',
},
videoSource: {
onChange: jest.fn(), onBlur: jest.fn(), local: '', formValue: '',
},
fallbackVideos: {
formValue: [], onChange: jest.fn(), onBlur: jest.fn(), local: [],
},
allowVideoDownloads: { local: false, onCheckedChange },
});
videoIdChangeAlertSpy.mockReturnValue({
videoIdChangeAlert: { set: jest.fn(), show: false, dismiss: jest.fn() },
});
sourceHooksSpy.mockReturnValue({
updateVideoId: jest.fn(),
updateVideoURL: jest.fn(),
});
fallbackHooksSpy.mockReturnValue({
addFallbackVideo: jest.fn(),
deleteFallbackVideo: jest.fn(),
});
render(<VideoSourceWidget />);
const checkbox = screen.getByLabelText('Allow video downloads');
fireEvent.click(checkbox);
expect(onCheckedChange).toHaveBeenCalled();
});
it('shows error alert when videoIdChangeAlert.show is true', () => {
widgetValuesSpy.mockReturnValue({
videoId: {
onChange: jest.fn(), onBlur: jest.fn(), local: '', formValue: '',
},
videoSource: {
onChange: jest.fn(), onBlur: jest.fn(), local: '', formValue: '',
},
fallbackVideos: {
formValue: [], onChange: jest.fn(), onBlur: jest.fn(), local: [],
},
allowVideoDownloads: { local: false, onCheckedChange: jest.fn() },
});
videoIdChangeAlertSpy.mockReturnValue({
videoIdChangeAlert: { set: jest.fn(), show: true, dismiss: jest.fn() },
});
sourceHooksSpy.mockReturnValue({
updateVideoId: jest.fn(),
updateVideoURL: jest.fn(),
});
fallbackHooksSpy.mockReturnValue({
addFallbackVideo: jest.fn(),
deleteFallbackVideo: jest.fn(),
});
render(<VideoSourceWidget />);
expect(screen.getByRole('alert')).toBeInTheDocument();
});
});

View File

@@ -1,12 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AdditionalCourseContentPluginSlot renders 1`] = `
<PluginSlot
id="org.openedx.frontend.authoring.additional_course_content_plugin.v1"
idAliases={
[
"additional_course_content_plugin",
]
}
/>
`;

View File

@@ -1,15 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import { AdditionalCourseContentPluginSlot } from '.';
jest.mock('@openedx/frontend-plugin-framework', () => ({
PluginSlot: 'PluginSlot',
}));
describe('AdditionalCourseContentPluginSlot', () => {
beforeEach(() => jest.resetAllMocks());
it('renders', () => {
const wrapper = shallow(<AdditionalCourseContentPluginSlot />);
expect(wrapper.snapshot).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,17 @@
import { render, initializeMocks } from '@src/testUtils';
import { AdditionalCourseContentPluginSlot } from '.';
jest.mock('@openedx/frontend-plugin-framework', () => ({
PluginSlot: 'PluginSlot',
}));
describe('AdditionalCourseContentPluginSlot', () => {
beforeEach(() => initializeMocks());
it('renders', () => {
const expectedId = 'org.openedx.frontend.authoring.additional_course_content_plugin.v1';
const { container } = render(<AdditionalCourseContentPluginSlot />);
expect(container.querySelector('pluginslot')).toBeInTheDocument();
expect(container.querySelector('pluginslot')).toHaveProperty('id', expectedId);
});
});