test: replacing snapshot tests with RTL tests part 5 (#2143)

* test: replacing snapshot tests with rtl tests part 5

* test: removig extra tests

* test: snaps update

* test: adding import shorthand and turning tests into ts

* docs: clarify which line the comment is about

---------

Co-authored-by: Braden MacDonald <braden@opencraft.com>
This commit is contained in:
jacobo-dominguez-wgu
2025-06-16 10:56:59 -06:00
committed by GitHub
parent 96df339be5
commit cba4e684ab
25 changed files with 406 additions and 940 deletions

View File

@@ -11,6 +11,9 @@ module.exports = createConfig('jest', {
],
moduleNameMapper: {
'^lodash-es$': 'lodash',
// This alias is for any code in the src directory that wants to avoid '../../' style relative imports:
'^@src/(.*)$': '<rootDir>/src/$1',
// This alias is used for plugins in the plugins/ folder only.
'^CourseAuthoring/(.*)$': '<rootDir>/src/$1',
},
modulePathIgnorePatterns: [

View File

@@ -1,46 +0,0 @@
// @ts-check
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import EditorContainer from './EditorContainer';
import { mockWaffleFlags } from '../data/apiHooks.mock';
mockWaffleFlags();
const mockPathname = '/editor/';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), // use actual for all non-hook parts
useParams: () => ({
blockId: 'company-id1',
blockType: 'html',
}),
useLocation: () => ({
pathname: mockPathname,
}),
useSearchParams: () => [{
get: () => 'lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd',
}],
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: () => ({
formatMessage: (message) => message.defaultMessage,
}),
}));
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: () => ({
useReactMarkdownEditor: true, // or false depending on the test
}),
}));
const props = { learningContextId: 'cOuRsEId' };
describe('Editor Container', () => {
describe('snapshots', () => {
test('rendering correctly with expected Input', () => {
expect(shallow(<EditorContainer {...props} />).snapshot).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,94 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import {
render, screen, initializeMocks, fireEvent, act,
} from '@src/testUtils';
import EditorContainer from './EditorContainer';
import { mockWaffleFlags } from '../data/apiHooks.mock';
import editorCmsApi from './data/services/cms/api';
mockWaffleFlags();
const mockPathname = '/editor/';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: () => ({
blockId: 'block-v1:Org+TS100+24+type@fake+block@123456fake',
blockType: 'fake',
}),
useLocation: () => ({
pathname: mockPathname,
}),
useSearchParams: () => [{
get: () => 'lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd',
}],
}));
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: () => ({
useReactMarkdownEditor: true, // or false depending on the test
}),
}));
// Mock this plugins component:
jest.mock('frontend-components-tinymce-advanced-plugins', () => ({ a11ycheckerCss: '' }));
// Always mock out the "fetch course images" endpoint:
jest.spyOn(editorCmsApi, 'fetchCourseImages').mockImplementation(async () => ( // eslint-disable-next-line
{ data: { assets: [], start: 0, end: 0, page: 0, pageSize: 50, totalCount: 0 } }
));
// Mock out the 'get ancestors' API:
jest.spyOn(editorCmsApi, 'fetchByUnitId').mockImplementation(async () => ({
status: 200,
data: {
ancestors: [{
id: 'block-v1:Org+TS100+24+type@vertical+block@parent',
display_name: 'You-Knit? The Test Unit',
category: 'vertical',
has_children: true,
}],
},
}));
jest.mock('../library-authoring/LibraryBlock', () => ({
LibraryBlock: jest.fn(() => (<div>Advanced Editor Iframe</div>)),
}));
const props = { learningContextId: 'cOuRsEId' };
describe('EditorContainer', () => {
beforeEach(() => {
initializeMocks();
jest.spyOn(editorCmsApi, 'fetchBlockById').mockImplementationOnce(async () => (
{
status: 200,
data: {
display_name: 'Fake Un-editable Block', category: 'fake', metadata: {}, data: '',
},
}
));
});
test('render component', () => {
render(<EditorContainer {...props} />);
expect(screen.getByText('View in Library')).toBeInTheDocument();
expect(screen.getByText('Advanced Editor Iframe')).toBeInTheDocument();
});
test('should call onClose param when receiving "cancel-clicked" message', () => {
const onCloseMock = jest.fn();
render(<EditorContainer {...props} onClose={onCloseMock} />);
const messageEvent = new MessageEvent('message', {
data: {
type: 'xblock-event',
eventName: 'cancel',
},
origin: getConfig().STUDIO_BASE_URL,
});
act(() => {
window.dispatchEvent(messageEvent);
});
fireEvent.click(screen.getByRole('button', { name: 'Discard Changes and Exit' }));
expect(onCloseMock).toHaveBeenCalled();
});
});

View File

@@ -1,69 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Editor Container snapshots rendering correctly with expected Input 1`] = `
<div
className="editor-page"
>
<AlertMessage
actions={
[
<ForwardRef
as={
{
"$$typeof": Symbol(react.forward_ref),
"defaultProps": {
"as": "a",
"className": undefined,
"destination": undefined,
"externalLinkAlternativeText": undefined,
"externalLinkTitle": undefined,
"isInline": false,
"onClick": undefined,
"showLaunchIcon": true,
"target": "_self",
"variant": "default",
},
"propTypes": {
"as": [Function],
"children": [Function],
"className": [Function],
"destination": [Function],
"externalLinkAlternativeText": [Function],
"externalLinkTitle": [Function],
"isInline": [Function],
"onClick": [Function],
"showLaunchIcon": [Function],
"target": [Function],
"variant": [Function],
},
"render": [Function],
}
}
destination="/library/lib:Axim:TEST/components?usageKey=lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd"
disabled={false}
rel="noopener noreferrer"
showLaunchIcon={true}
target="_blank"
>
View in Library
</ForwardRef>,
]
}
className="m-3"
description="Edits made here will only be reflected in this course. These edits may be overridden later if updates are accepted."
icon={[Function]}
show={true}
title="Editing Content from a Library"
variant="warning"
/>
<EditorPage
blockId="company-id1"
blockType="html"
courseId="cOuRsEId"
lmsEndpointUrl="http://localhost:18000"
onClose={null}
returnFunction={null}
studioEndpointUrl="http://localhost:18010"
/>
</div>
`;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Collapsible, Icon, IconButton } from '@openedx/paragon';
import { ExpandLess, ExpandMore, InfoOutline } from '@openedx/paragon/icons';
@@ -20,9 +20,9 @@ const CollapsibleFormWidget = ({
subtitle,
title,
fontSize,
// injected
intl,
}) => (
}) => {
const intl = useIntl();
return (
<Collapsible.Advanced
className="collapsible-card rounded mx-4 my-3 px-4 text-primary-500"
defaultOpen
@@ -53,7 +53,8 @@ const CollapsibleFormWidget = ({
{children}
</Collapsible.Body>
</Collapsible.Advanced>
);
);
};
CollapsibleFormWidget.defaultProps = {
subtitle: null,
@@ -66,9 +67,6 @@ CollapsibleFormWidget.propTypes = {
subtitle: PropTypes.node,
title: PropTypes.node.isRequired,
fontSize: PropTypes.string,
// injected
intl: intlShape.isRequired,
};
export const CollapsibleFormWidgetInternal = CollapsibleFormWidget; // For testing only
export default injectIntl(CollapsibleFormWidget);
export default CollapsibleFormWidget;

View File

@@ -1,29 +0,0 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from '../../../../../testUtils';
import { CollapsibleFormWidgetInternal as CollapsibleFormWidget } from './CollapsibleFormWidget';
describe('CollapsibleFormWidget', () => {
const props = {
isError: false,
subtitle: 'SuBTItle',
title: 'tiTLE',
// inject
intl: { formatMessage },
};
describe('render', () => {
const testContent = (<p>Some test string</p>);
test('snapshots: renders as expected with default props', () => {
expect(
shallow(<CollapsibleFormWidget {...props}>{testContent}</CollapsibleFormWidget>).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders with open={true} when there is error', () => {
expect(
shallow(<CollapsibleFormWidget {...props} isError>{testContent}</CollapsibleFormWidget>).snapshot,
).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,25 @@
import React from 'react';
import {
render, screen, initializeMocks,
} from '@src/testUtils';
import CollapsibleFormWidget from './CollapsibleFormWidget';
describe('CollapsibleFormWidget', () => {
const props = {
isError: false,
subtitle: 'Sample subtitle',
title: 'Sample title',
fontSize: 'x-small',
};
const testContent = (<p>Some test string</p>);
beforeEach(() => {
initializeMocks();
});
test('renders component', () => {
render(<CollapsibleFormWidget {...props}>{testContent}</CollapsibleFormWidget>);
expect(screen.getByText('Sample title')).toBeInTheDocument();
expect(screen.getByText('Some test string')).toBeInTheDocument();
});
});

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DurationWidget render snapshots: renders as expected with default props 1`] = `
<injectIntl(ShimmedIntlComponent)
<CollapsibleFormWidget
fontSize="x-small"
subtitle="Custom: 00:00:00"
title="Duration"
@@ -60,5 +60,5 @@ exports[`DurationWidget render snapshots: renders as expected with default props
Total: 00:00:00
</span>
</div>
</injectIntl(ShimmedIntlComponent)>
</CollapsibleFormWidget>
`;

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HandoutWidget snapshots snapshots: renders as expected with default props 1`] = `
<injectIntl(ShimmedIntlComponent)
<CollapsibleFormWidget
fontSize="x-small"
isError={true}
subtitle="None"
@@ -60,11 +60,11 @@ exports[`HandoutWidget snapshots snapshots: renders as expected with default pro
/>
</Button>
</Stack>
</injectIntl(ShimmedIntlComponent)>
</CollapsibleFormWidget>
`;
exports[`HandoutWidget snapshots snapshots: renders as expected with handout 1`] = `
<injectIntl(ShimmedIntlComponent)
<CollapsibleFormWidget
fontSize="x-small"
isError={true}
subtitle="sOMeUrl "
@@ -159,7 +159,7 @@ exports[`HandoutWidget snapshots snapshots: renders as expected with handout 1`]
id="authoring.videoeditor.handout.handoutHelpMessage"
/>
</Stack>
</injectIntl(ShimmedIntlComponent)>
</CollapsibleFormWidget>
`;
exports[`HandoutWidget snapshots snapshots: renders as expected with isLibrary true 1`] = `null`;

View File

@@ -3,8 +3,7 @@ import { connect, useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import {
FormattedMessage,
injectIntl,
intlShape,
useIntl,
} from '@edx/frontend-platform/i18n';
import {
ActionRow,
@@ -22,12 +21,11 @@ import { LicenseLevel, LicenseNames, LicenseTypes } from '../../../../../../data
const LicenseSelector = ({
license,
level,
// injected
intl,
// redux
courseLicenseType,
updateField,
}) => {
const intl = useIntl();
const { levelDescription } = hooks.determineText({ level });
const onLicenseChange = hooks.onSelectLicense({ dispatch: useDispatch() });
const ref = React.useRef();
@@ -74,8 +72,6 @@ const LicenseSelector = ({
LicenseSelector.propTypes = {
license: PropTypes.string.isRequired,
level: PropTypes.string.isRequired,
// injected
intl: intlShape.isRequired,
// redux
courseLicenseType: PropTypes.string.isRequired,
updateField: PropTypes.func.isRequired,
@@ -90,4 +86,4 @@ export const mapDispatchToProps = (dispatch) => ({
});
export const LicenseSelectorInternal = LicenseSelector; // For testing only
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(LicenseSelector));
export default connect(mapStateToProps, mapDispatchToProps)(LicenseSelector);

View File

@@ -1,84 +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 { LicenseSelectorInternal as LicenseSelector, mapStateToProps, mapDispatchToProps } from './LicenseSelector';
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('react-redux', () => {
const dispatchFn = jest.fn();
return {
...jest.requireActual('react-redux'),
dispatch: dispatchFn,
useDispatch: jest.fn(() => dispatchFn),
};
});
jest.mock('../../../../../../data/redux', () => ({
actions: {
video: {
updateField: jest.fn().mockName('actions.video.updateField'),
},
},
selectors: {
video: {
courseLicenseType: jest.fn(state => ({ courseLicenseType: state })),
},
},
}));
describe('LicenseSelector', () => {
const props = {
intl: { formatMessage },
license: 'all-rights-reserved',
level: 'course',
courseLicenseType: 'all-rights-reserved',
updateField: jest.fn().mockName('args.updateField'),
};
describe('snapshots', () => {
test('snapshots: renders as expected with default props', () => {
expect(
shallow(<LicenseSelector {...props} />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with library level', () => {
expect(
shallow(<LicenseSelector {...props} level="library" />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with block level', () => {
expect(
shallow(<LicenseSelector {...props} level="block" />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with no license', () => {
expect(
shallow(<LicenseSelector {...props} license="" />).snapshot,
).toMatchSnapshot();
});
});
describe('mapStateToProps', () => {
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
test('courseLicenseType from video.courseLicenseType', () => {
expect(
mapStateToProps(testState).courseLicenseType,
).toEqual(selectors.video.courseLicenseType(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,86 @@
import React from 'react';
import {
render, fireEvent, screen, initializeMocks,
} from '@src/testUtils';
import { LicenseSelectorInternal } from './LicenseSelector';
import * as hooks from './hooks';
jest.mock('./hooks', () => ({
determineText: jest.fn(() => ({ levelDescription: 'Test level description' })),
onSelectLicense: jest.fn(() => jest.fn()),
}));
const LicenseTypes = {
select: 'select',
allRightsReserved: 'All Rights Reserved',
creativeCommons: 'Creative Commons',
};
const LicenseLevel = { course: 'course', video: 'video' };
describe('LicenseSelectorInternal', () => {
const updateField = jest.fn();
const onLicenseChange = jest.fn();
const props = {
license: LicenseTypes.select,
level: LicenseLevel.video,
courseLicenseType: LicenseTypes.select,
updateField,
};
beforeEach(() => {
initializeMocks();
(hooks.onSelectLicense as jest.Mock).mockReturnValue(onLicenseChange);
(hooks.determineText as jest.Mock).mockReturnValue({ levelDescription: 'Test level description' });
});
it('renders select with correct options and default value', () => {
render(<LicenseSelectorInternal {...props} />);
const select = screen.getByRole('combobox');
expect(select).toBeInTheDocument();
expect((select as HTMLSelectElement).value).toBe(props.license);
expect(screen.getByText(LicenseTypes.allRightsReserved)).toBeInTheDocument();
expect(screen.getByText(LicenseTypes.creativeCommons)).toBeInTheDocument();
});
it('disables select when level is course', () => {
render(<LicenseSelectorInternal {...props} level={LicenseLevel.course} />);
expect(screen.getByRole('combobox')).toBeDisabled();
});
it('shows delete button when level is not course', () => {
render(<LicenseSelectorInternal {...props} />);
expect(screen.getByRole('button')).toBeInTheDocument();
});
it('does not show delete button when level is course', () => {
render(<LicenseSelectorInternal {...props} level={LicenseLevel.course} />);
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
it('calls onLicenseChange when select changes', () => {
render(<LicenseSelectorInternal {...props} />);
fireEvent.change(screen.getByRole('combobox'), { target: { value: LicenseTypes.select } });
expect(onLicenseChange).toHaveBeenCalledWith(LicenseTypes.select);
});
it('calls updateField and resets select when delete button clicked', () => {
render(<LicenseSelectorInternal {...props} />);
fireEvent.click(screen.getByRole('button'));
expect(updateField).toHaveBeenCalledWith({ licenseType: '', licenseDetails: {} });
});
it('renders level description', () => {
render(<LicenseSelectorInternal {...props} />);
expect(screen.getByText('Test level description')).toBeInTheDocument();
});
it('renders border when license is not select', () => {
const { container } = render(<LicenseSelectorInternal {...props} license={LicenseTypes.allRightsReserved} />);
expect(container.querySelector('.border-primary-100')).toBeInTheDocument();
});
it('does not render border when license is select', () => {
const { container } = render(<LicenseSelectorInternal {...props} license={LicenseTypes.select} />);
expect(container.querySelector('.border-primary-100')).not.toBeInTheDocument();
});
});

View File

@@ -1,203 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LicenseSelector snapshots snapshots: renders as expected with block level 1`] = `
<Fragment>
<ActionRow>
<Form.Control
as="select"
className="w-100 m-0 p-0"
defaultValue="all-rights-reserved"
disabled={false}
floatingLabel="License Type"
onChange={[Function]}
>
<option
hidden={true}
>
Select
</option>
<option
value="all-rights-reserved"
>
All Rights Reserved
</option>
<option
value="creative-commons"
>
Creative Commons
</option>
</Form.Control>
<Fragment>
<ActionRow.Spacer />
<IconButtonWithTooltip
iconAs="Icon"
onClick={[Function]}
tooltipContent={
<FormattedMessage
defaultMessage="Clear and apply the course-level license"
description="Message presented to user for action to delete license selection"
id="authoring.videoeditor.license.deleteLicenseSelection"
/>
}
tooltipPlacement="top"
/>
</Fragment>
</ActionRow>
<div
className="x-small mt-3"
>
<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
className="border-primary-100 mt-3 border-bottom"
/>
</Fragment>
`;
exports[`LicenseSelector snapshots snapshots: renders as expected with default props 1`] = `
<Fragment>
<ActionRow>
<Form.Control
as="select"
className="w-100 m-0 p-0"
defaultValue="all-rights-reserved"
disabled={true}
floatingLabel="License Type"
onChange={[Function]}
>
<option
hidden={true}
>
Select
</option>
<option
value="all-rights-reserved"
>
All Rights Reserved
</option>
<option
value="creative-commons"
>
Creative Commons
</option>
</Form.Control>
</ActionRow>
<div
className="x-small mt-3"
>
<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
className="border-primary-100 mt-3 border-bottom"
/>
</Fragment>
`;
exports[`LicenseSelector snapshots snapshots: renders as expected with library level 1`] = `
<Fragment>
<ActionRow>
<Form.Control
as="select"
className="w-100 m-0 p-0"
defaultValue="all-rights-reserved"
disabled={false}
floatingLabel="License Type"
onChange={[Function]}
>
<option
hidden={true}
>
Select
</option>
<option
value="all-rights-reserved"
>
All Rights Reserved
</option>
<option
value="creative-commons"
>
Creative Commons
</option>
</Form.Control>
<Fragment>
<ActionRow.Spacer />
<IconButtonWithTooltip
iconAs="Icon"
onClick={[Function]}
tooltipContent={
<FormattedMessage
defaultMessage="Clear and apply the course-level license"
description="Message presented to user for action to delete license selection"
id="authoring.videoeditor.license.deleteLicenseSelection"
/>
}
tooltipPlacement="top"
/>
</Fragment>
</ActionRow>
<div
className="x-small mt-3"
>
<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
className="border-primary-100 mt-3 border-bottom"
/>
</Fragment>
`;
exports[`LicenseSelector snapshots snapshots: renders as expected with no license 1`] = `
<Fragment>
<ActionRow>
<Form.Control
as="select"
className="w-100 m-0 p-0"
defaultValue=""
disabled={true}
floatingLabel="License Type"
onChange={[Function]}
>
<option
hidden={true}
>
Select
</option>
<option
value="all-rights-reserved"
>
All Rights Reserved
</option>
<option
value="creative-commons"
>
Creative Commons
</option>
</Form.Control>
</ActionRow>
<div
className="x-small mt-3"
>
<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
className="border-primary-100 mt-3 border-bottom"
/>
</Fragment>
`;

View File

@@ -1,7 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LicenseWidget snapshots snapshots: renders as expected with default props 1`] = `
<injectIntl(ShimmedIntlComponent)
<CollapsibleFormWidget
fontSize=""
subtitle={
<div>
<injectIntl(ShimmedIntlComponent)
@@ -25,7 +26,7 @@ exports[`LicenseWidget snapshots snapshots: renders as expected with default pro
gap={4}
>
<Fragment>
<injectIntl(ShimmedIntlComponent)
<[object Object]
level="course"
license="all-rights-reserved"
/>
@@ -64,11 +65,12 @@ exports[`LicenseWidget snapshots snapshots: renders as expected with default pro
</Button>
</Fragment>
</Stack>
</injectIntl(ShimmedIntlComponent)>
</CollapsibleFormWidget>
`;
exports[`LicenseWidget snapshots snapshots: renders as expected with isLibrary true 1`] = `
<injectIntl(ShimmedIntlComponent)
<CollapsibleFormWidget
fontSize=""
subtitle={
<div>
<injectIntl(ShimmedIntlComponent)
@@ -92,7 +94,7 @@ exports[`LicenseWidget snapshots snapshots: renders as expected with isLibrary t
gap={4}
>
<Fragment>
<injectIntl(ShimmedIntlComponent)
<[object Object]
level="library"
license="all-rights-reserved"
/>
@@ -114,11 +116,12 @@ exports[`LicenseWidget snapshots snapshots: renders as expected with isLibrary t
/>
</Fragment>
</Stack>
</injectIntl(ShimmedIntlComponent)>
</CollapsibleFormWidget>
`;
exports[`LicenseWidget snapshots snapshots: renders as expected with licenseType defined 1`] = `
<injectIntl(ShimmedIntlComponent)
<CollapsibleFormWidget
fontSize=""
subtitle={
<div>
<injectIntl(ShimmedIntlComponent)
@@ -142,7 +145,7 @@ exports[`LicenseWidget snapshots snapshots: renders as expected with licenseType
gap={4}
>
<Fragment>
<injectIntl(ShimmedIntlComponent)
<[object Object]
level="block"
license="all-rights-reserved"
/>
@@ -164,5 +167,5 @@ exports[`LicenseWidget snapshots snapshots: renders as expected with licenseType
/>
</Fragment>
</Stack>
</injectIntl(ShimmedIntlComponent)>
</CollapsibleFormWidget>
`;

View File

@@ -7,7 +7,7 @@ exports[`SocialShareWidget rendered with videoSharingEnabled false with videoSha
exports[`SocialShareWidget rendered with videoSharingEnabled false with videoSharingEnabledForCourse and isLibrary false and videoSharingEnabledForAll true should return null 1`] = `null`;
exports[`SocialShareWidget rendered with videoSharingEnabled true and allowVideoSharing value equals false should have subtitle with text that reads Enabled 1`] = `
<injectIntl(ShimmedIntlComponent)
<CollapsibleFormWidget
fontSize="x-small"
subtitle="Disabled"
title="Social Sharing"
@@ -42,11 +42,11 @@ exports[`SocialShareWidget rendered with videoSharingEnabled true and allowVideo
Learn more about social sharing
</Hyperlink>
</div>
</injectIntl(ShimmedIntlComponent)>
</CollapsibleFormWidget>
`;
exports[`SocialShareWidget rendered with videoSharingEnabled true and allowVideoSharing value equals true should have subtitle with text that reads Enabled 1`] = `
<injectIntl(ShimmedIntlComponent)
<CollapsibleFormWidget
fontSize="x-small"
subtitle="Enabled"
title="Social Sharing"
@@ -81,5 +81,5 @@ exports[`SocialShareWidget rendered with videoSharingEnabled true and allowVideo
Learn more about social sharing
</Hyperlink>
</div>
</injectIntl(ShimmedIntlComponent)>
</CollapsibleFormWidget>
`;

View File

@@ -3,7 +3,7 @@
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`] = `
<injectIntl(ShimmedIntlComponent)
<CollapsibleFormWidget
fontSize="x-small"
isError={true}
subtitle="Yes"
@@ -38,11 +38,11 @@ exports[`ThumbnailWidget snapshots snapshots: renders as expected where videoId
tooltipPlacement="top"
/>
</Stack>
</injectIntl(ShimmedIntlComponent)>
</CollapsibleFormWidget>
`;
exports[`ThumbnailWidget snapshots snapshots: renders as expected where videoId is valid and no thumbnail 1`] = `
<injectIntl(ShimmedIntlComponent)
<CollapsibleFormWidget
fontSize="x-small"
isError={true}
subtitle="None"
@@ -106,11 +106,11 @@ exports[`ThumbnailWidget snapshots snapshots: renders as expected where videoId
/>
</Button>
</Stack>
</injectIntl(ShimmedIntlComponent)>
</CollapsibleFormWidget>
`;
exports[`ThumbnailWidget snapshots snapshots: renders as expected with a thumbnail provided 1`] = `
<injectIntl(ShimmedIntlComponent)
<CollapsibleFormWidget
fontSize="x-small"
isError={true}
subtitle="Yes"
@@ -148,7 +148,7 @@ exports[`ThumbnailWidget snapshots snapshots: renders as expected with a thumbna
thumbnail={true}
/>
</Stack>
</injectIntl(ShimmedIntlComponent)>
</CollapsibleFormWidget>
`;
exports[`ThumbnailWidget snapshots snapshots: renders as expected with default props 1`] = `null`;

View File

@@ -1,58 +0,0 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Button, IconButton } from '@openedx/paragon';
import { thunkActions } from '../../../../../../data/redux';
import { ImportTranscriptCardInternal as ImportTranscriptCard, mapDispatchToProps, mapStateToProps } from './ImportTranscriptCard';
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(() => ({ transcripts: ['error.transcripts', jest.fn().mockName('error.setTranscripts')] })),
}));
jest.mock('../../../../../../data/redux', () => ({
thunkActions: {
video: {
importTranscript: jest.fn().mockName('thunkActions.video.importTranscript'),
},
},
}));
describe('ImportTranscriptCard', () => {
const props = {
setOpen: jest.fn().mockName('setOpen'),
importTranscript: jest.fn().mockName('args.importTranscript'),
};
let el;
describe('snapshots', () => {
test('snapshots: renders as expected with default props', () => {
expect(
shallow(<ImportTranscriptCard {...props} />).snapshot,
).toMatchSnapshot();
});
});
describe('behavior inspection', () => {
beforeEach(() => {
el = shallow(<ImportTranscriptCard {...props} />);
});
test('close behavior is linked to IconButton', () => {
expect(el.instance.findByType(IconButton)[0]
.props.onClick).toBeDefined();
});
test('import behavior is linked to Button onClick', () => {
expect(el.instance.findByType(Button)[0]
.props.onClick).toEqual(props.importTranscript);
});
});
describe('mapStateToProps', () => {
it('returns an empty object', () => {
expect(mapStateToProps()).toEqual({});
});
});
describe('mapDispatchToProps', () => {
test('updateField from thunkActions.video.importTranscript', () => {
expect(mapDispatchToProps.importTranscript).toEqual(thunkActions.video.importTranscript);
});
});
});

View File

@@ -0,0 +1,50 @@
import React from 'react';
import {
render, screen, fireEvent, initializeMocks,
} from '@src/testUtils';
import { ImportTranscriptCardInternal as ImportTranscriptCard } from './ImportTranscriptCard';
jest.mock('../../../../../../data/redux', () => ({
thunkActions: {
video: {
importTranscript: jest.fn().mockName('thunkActions.video.importTranscript'),
},
},
}));
describe('ImportTranscriptCard (RTL)', () => {
const mockSetOpen = jest.fn();
const mockImportTranscript = jest.fn();
beforeEach(() => {
initializeMocks();
});
it('renders header, message, and button', () => {
render(
<ImportTranscriptCard setOpen={mockSetOpen} importTranscript={mockImportTranscript} />,
);
expect(screen.getByText('Import transcript from YouTube?')).toBeInTheDocument();
expect(screen.getByText('We found transcript for this video on YouTube. Would you like to import it now?')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Import Transcript' })).toBeInTheDocument();
});
it('calls setOpen(false) when close IconButton is clicked', () => {
const { container } = render(
<ImportTranscriptCard setOpen={mockSetOpen} importTranscript={mockImportTranscript} />,
);
const closeButton = container.querySelector('.btn-icon-primary');
expect(closeButton).toBeInTheDocument();
fireEvent.click(closeButton!);
expect(mockSetOpen).toHaveBeenCalledWith(false);
});
it('calls importTranscript when import button is clicked', () => {
render(
<ImportTranscriptCard setOpen={mockSetOpen} importTranscript={mockImportTranscript} />,
);
const importBtn = screen.getByRole('button', { name: 'Import Transcript' });
fireEvent.click(importBtn);
expect(mockImportTranscript).toHaveBeenCalled();
});
});

View File

@@ -1,98 +1,82 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import {
render, fireEvent, screen, initializeMocks,
} from 'CourseAuthoring/testUtils';
import { TranscriptInternal, hooks } from './Transcript';
import * as module from './Transcript';
jest.mock('./TranscriptActionMenu', () => jest.fn(() => <div>TranscriptActionMenu</div>));
jest.mock('./LanguageSelector', () => jest.fn(() => <div>LanguageSelector</div>));
import { MockUseState } from '../../../../../../testUtils';
const defaultProps = {
index: 0,
language: '',
transcriptUrl: undefined,
deleteTranscript: jest.fn(),
};
const Transcript = module.TranscriptInternal;
jest.mock('./LanguageSelector', () => 'LanguageSelector');
jest.mock('./TranscriptActionMenu', () => 'TranscriptActionMenu');
describe('Transcript Component', () => {
describe('state hooks', () => {
const state = new MockUseState(module.hooks);
describe('TranscriptInternal', () => {
const cancelDelete = jest.fn();
const deleteTranscript = jest.fn();
jest.spyOn(hooks, 'setUpDeleteConfirmation').mockReturnValue({
inDeleteConfirmation: false,
launchDeleteConfirmation: deleteTranscript,
cancelDelete,
});
beforeEach(() => {
jest.clearAllMocks();
});
describe('state hooks', () => {
state.testGetter(state.keys.inDeleteConfirmation);
initializeMocks();
});
describe('setUpDeleteConfirmation hook', () => {
beforeEach(() => {
state.mock();
});
afterEach(() => {
state.restore();
});
test('inDeleteConfirmation: state values', () => {
expect(module.hooks.setUpDeleteConfirmation().inDeleteConfirmation).toEqual(false);
});
test('inDeleteConfirmation setters: launch', () => {
module.hooks.setUpDeleteConfirmation().launchDeleteConfirmation();
expect(state.setState[state.keys.inDeleteConfirmation]).toHaveBeenCalledWith(true);
});
test('inDeleteConfirmation setters: cancel', () => {
module.hooks.setUpDeleteConfirmation().cancelDelete();
expect(state.setState[state.keys.inDeleteConfirmation]).toHaveBeenCalledWith(false);
});
});
it('renders ActionRow and LanguageSelector when not in delete confirmation', () => {
render(<TranscriptInternal {...defaultProps} />);
expect(screen.getByText('LanguageSelector')).toBeInTheDocument();
expect(screen.getByRole('button')).toBeInTheDocument();
});
describe('component', () => {
describe('component', () => {
const props = {
index: 'sOmenUmBer',
language: 'lAnG',
deleteTranscript: jest.fn().mockName('thunkActions.video.deleteTranscript'),
};
afterAll(() => {
jest.clearAllMocks();
it('renders TranscriptActionMenu when language is not empty', () => {
const props = { language: 'en', transcriptUrl: 'url' };
render(<TranscriptInternal {...defaultProps} {...props} />);
expect(screen.getByText('TranscriptActionMenu')).toBeInTheDocument();
});
test('snapshots: renders as expected with default props: dont show confirm delete', () => {
jest.spyOn(module.hooks, 'setUpDeleteConfirmation').mockImplementationOnce(() => ({
inDeleteConfirmation: false,
launchDeleteConfirmation: jest.fn().mockName('launchDeleteConfirmation'),
cancelDelete: jest.fn().mockName('cancelDelete'),
}));
expect(
shallow(<Transcript {...props} />).snapshot,
).toMatchSnapshot();
it('calls launchDeleteConfirmation when IconButton is clicked', () => {
render(<TranscriptInternal {...defaultProps} />);
fireEvent.click(screen.getByRole('button'));
expect(deleteTranscript).toHaveBeenCalled();
});
test('snapshots: renders as expected with default props: dont show confirm delete, language is blank so delete is shown instead of action menu', () => {
jest.spyOn(module.hooks, 'setUpDeleteConfirmation').mockImplementationOnce(() => ({
inDeleteConfirmation: false,
launchDeleteConfirmation: jest.fn().mockName('launchDeleteConfirmation'),
cancelDelete: jest.fn().mockName('cancelDelete'),
}));
expect(
shallow(<Transcript {...props} language="" />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with default props: show confirm delete', () => {
jest.spyOn(module.hooks, 'setUpDeleteConfirmation').mockImplementationOnce(() => ({
it('renders delete confirmation card when inDeleteConfirmation is true', () => {
jest.spyOn(hooks, 'setUpDeleteConfirmation').mockReturnValue({
inDeleteConfirmation: true,
launchDeleteConfirmation: jest.fn().mockName('launchDeleteConfirmation'),
cancelDelete: jest.fn().mockName('cancelDelete'),
}));
expect(
shallow(<Transcript {...props} />).snapshot,
).toMatchSnapshot();
launchDeleteConfirmation: jest.fn(),
cancelDelete,
});
test('snapshots: renders as expected with transcriptUrl', () => {
jest.spyOn(module.hooks, 'setUpDeleteConfirmation').mockImplementationOnce(() => ({
inDeleteConfirmation: false,
launchDeleteConfirmation: jest.fn().mockName('launchDeleteConfirmation'),
cancelDelete: jest.fn().mockName('cancelDelete'),
}));
expect(
shallow(<Transcript {...props} transcriptUrl="url" />).snapshot,
).toMatchSnapshot();
render(<TranscriptInternal {...defaultProps} />);
expect(screen.getByText('Delete this transcript?')).toBeInTheDocument();
expect(screen.getByText('Are you sure you want to delete this transcript?')).toBeInTheDocument();
});
it('calls cancelDelete when cancel button is clicked', () => {
jest.spyOn(hooks, 'setUpDeleteConfirmation').mockReturnValue({
inDeleteConfirmation: true,
launchDeleteConfirmation: jest.fn(),
cancelDelete,
});
render(<TranscriptInternal {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(cancelDelete).toHaveBeenCalled();
});
it('calls deleteTranscript and cancelDelete when confirm delete is clicked', () => {
jest.spyOn(hooks, 'setUpDeleteConfirmation').mockReturnValue({
inDeleteConfirmation: true,
launchDeleteConfirmation: jest.fn(),
cancelDelete,
});
const props = { language: 'es', deleteTranscript };
render(<TranscriptInternal {...defaultProps} {...props} />);
fireEvent.click(screen.getByRole('button', { name: 'Delete' }));
expect(deleteTranscript).toHaveBeenCalledWith({ language: 'es' });
expect(cancelDelete).toHaveBeenCalled();
});
});

View File

@@ -1,40 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ImportTranscriptCard snapshots snapshots: renders as expected with default props 1`] = `
<Stack
className="border rounded border-primary-200 p-4"
gap={3}
>
<ActionRow
className="h5"
>
<FormattedMessage
defaultMessage="Import transcript from YouTube?"
description="Header for import transcript card"
id="authoring.videoEditor.transcripts.importCard.header"
/>
<ActionRow.Spacer />
<IconButton
iconAs="Icon"
onClick={[Function]}
src={[MockFunction icons.Close]}
/>
</ActionRow>
<FormattedMessage
defaultMessage="We found transcript for this video on YouTube. Would you like to import it now?"
description="Message for import transcript card asking user if they want to import transcript"
id="authoring.videoEditor.transcrtipts.importCard.message"
/>
<Button
onClick={[MockFunction args.importTranscript]}
size="sm"
variant="outline-primary"
>
<FormattedMessage
defaultMessage="Import Transcript"
description="Label for youTube import transcript button"
id="authoring.videoEditor.transcripts.importButton.label"
/>
</Button>
</Stack>
`;

View File

@@ -1,103 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Transcript Component component component snapshots: renders as expected with default props: dont show confirm delete 1`] = `
<Fragment>
<ActionRow>
<LanguageSelector
language="lAnG"
title="sOmenUmBer"
/>
<ActionRow.Spacer />
<TranscriptActionMenu
index="sOmenUmBer"
language="lAnG"
launchDeleteConfirmation={[MockFunction launchDeleteConfirmation]}
/>
</ActionRow>
</Fragment>
`;
exports[`Transcript Component component component snapshots: renders as expected with default props: dont show confirm delete, language is blank so delete is shown instead of action menu 1`] = `
<Fragment>
<ActionRow>
<LanguageSelector
language=""
title="sOmenUmBer"
/>
<ActionRow.Spacer />
<IconButton
iconAs="Icon"
onClick={[Function]}
/>
</ActionRow>
</Fragment>
`;
exports[`Transcript Component component component snapshots: renders as expected with default props: show confirm delete 1`] = `
<Fragment>
<Card
className="mb-2"
>
<Card.Header
title={
<FormattedMessage
defaultMessage="Delete this transcript?"
description="Title for Warning which allows users to select next step in the process of deleting a transcript"
id="authoring.videoeditor.transcripts.deleteConfirmationTitle"
/>
}
/>
<Card.Body>
<Card.Section>
<FormattedMessage
defaultMessage="Are you sure you want to delete this transcript?"
description="Warning which allows users to select next step in the process of deleting a transcript"
id="authoring.videoeditor.transcripts.deleteConfirmationMessage"
/>
</Card.Section>
<Card.Footer>
<Button
className="mb-2 mb-sm-0"
onClick={[MockFunction cancelDelete]}
variant="tertiary"
>
<FormattedMessage
defaultMessage="Cancel"
description="Label For Button, which allows users to stop the process of deleting a transcript"
id="authoring.videoeditor.transcripts.cancelDeleteLabel"
/>
</Button>
<Button
className="mb-2 mb-sm-0"
onClick={[Function]}
variant="danger"
>
<FormattedMessage
defaultMessage="Delete"
description="Label For Button, which allows users to confirm the process of deleting a transcript"
id="authoring.videoeditor.transcripts.confirmDeleteLabel"
/>
</Button>
</Card.Footer>
</Card.Body>
</Card>
</Fragment>
`;
exports[`Transcript Component component component snapshots: renders as expected with transcriptUrl 1`] = `
<Fragment>
<ActionRow>
<LanguageSelector
language="lAnG"
title="sOmenUmBer"
/>
<ActionRow.Spacer />
<TranscriptActionMenu
index="sOmenUmBer"
language="lAnG"
launchDeleteConfirmation={[MockFunction launchDeleteConfirmation]}
transcriptUrl="url"
/>
</ActionRow>
</Fragment>
`;

View File

@@ -1,8 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VideoSourceWidget snapshots snapshots: renders as expected with default props 1`] = `
<injectIntl(ShimmedIntlComponent)
<CollapsibleFormWidget
fontSize="x-small"
subtitle={null}
title="Video source"
>
<ErrorAlert
@@ -154,12 +155,13 @@ exports[`VideoSourceWidget snapshots snapshots: renders as expected with default
id="authoring.videoeditor.videoSource.fallbackVideo.addButtonLabel"
/>
</Button>
</injectIntl(ShimmedIntlComponent)>
</CollapsibleFormWidget>
`;
exports[`VideoSourceWidget snapshots snapshots: renders as expected with videoSharingEnabledForCourse=true 1`] = `
<injectIntl(ShimmedIntlComponent)
<CollapsibleFormWidget
fontSize="x-small"
subtitle={null}
title="Video source"
>
<ErrorAlert
@@ -311,5 +313,5 @@ exports[`VideoSourceWidget snapshots snapshots: renders as expected with videoSh
id="authoring.videoeditor.videoSource.fallbackVideo.addButtonLabel"
/>
</Button>
</injectIntl(ShimmedIntlComponent)>
</CollapsibleFormWidget>
`;

View File

@@ -1,145 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CollapsibleFormWidget render snapshots: renders as expected with default props 1`] = `
<Advanced
className="collapsible-card rounded mx-4 my-3 px-4 text-primary-500"
defaultOpen={true}
>
<Trigger
className="collapsible-trigger d-flex border-0 align-items-center pt-4 p-0"
style={
{
"justifyContent": "unset",
}
}
>
<Visible
className="p-0 pb-3"
whenClosed={true}
>
<div
className="d-flex flex-column flex-grow-1"
>
<div
className="d-flex flex-grow-1 w-75 x-small"
>
tiTLE
</div>
<div
className=" mb-4 mt-3"
>
SuBTItle
</div>
</div>
<div
className="d-flex flex-row align-self-start"
>
<IconButton
alt="Expand"
iconAs="Icon"
variant="dark"
/>
</div>
</Visible>
<Visible
whenOpen={true}
>
<div
className="d-flex flex-grow-1 w-75 x-small"
>
tiTLE
</div>
<div
className="align-self-start"
>
<IconButton
alt="Collapse"
iconAs="Icon"
variant="dark"
/>
</div>
</Visible>
</Trigger>
<Body
className="collapsible-body rounded px-0 pb-4"
>
<p>
Some test string
</p>
</Body>
</Advanced>
`;
exports[`CollapsibleFormWidget render snapshots: renders with open={true} when there is error 1`] = `
<Advanced
className="collapsible-card rounded mx-4 my-3 px-4 text-primary-500"
defaultOpen={true}
open={true}
>
<Trigger
className="collapsible-trigger d-flex border-0 align-items-center pt-4 p-0"
style={
{
"justifyContent": "unset",
}
}
>
<Visible
className="p-0 pb-3"
whenClosed={true}
>
<div
className="d-flex flex-column flex-grow-1"
>
<div
className="d-flex flex-grow-1 w-75 x-small"
>
tiTLE
</div>
<div
className=" mb-4 mt-3"
>
SuBTItle
</div>
</div>
<div
className="d-flex flex-row align-self-start"
>
<Icon
className="alert-icon"
/>
<IconButton
alt="Expand"
iconAs="Icon"
variant="dark"
/>
</div>
</Visible>
<Visible
whenOpen={true}
>
<div
className="d-flex flex-grow-1 w-75 x-small"
>
tiTLE
</div>
<div
className="align-self-start"
>
<IconButton
alt="Collapse"
iconAs="Icon"
variant="dark"
/>
</div>
</Visible>
</Trigger>
<Body
className="collapsible-body rounded px-0 pb-4"
>
<p>
Some test string
</p>
</Body>
</Advanced>
`;

View File

@@ -4,7 +4,7 @@
"outDir": "dist",
"baseUrl": "./src",
"paths": {
"*": ["*"]
"@src/*": ["./*"],
}
},
"include": ["*.js", ".eslintrc.js", "src/**/*", "plugins/**/*"],

View File

@@ -4,6 +4,8 @@ const { createConfig } = require('@openedx/frontend-build');
const config = createConfig('webpack-dev', {
resolve: {
alias: {
// Within this app, we can use '@src/foo instead of relative URLs like '../../../foo'
'@src': path.resolve(__dirname, 'src/'),
// Plugins can use 'CourseAuthoring' as an import alias for this app:
CourseAuthoring: path.resolve(__dirname, 'src/'),
},