test: replacing snapshot tests with RTL tests part 4 (#2135)

* test: replacing snapshot tests with rtl tests part 4

* test: removing not needed icon mocks, and changing name to render function for editors
This commit is contained in:
jacobo-dominguez-wgu
2025-06-12 15:12:28 -06:00
committed by GitHub
parent 4a1f454855
commit ca85ca8e4b
21 changed files with 384 additions and 1151 deletions

View File

@@ -1,22 +1,9 @@
import React from 'react';
import * as reactRedux from 'react-redux';
import { Provider } from 'react-redux';
import * as hooks from './hooks';
import VideoSelector from './VideoSelector';
import { render as baseRender, initializeMocks, screen } from '../testUtils';
import { EditorContextProvider } from './EditorContext';
import editorStore from './data/store';
const render = (ui) => baseRender(ui, {
extraWrapper: ({ children }) => (
<EditorContextProvider learningContextId="course-v1:Org+COURSE+RUN">
<Provider store={editorStore}>
{children}
</Provider>
</EditorContextProvider>
),
});
import editorRender from './editorTestRender';
import { initializeMocks, screen } from '../testUtils';
const defaultProps = {
blockId: 'block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4',
@@ -32,7 +19,7 @@ describe('VideoSelector', () => {
test('renders VideoGallery when loading is false', () => {
jest.spyOn(hooks, 'useInitializeApp').mockReturnValue(false);
render(<VideoSelector {...defaultProps} />);
editorRender(<VideoSelector {...defaultProps} />);
expect(screen.getByText('Add video to your course')).toBeInTheDocument();
});
@@ -40,7 +27,7 @@ describe('VideoSelector', () => {
// "testing the application components in the way the user would use it"
test('renders nothing when loading is true', () => {
jest.spyOn(hooks, 'useInitializeApp').mockReturnValue(true);
render(<VideoSelector {...defaultProps} />);
editorRender(<VideoSelector {...defaultProps} />);
expect(screen.queryByText('Add video to your course')).not.toBeInTheDocument();
});
@@ -54,7 +41,7 @@ describe('VideoSelector', () => {
const mockDispatch = jest.fn();
jest.spyOn(reactRedux, 'useDispatch').mockReturnValue(mockDispatch);
jest.spyOn(hooks, 'useInitializeApp');
render(<VideoSelector {...defaultProps} />);
editorRender(<VideoSelector {...defaultProps} />);
expect(hooks.useInitializeApp).toHaveBeenCalledWith({
dispatch: mockDispatch,
data: initData,

View File

@@ -1,165 +0,0 @@
/* eslint-disable react/prop-types */
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { act, render, waitFor } from '@testing-library/react';
import { actions, selectors } from '../../../../../data/redux';
import { AnswersContainerInternal as AnswersContainer, mapStateToProps, mapDispatchToProps } from './AnswersContainer';
import { ProblemTypeKeys } from '../../../../../data/constants/problem';
jest.mock('@edx/frontend-platform/i18n', () => ({
FormattedMessage: ({ defaultMessage }) => (<p>{defaultMessage}</p>),
defineMessages: m => m,
injectIntl: (args) => args,
intlShape: {},
getLocale: jest.fn(),
}));
jest.mock('./AnswerOption', () => function mockAnswerOption() {
return <div>MockAnswerOption</div>;
});
jest.mock('../../../../../data/redux', () => ({
actions: {
problem: {
updateField: jest.fn().mockName('actions.problem.updateField'),
addAnswer: jest.fn().mockName('actions.problem.addAnswer'),
},
},
selectors: {
problem: {
answers: jest.fn(state => ({ answers: state })),
},
},
}));
describe('AnswersContainer', () => {
const props = {
answers: [],
updateField: jest.fn(),
addAnswer: jest.fn(),
};
describe('render', () => {
test('snapshot: renders correct default', () => {
act(() => {
expect(shallow(<AnswersContainer {...props} />).snapshot).toMatchSnapshot();
});
});
test('snapshot: renders correctly with answers', () => {
act(() => {
expect(shallow(
<AnswersContainer
{...props}
answers={[{ id: 'a', title: 'sOMetITlE', correct: true }, { id: 'b', title: 'sOMetITlE', correct: true }]}
/>,
).snapshot).toMatchSnapshot();
});
});
test('snapshot: numeric problems: answer range/answer select button: empty', () => {
act(() => {
const emptyAnswerProps = {
problemType: ProblemTypeKeys.NUMERIC,
answers: [],
updateField: jest.fn(),
addAnswer: jest.fn(),
addAnswerRange: jest.fn(),
};
expect(shallow(
<AnswersContainer
{...emptyAnswerProps}
/>,
).snapshot).toMatchSnapshot();
});
});
test('snapshot: numeric problems: answer range/answer select button: Range disables the additon of more adds', () => {
act(() => {
const answerRangeProps = {
problemType: ProblemTypeKeys.NUMERIC,
answers: [{
id: 'A',
title: 'Answer 1',
correct: true,
selectedFeedback: 'selected feedback',
unselectedFeedback: 'unselected feedback',
isAnswerRange: true,
}],
updateField: jest.fn(),
addAnswer: jest.fn(),
addAnswerRange: jest.fn(),
};
expect(shallow(
<AnswersContainer
{...answerRangeProps}
/>,
).snapshot).toMatchSnapshot();
});
});
test('snapshot: numeric problems: answer range/answer select button: multiple answers disables range.', () => {
act(() => {
const answersProps = {
problemType: ProblemTypeKeys.NUMERIC,
answers: [{
id: 'A',
title: 'Answer 1',
correct: true,
selectedFeedback: 'selected feedback',
unselectedFeedback: 'unselected feedback',
isAnswerRange: false,
},
{
id: 'B',
title: 'Answer 1',
correct: true,
selectedFeedback: 'selected feedback',
unselectedFeedback: 'unselected feedback',
isAnswerRange: false,
},
],
updateField: jest.fn(),
addAnswer: jest.fn(),
addAnswerRange: jest.fn(),
};
expect(shallow(
<AnswersContainer
{...answersProps}
/>,
).snapshot).toMatchSnapshot();
});
});
test('useAnswerContainer', async () => {
let container = null;
await act(async () => {
const wrapper = render(
<AnswersContainer
{...props}
answers={[{ id: 'a', title: 'sOMetITlE', correct: true }, { id: 'b', title: 'sOMetITlE', correct: true }]}
/>,
);
container = wrapper.container;
});
await waitFor(() => expect(container.querySelector('button')).toBeTruthy());
await new Promise(resolve => { setTimeout(resolve, 500); });
expect(props.updateField).toHaveBeenCalledWith(expect.objectContaining({ correctAnswerCount: 2 }));
});
});
describe('mapStateToProps', () => {
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
test('answers from problem.answers', () => {
expect(
mapStateToProps(testState).answers,
).toEqual(selectors.problem.answers(testState));
});
});
describe('mapDispatchToProps', () => {
test('updateField from actions.problem.updateField', () => {
expect(mapDispatchToProps.updateField).toEqual(actions.problem.updateField);
});
test('updateField from actions.problem.addAnswer', () => {
expect(mapDispatchToProps.addAnswer).toEqual(actions.problem.addAnswer);
});
});
});

View File

@@ -0,0 +1,65 @@
import React from 'react';
import {
render, screen, fireEvent, initializeMocks,
} from '../../../../../../testUtils';
import { AnswersContainerInternal as AnswersContainer } from './AnswersContainer';
import { ProblemTypeKeys } from '../../../../../data/constants/problem';
const { useAnswerContainer } = require('./hooks');
jest.mock('./AnswerOption', () => jest.fn(({ answer }) => <div>AnswerOption-{answer.id}</div>));
jest.mock(
'../../../../../sharedComponents/Button',
() => jest.fn(({ children, ...props }) => <button type="button" {...props}>{children}</button>),
);
jest.mock('./hooks', () => ({
useAnswerContainer: jest.fn(),
isSingleAnswerProblem: jest.fn(() => false),
}));
describe('AnswersContainer', () => {
const defaultProps = {
problemType: 'multiple_choice',
answers: [
{ id: 'a1', isAnswerRange: false },
{ id: 'a2', isAnswerRange: false },
],
addAnswer: jest.fn(),
addAnswerRange: jest.fn(),
updateField: jest.fn(),
};
beforeEach(() => {
initializeMocks();
});
it('renders AnswerOption for each answer', () => {
render(<AnswersContainer {...defaultProps} />);
expect(screen.getByText('AnswerOption-a1')).toBeInTheDocument();
expect(screen.getByText('AnswerOption-a2')).toBeInTheDocument();
});
it('renders add answer Button for non-NUMERIC problemType and calls addAnswer on click', () => {
render(<AnswersContainer {...defaultProps} />);
const button = screen.getByRole('button', { name: 'Add answer' });
expect(button).toBeInTheDocument();
fireEvent.click(button);
expect(defaultProps.addAnswer).toHaveBeenCalled();
});
it('renders Dropdown for NUMERIC problemType', () => {
render(<AnswersContainer {...defaultProps} problemType={ProblemTypeKeys.NUMERIC} />);
expect(screen.getByRole('button', { name: 'Add answer' })).toBeInTheDocument();
expect(screen.getByText('Add answer').closest('.dropdown')).toBeInTheDocument();
});
it('calls useAnswerContainer with correct args', () => {
render(<AnswersContainer {...defaultProps} />);
expect(useAnswerContainer).toHaveBeenCalledWith({
answers: defaultProps.answers,
problemType: defaultProps.problemType,
updateField: defaultProps.updateField,
});
});
});

View File

@@ -1,238 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AnswersContainer render snapshot: numeric problems: answer range/answer select button: Range disables the additon of more adds 1`] = `
<div
className="answers-container border border-light-700 rounded py-4 pl-4 pr-3"
>
<mockAnswerOption
answer={
{
"correct": true,
"id": "A",
"isAnswerRange": true,
"selectedFeedback": "selected feedback",
"title": "Answer 1",
"unselectedFeedback": "unselected feedback",
}
}
hasSingleAnswer={false}
key="A"
/>
<Dropdown>
<Dropdown.Toggle
className="pl-0"
id="Add-Answer-Or-Answer-Range"
variant="tertiary"
>
<Icon />
<FormattedMessage
defaultMessage="Add answer"
description="Button text to add answer"
id="authoring.answerwidget.answer.addAnswerButton"
/>
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item
className="AddAnswerRange disabled"
key="add-answer"
onClick={[MockFunction]}
>
<FormattedMessage
defaultMessage="Add answer"
description="Button text to add answer"
id="authoring.answerwidget.answer.addAnswerButton"
/>
</Dropdown.Item>
<Dropdown.Item
className="AddAnswerRange disabled"
key="add-answer-range"
onClick={[MockFunction]}
>
<FormattedMessage
defaultMessage="Add answer range"
description="Button text to add a range of answers"
id="authoring.answerwidget.answer.addAnswerRangeButton"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</div>
`;
exports[`AnswersContainer render snapshot: numeric problems: answer range/answer select button: empty 1`] = `
<div
className="answers-container border border-light-700 rounded py-4 pl-4 pr-3"
>
<Dropdown>
<Dropdown.Toggle
className="pl-0"
id="Add-Answer-Or-Answer-Range"
variant="tertiary"
>
<Icon />
<FormattedMessage
defaultMessage="Add answer"
description="Button text to add answer"
id="authoring.answerwidget.answer.addAnswerButton"
/>
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item
className="AddAnswerRange "
key="add-answer"
onClick={[MockFunction]}
>
<FormattedMessage
defaultMessage="Add answer"
description="Button text to add answer"
id="authoring.answerwidget.answer.addAnswerButton"
/>
</Dropdown.Item>
<Dropdown.Item
className="AddAnswerRange "
key="add-answer-range"
onClick={[MockFunction]}
>
<FormattedMessage
defaultMessage="Add answer range"
description="Button text to add a range of answers"
id="authoring.answerwidget.answer.addAnswerRangeButton"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</div>
`;
exports[`AnswersContainer render snapshot: numeric problems: answer range/answer select button: multiple answers disables range. 1`] = `
<div
className="answers-container border border-light-700 rounded py-4 pl-4 pr-3"
>
<mockAnswerOption
answer={
{
"correct": true,
"id": "A",
"isAnswerRange": false,
"selectedFeedback": "selected feedback",
"title": "Answer 1",
"unselectedFeedback": "unselected feedback",
}
}
hasSingleAnswer={false}
key="A"
/>
<mockAnswerOption
answer={
{
"correct": true,
"id": "B",
"isAnswerRange": false,
"selectedFeedback": "selected feedback",
"title": "Answer 1",
"unselectedFeedback": "unselected feedback",
}
}
hasSingleAnswer={false}
key="B"
/>
<Dropdown>
<Dropdown.Toggle
className="pl-0"
id="Add-Answer-Or-Answer-Range"
variant="tertiary"
>
<Icon />
<FormattedMessage
defaultMessage="Add answer"
description="Button text to add answer"
id="authoring.answerwidget.answer.addAnswerButton"
/>
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item
className="AddAnswerRange "
key="add-answer"
onClick={[MockFunction]}
>
<FormattedMessage
defaultMessage="Add answer"
description="Button text to add answer"
id="authoring.answerwidget.answer.addAnswerButton"
/>
</Dropdown.Item>
<Dropdown.Item
className="AddAnswerRange disabled"
key="add-answer-range"
onClick={[MockFunction]}
>
<FormattedMessage
defaultMessage="Add answer range"
description="Button text to add a range of answers"
id="authoring.answerwidget.answer.addAnswerRangeButton"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</div>
`;
exports[`AnswersContainer render snapshot: renders correct default 1`] = `
<div
className="answers-container border border-light-700 rounded py-4 pl-4 pr-3"
>
<Button
className={null}
onClick={[MockFunction]}
text={null}
variant="add"
>
<FormattedMessage
defaultMessage="Add answer"
description="Button text to add answer"
id="authoring.answerwidget.answer.addAnswerButton"
/>
</Button>
</div>
`;
exports[`AnswersContainer render snapshot: renders correctly with answers 1`] = `
<div
className="answers-container border border-light-700 rounded py-4 pl-4 pr-3"
>
<mockAnswerOption
answer={
{
"correct": true,
"id": "a",
"title": "sOMetITlE",
}
}
hasSingleAnswer={false}
key="a"
/>
<mockAnswerOption
answer={
{
"correct": true,
"id": "b",
"title": "sOMetITlE",
}
}
hasSingleAnswer={false}
key="b"
/>
<Button
className={null}
onClick={[MockFunction]}
text={null}
variant="add"
>
<FormattedMessage
defaultMessage="Add answer"
description="Button text to add answer"
id="authoring.answerwidget.answer.addAnswerButton"
/>
</Button>
</div>
`;

View File

@@ -1,60 +0,0 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import TypeRow from './TypeRow';
import { typeRowHooks } from '../hooks';
jest.mock('../hooks', () => ({
typeRowHooks: jest.fn(),
}));
describe('TypeRow', () => {
const typeKey = 'TEXTINPUT';
const props = {
answers: [],
blockTitle: 'bLoCkTiTLE',
correctAnswerCount: 0,
typeKey,
label: 'Text Input Problem',
selected: true,
lastRow: false,
problemType: 'prOBlEMtyPE',
setBlockTitle: jest.fn().mockName('args.setBlockTitle'),
updateField: jest.fn().mockName('args.updateField'),
updateAnswer: jest.fn().mockName('args.updateAnswer'),
};
const typeRowHooksProps = {
onClick: jest.fn().mockName('typeRowHooks.onClick'),
};
typeRowHooks.mockReturnValue(typeRowHooksProps);
describe('behavior', () => {
it(' calls typeRowHooks when initialized', () => {
shallow(<TypeRow {...props} />);
expect(typeRowHooks).toHaveBeenCalledWith({
answers: props.answers,
blockTitle: props.blockTitle,
correctAnswerCount: props.correctAnswerCount,
problemType: props.problemType,
typeKey,
setBlockTitle: props.setBlockTitle,
updateField: props.updateField,
updateAnswer: props.updateAnswer,
});
});
});
describe('snapshot', () => {
test('snapshot: renders type row setting card', () => {
expect(shallow(<TypeRow {...props} />).snapshot).toMatchSnapshot();
});
test('snapshot: renders type row setting card not selected', () => {
expect(shallow(<TypeRow {...props} selected={false} />).snapshot).toMatchSnapshot();
});
test('snapshot: renders type row setting card last row', () => {
expect(shallow(<TypeRow {...props} lastRow />).snapshot).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,68 @@
import React from 'react';
import {
render, screen, fireEvent, initializeMocks,
} from '../../../../../../../testUtils';
import TypeRow from './TypeRow';
const mockOnClick = jest.fn();
jest.mock('../hooks', () => ({
typeRowHooks: () => ({ onClick: mockOnClick }),
}));
const defaultProps = {
answers: [
{
correct: true, id: '1', selectedFeedback: 'Good', title: 'A', unselectedFeedback: 'Try again',
},
{
correct: false, id: '2', selectedFeedback: 'No', title: 'B', unselectedFeedback: 'Nope',
},
],
blockTitle: 'Block Title',
correctAnswerCount: 1,
typeKey: 'multiple_choice',
label: 'Multiple Choice',
selected: false,
lastRow: false,
problemType: 'choice',
setBlockTitle: jest.fn(),
updateField: jest.fn(),
updateAnswer: jest.fn(),
};
describe('TypeRow Component', () => {
beforeEach(() => {
initializeMocks();
});
test('renders label and Check icon when not selected', () => {
const { container } = render(<TypeRow {...defaultProps} />);
expect(screen.getByText('Multiple Choice')).toBeInTheDocument();
const icon = container.querySelector('svg');
expect(icon).toBeInTheDocument();
expect(icon?.parentElement).toHaveClass('text-success');
});
test('calls onClick from typeRowHooks when Button is clicked', () => {
render(<TypeRow {...defaultProps} />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(mockOnClick).toHaveBeenCalled();
});
test('renders with minimal valid props', () => {
const minimalProps = {
...defaultProps,
answers: [],
blockTitle: '',
correctAnswerCount: 0,
typeKey: 'short_answer',
label: 'Short Answer',
selected: false,
lastRow: false,
problemType: 'short',
};
render(<TypeRow {...minimalProps} />);
expect(screen.getByText('Short Answer')).toBeInTheDocument();
});
});

View File

@@ -1,82 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TypeRow snapshot snapshot: renders type row setting card 1`] = `
<Fragment>
<Button
className="d-flex p-0 flex-row justify-content-between w-100"
onClick={[MockFunction typeRowHooks.onClick]}
text={null}
variant="default"
>
<span
className="small text-primary-500"
>
Text Input Problem
</span>
<span
hidden={true}
>
<Icon
className="text-success"
/>
</span>
</Button>
<hr
className="d-block"
/>
</Fragment>
`;
exports[`TypeRow snapshot snapshot: renders type row setting card last row 1`] = `
<Fragment>
<Button
className="d-flex p-0 flex-row justify-content-between w-100"
onClick={[MockFunction typeRowHooks.onClick]}
text={null}
variant="default"
>
<span
className="small text-primary-500"
>
Text Input Problem
</span>
<span
hidden={true}
>
<Icon
className="text-success"
/>
</span>
</Button>
<hr
className="d-none"
/>
</Fragment>
`;
exports[`TypeRow snapshot snapshot: renders type row setting card not selected 1`] = `
<Fragment>
<Button
className="d-flex p-0 flex-row justify-content-between w-100"
onClick={[MockFunction typeRowHooks.onClick]}
text={null}
variant="default"
>
<span
className="small text-primary-500"
>
Text Input Problem
</span>
<span
hidden={false}
>
<Icon
className="text-success"
/>
</span>
</Button>
<hr
className="d-block"
/>
</Fragment>
`;

View File

@@ -1,12 +1,8 @@
import React from 'react';
import { Provider } from 'react-redux';
import {
render as baseRender, screen, fireEvent, initializeMocks,
} from '../../../../../testUtils';
import { screen, fireEvent, initializeMocks } from '../../../../../testUtils';
import editorRender from '../../../../editorTestRender';
import { EditProblemViewInternal, mapStateToProps } from './index';
import { ProblemTypeKeys } from '../../../../data/constants/problem';
import { EditorContextProvider } from '../../../../EditorContext';
import editorStore from '../../../../data/store';
import { selectors } from '../../../../data/redux';
const { saveBlock } = require('../../../../hooks');
@@ -44,16 +40,6 @@ jest.mock('./hooks', () => ({
getContent: jest.fn(() => 'content'),
}));
const render = (ui) => baseRender(ui, {
extraWrapper: ({ children }) => (
<EditorContextProvider learningContextId="course-v1:Org+COURSE+RUN">
<Provider store={editorStore}>
{children}
</Provider>
</EditorContextProvider>
),
});
describe('EditProblemView', () => {
const baseProps = {
problemType: 'standard',
@@ -71,7 +57,7 @@ describe('EditProblemView', () => {
});
it('renders standard problem widgets', () => {
render(<EditProblemViewInternal {...baseProps} />);
editorRender(<EditProblemViewInternal {...baseProps} />);
expect(screen.getByText('QuestionWidget')).toBeInTheDocument();
expect(screen.getByText('ExplanationWidget')).toBeInTheDocument();
expect(screen.getByText('AnswerWidget')).toBeInTheDocument();
@@ -81,23 +67,23 @@ describe('EditProblemView', () => {
});
it('renders advanced problem with RawEditor', () => {
render(<EditProblemViewInternal {...baseProps} problemType={ProblemTypeKeys.ADVANCED} />);
editorRender(<EditProblemViewInternal {...baseProps} problemType={ProblemTypeKeys.ADVANCED} />);
expect(screen.getByText('xml:<problem></problem>')).toBeInTheDocument();
expect(screen.getByText('SettingsWidget')).toBeInTheDocument();
});
it('renders markdown editor with RawEditor', () => {
render(<EditProblemViewInternal {...baseProps} isMarkdownEditorEnabled />);
editorRender(<EditProblemViewInternal {...baseProps} isMarkdownEditorEnabled />);
expect(screen.getByText('markdown:## Problem')).toBeInTheDocument();
});
it('shows AlertModal with correct title/body for standard', () => {
render(<EditProblemViewInternal {...baseProps} />);
editorRender(<EditProblemViewInternal {...baseProps} />);
expect(screen.getAllByText('No correct answer has been specified.').length).toBeGreaterThan(0);
});
it('calls saveBlock when save button is clicked', () => {
render(<EditProblemViewInternal {...baseProps} />);
editorRender(<EditProblemViewInternal {...baseProps} />);
const saveBtn = screen.getByRole('button', { name: 'Ok' });
fireEvent.click(saveBtn);
expect(saveBlock).toHaveBeenCalled();
@@ -110,7 +96,7 @@ describe('EditProblemView', () => {
openSaveWarningModal: jest.fn(),
closeSaveWarningModal,
});
render(<EditProblemViewInternal {...baseProps} />);
editorRender(<EditProblemViewInternal {...baseProps} />);
const cancelBtn = screen.getByRole('button', { name: 'Cancel' });
fireEvent.click(cancelBtn);
expect(closeSaveWarningModal).toHaveBeenCalled();

View File

@@ -1,61 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectTypeWrapper snapshot 1`] = `
<EditorModalWrapper>
<ModalDialog.Header
className="shadow-sm zindex-10"
>
<ModalDialog.Title>
<FormattedMessage
defaultMessage="Select problem type"
description="Title for select problem type modal"
id="authoring.problemEditor.selectType.title"
/>
<div
className="pgn__modal-close-container"
>
<IconButton
alt="Exit the editor"
iconAs="Icon"
src={[MockFunction icons.Close]}
/>
</div>
</ModalDialog.Title>
</ModalDialog.Header>
<EditorModalBody>
<h1>
test child
</h1>
</EditorModalBody>
<FooterWrapper>
<ModalDialog.Footer
className="border-top-0"
>
<ActionRow>
<ActionRow.Spacer />
<Button
aria-label="Cancel"
variant="tertiary"
>
<FormattedMessage
defaultMessage="Cancel"
description="Label for cancel button."
id="authoring.problemeditor.selecttype.cancelButton.label"
/>
</Button>
<Button
aria-label="Select"
disabled={false}
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Select"
description="Label for select button."
id="authoring.problemeditor.selecttype.selectButton.label"
/>
</Button>
</ActionRow>
</ModalDialog.Footer>
</FooterWrapper>
</EditorModalWrapper>
`;

View File

@@ -1,34 +1,85 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { IconButton } from '@openedx/paragon';
import SelectTypeWrapper from '.';
import { handleCancel } from '../../../../EditorContainer/hooks';
jest.mock('../../../../EditorContainer/hooks', () => ({
handleCancel: jest.fn().mockName('handleCancel'),
}));
import {
screen, fireEvent, initializeMocks,
} from '../../../../../../testUtils';
import editorRender from '../../../../../editorTestRender';
import SelectTypeWrapper from './index';
import * as hooks from '../hooks';
describe('SelectTypeWrapper', () => {
const props = {
children: (<h1>test child</h1>),
onClose: jest.fn(),
selected: 'iMAsElecTedValUE',
};
const mockOnClose = jest.fn();
test('snapshot', () => {
expect(shallow(<SelectTypeWrapper {...props} />).snapshot).toMatchSnapshot();
beforeEach(() => {
initializeMocks();
});
describe('behavior', () => {
let el;
beforeEach(() => {
el = shallow(<SelectTypeWrapper {...props} />);
});
test('close behavior is linked to modal onClose', () => {
const expected = handleCancel({ onClose: props.onClose });
expect(el.instance.findByType(IconButton)[0].props.onClick)
.toEqual(expected);
});
it('renders component with provided content', () => {
editorRender(
<SelectTypeWrapper selected="foo" onClose={mockOnClose}>
<div>Child Content</div>
</SelectTypeWrapper>,
);
expect(screen.getByText('Child Content')).toBeInTheDocument();
});
it('calls onClose when close button is clicked', () => {
editorRender(
<SelectTypeWrapper selected="foo" onClose={mockOnClose}>
<div />
</SelectTypeWrapper>,
);
fireEvent.click(screen.getByLabelText('Exit the editor'));
expect(mockOnClose).toHaveBeenCalled();
});
it('calls onClose when cancel button is clicked', () => {
editorRender(
<SelectTypeWrapper selected="foo" onClose={mockOnClose}>
<div />
</SelectTypeWrapper>,
);
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(mockOnClose).toHaveBeenCalled();
});
it('calls hooks.onSelect with correct args when select button is clicked', () => {
const onSelectMock = jest.fn();
jest.spyOn(hooks, 'onSelect').mockImplementation(onSelectMock);
editorRender(
<SelectTypeWrapper selected="foo" onClose={mockOnClose}>
<div />
</SelectTypeWrapper>,
);
fireEvent.click(screen.getByRole('button', { name: 'Select' }));
expect(hooks.onSelect).toHaveBeenCalledWith(
expect.objectContaining({
selected: 'foo',
updateField: expect.any(Function),
setBlockTitle: expect.any(Function),
defaultSettings: expect.any(Object),
}),
);
expect(onSelectMock).toHaveBeenCalled();
});
it('disables select button when selected is empty', () => {
editorRender(
<SelectTypeWrapper selected="" onClose={mockOnClose}>
<div />
</SelectTypeWrapper>,
);
const selectBtn = screen.getByRole('button', { name: 'Select' });
expect(selectBtn).toBeDisabled();
});
it('enables select button when selected is not empty', () => {
editorRender(
<SelectTypeWrapper selected="bar" onClose={mockOnClose}>
<div />
</SelectTypeWrapper>,
);
const selectBtn = screen.getByRole('button', { name: 'Select' });
expect(selectBtn).not.toBeDisabled();
});
});

View File

@@ -3,17 +3,15 @@ import PropTypes from 'prop-types';
import { Hyperlink, Image, Container } from '@openedx/paragon';
import {
FormattedMessage,
injectIntl,
intlShape,
useIntl,
} from '@edx/frontend-platform/i18n';
import messages from './messages';
import { ProblemTypes } from '../../../../../data/constants/problem';
const Preview = ({
problemType,
// injected
intl,
}) => {
const intl = useIntl();
if (problemType === null) {
return null;
}
@@ -48,9 +46,6 @@ Preview.defaultProps = {
Preview.propTypes = {
problemType: PropTypes.string,
// injected
intl: intlShape.isRequired,
};
export const PreviewInternal = Preview; // For testing only
export default injectIntl(Preview);
export default Preview;

View File

@@ -1,45 +0,0 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from '../../../../../testUtils';
import { PreviewInternal as Preview } from './Preview';
describe('Preview', () => {
const props = {
intl: { formatMessage },
problemType: null,
};
describe('snapshots', () => {
test('snapshots: renders as expected with default props', () => {
expect(
shallow(<Preview {...props} />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with problemType is stringresponse', () => {
expect(
shallow(<Preview {...props} problemType="stringresponse" />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with problemType is numericalresponse', () => {
expect(
shallow(<Preview {...props} problemType="numericalresponse" />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with problemType is optionresponse', () => {
expect(
shallow(<Preview {...props} problemType="optionresponse" />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with problemType is choiceresponse', () => {
expect(
shallow(<Preview {...props} problemType="choiceresponse" />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with problemType is multiplechoiceresponse', () => {
expect(
shallow(<Preview {...props} problemType="multiplechoiceresponse" />).snapshot,
).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { render, screen, initializeMocks } from '../../../../../../testUtils';
import Preview from './Preview';
// Mock ProblemTypes and messages
jest.mock('../../../../../data/constants/problem', () => ({
ProblemTypes: {
example: {
title: 'Example Title',
preview: 'example.png',
previewDescription: 'Example description',
helpLink: 'https://help.example.com',
},
},
}));
describe('Preview', () => {
beforeEach(() => {
initializeMocks();
});
it('renders nothing if problemType is null', () => {
const { container } = render(<Preview problemType={null} />);
const reduxProviderDiv = container.querySelector('div[data-testid="redux-provider"]');
expect(reduxProviderDiv?.innerHTML).toBe('');
});
it('renders preview with correct data for a valid problemType', () => {
render(<Preview problemType="example" />);
expect(screen.getByText('Example Title problem')).toBeInTheDocument();
expect(screen.getByText('Example description')).toBeInTheDocument();
expect(screen.getByRole('img')).toHaveAttribute('src', 'example.png');
expect(screen.getByRole('img')).toHaveAttribute('alt', 'A preview illustration of a null problem');
expect(screen.getByRole('link', { name: 'Learn more in a new tab' })).toHaveAttribute('href', 'https://help.example.com');
});
it('renders the help link with target="_blank"', () => {
render(<Preview problemType="example" />);
const link = screen.getByRole('link', { name: 'Learn more in a new tab' });
expect(link).toHaveAttribute('target', '_blank');
});
it('renders the correct title and description', () => {
render(<Preview problemType="example" />);
expect(screen.getByText('Example Title problem')).toBeInTheDocument();
expect(screen.getByText('Example description')).toBeInTheDocument();
});
});

View File

@@ -1,233 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Preview snapshots snapshots: renders as expected with default props 1`] = `null`;
exports[`Preview snapshots snapshots: renders as expected with problemType is choiceresponse 1`] = `
<Container
className="bg-light-300 rounded p-4"
style={
{
"height": "400px",
"width": "494px",
}
}
>
<div
className="small"
>
Multi-select problem
</div>
<Image
alt="A preview illustration of a {problemType, select,
multiplechoiceresponse {single select}
stringreponse {text input}
numericalresponse {numerical input}
optionresponse {dropdown}
choiceresponse {multiple select}
other {null}
} problem"
className="my-3"
fluid={true}
src="test-file-stub"
/>
<div
className="mb-3"
>
Learners must select all correct answers from a list of possible options.
</div>
<Hyperlink
destination="https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_multi_select.html"
target="_blank"
>
<FormattedMessage
defaultMessage="Learn more"
description="Label for Learn more button"
id="authoring.problemEditor.learnMoreButtonLabel.label"
/>
</Hyperlink>
</Container>
`;
exports[`Preview snapshots snapshots: renders as expected with problemType is multiplechoiceresponse 1`] = `
<Container
className="bg-light-300 rounded p-4"
style={
{
"height": "400px",
"width": "494px",
}
}
>
<div
className="small"
>
Single select problem
</div>
<Image
alt="A preview illustration of a {problemType, select,
multiplechoiceresponse {single select}
stringreponse {text input}
numericalresponse {numerical input}
optionresponse {dropdown}
choiceresponse {multiple select}
other {null}
} problem"
className="my-3"
fluid={true}
src="test-file-stub"
/>
<div
className="mb-3"
>
Learners must select the correct answer from a list of possible options.
</div>
<Hyperlink
destination="https://docs.openedx.org/en/latest/educators/concepts/exercise_tools/about_multi_select.html"
target="_blank"
>
<FormattedMessage
defaultMessage="Learn more"
description="Label for Learn more button"
id="authoring.problemEditor.learnMoreButtonLabel.label"
/>
</Hyperlink>
</Container>
`;
exports[`Preview snapshots snapshots: renders as expected with problemType is numericalresponse 1`] = `
<Container
className="bg-light-300 rounded p-4"
style={
{
"height": "400px",
"width": "494px",
}
}
>
<div
className="small"
>
Numerical input problem
</div>
<Image
alt="A preview illustration of a {problemType, select,
multiplechoiceresponse {single select}
stringreponse {text input}
numericalresponse {numerical input}
optionresponse {dropdown}
choiceresponse {multiple select}
other {null}
} problem"
className="my-3"
fluid={true}
src="test-file-stub"
/>
<div
className="mb-3"
>
Specify one or more correct numeric answers, submitted in a response field.
</div>
<Hyperlink
destination="https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/manage_numerical_input_problem.html"
target="_blank"
>
<FormattedMessage
defaultMessage="Learn more"
description="Label for Learn more button"
id="authoring.problemEditor.learnMoreButtonLabel.label"
/>
</Hyperlink>
</Container>
`;
exports[`Preview snapshots snapshots: renders as expected with problemType is optionresponse 1`] = `
<Container
className="bg-light-300 rounded p-4"
style={
{
"height": "400px",
"width": "494px",
}
}
>
<div
className="small"
>
Dropdown problem
</div>
<Image
alt="A preview illustration of a {problemType, select,
multiplechoiceresponse {single select}
stringreponse {text input}
numericalresponse {numerical input}
optionresponse {dropdown}
choiceresponse {multiple select}
other {null}
} problem"
className="my-3"
fluid={true}
src="test-file-stub"
/>
<div
className="mb-3"
>
Learners must select the correct answer from a list of possible options
</div>
<Hyperlink
destination="https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_dropdown.html"
target="_blank"
>
<FormattedMessage
defaultMessage="Learn more"
description="Label for Learn more button"
id="authoring.problemEditor.learnMoreButtonLabel.label"
/>
</Hyperlink>
</Container>
`;
exports[`Preview snapshots snapshots: renders as expected with problemType is stringresponse 1`] = `
<Container
className="bg-light-300 rounded p-4"
style={
{
"height": "400px",
"width": "494px",
}
}
>
<div
className="small"
>
Text input problem
</div>
<Image
alt="A preview illustration of a {problemType, select,
multiplechoiceresponse {single select}
stringreponse {text input}
numericalresponse {numerical input}
optionresponse {dropdown}
choiceresponse {multiple select}
other {null}
} problem"
className="my-3"
fluid={true}
src="test-file-stub"
/>
<div
className="mb-3"
>
Specify one or more correct text answers, including numbers and special characters, submitted in a response field.
</div>
<Hyperlink
destination="https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_text_input.html"
target="_blank"
>
<FormattedMessage
defaultMessage="Learn more"
description="Label for Learn more button"
id="authoring.problemEditor.learnMoreButtonLabel.label"
/>
</Hyperlink>
</Container>
`;

View File

@@ -1,12 +1,9 @@
import { Provider } from 'react-redux';
import {
fireEvent,
render,
screen,
initializeMocks,
} from '../../../../../testUtils';
import editorStore from '../../../../data/store';
import { EditorContextProvider } from '../../../../EditorContext';
import editorRender from '../../../../editorTestRender';
import * as hooks from './hooks';
import SelectTypeModal from '.';
@@ -19,12 +16,8 @@ describe('SelectTypeModal', () => {
const mockSelect = jest.fn();
jest.spyOn(hooks, 'onSelect').mockImplementation(mockSelect);
// This is a new-style test, unlike most of the old snapshot-based editor tests.
render(
<EditorContextProvider learningContextId="course-v1:Org+COURSE+RUN">
<Provider store={editorStore}>
<SelectTypeModal onClose={mockClose} />
</Provider>
</EditorContextProvider>,
editorRender(
<SelectTypeModal onClose={mockClose} />,
);
// First we see the menu of problem types:

View File

@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import {
FormattedMessage,
injectIntl,
} from '@edx/frontend-platform/i18n';
import {
Stack,
@@ -55,5 +54,4 @@ LicenseDisplay.propTypes = {
licenseDescription: PropTypes.string.isRequired,
};
export const LicenseDisplayInternal = LicenseDisplay; // For testing only
export default injectIntl(LicenseDisplay);
export default LicenseDisplay;

View File

@@ -1,47 +0,0 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { LicenseDisplayInternal as LicenseDisplay } from './LicenseDisplay';
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(() => ({ license: ['error.license', jest.fn().mockName('error.setLicense')] })),
}));
describe('LicenseDisplay', () => {
const props = {
license: 'all-rights-reserved',
details: {},
licenseDescription: 'FormattedMessage component with license description',
level: 'course',
};
describe('snapshots', () => {
test('snapshots: renders as expected with default props', () => {
expect(
shallow(<LicenseDisplay {...props} />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with level set to library', () => {
expect(
shallow(<LicenseDisplay {...props} level="library" />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with level set to block', () => {
expect(
shallow(<LicenseDisplay {...props} level="block" />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with level set to block and license set to select', () => {
expect(
shallow(<LicenseDisplay {...props} level="block" license="select" />).snapshot,
).toMatchSnapshot();
});
test('snapshots: renders as expected with level set to block and license set to Creative Commons', () => {
expect(
shallow(<LicenseDisplay {...props} level="block" license="creative-commons" />).snapshot,
).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { initializeMocks, render, screen } from '../../../../../../../testUtils';
import LicenseDisplay from './LicenseDisplay';
jest.mock('../../../../../../data/constants/licenses', () => ({
LicenseTypes: {
select: 'select',
creativeCommons: 'creativeCommons',
proprietary: 'proprietary',
},
}));
const defaultDetails = {
attribution: true,
noncommercial: false,
noDerivatives: false,
shareAlike: false,
};
describe('LicenseDisplay', () => {
beforeEach(() => {
initializeMocks();
});
it('renders nothing if license is select', () => {
const { container } = render(
<LicenseDisplay
license="select"
details={defaultDetails}
level="course"
licenseDescription="Some description"
/>,
);
const reduxProviderDiv = container.querySelector('div[data-testid="redux-provider"]');
expect(reduxProviderDiv?.innerHTML).toBe('');
});
it('renders displaySubsectionTitle and licenseDescription for non-select license', () => {
render(
<LicenseDisplay
license="proprietary"
details={defaultDetails}
level="course"
licenseDescription="Proprietary license description"
/>,
);
expect(screen.getByText('License Display')).toBeInTheDocument();
expect(screen.getByText('Proprietary license description')).toBeInTheDocument();
});
it('renders Hyperlink for creativeCommons license', () => {
render(
<LicenseDisplay
license="creativeCommons"
details={defaultDetails}
level="course"
licenseDescription="Creative Commons description"
/>,
);
const link = screen.getByRole('link', { name: 'View license details in a new tab' });
expect(link).toHaveAttribute('href', 'https://creativecommons.org/about');
expect(link).toHaveAttribute('target', '_blank');
});
it('does not render Hyperlink for non-creativeCommons license', () => {
render(
<LicenseDisplay
license="proprietary"
details={defaultDetails}
level="course"
licenseDescription="desc"
/>,
);
expect(screen.queryByRole('link', { name: 'View Details' })).toBeNull();
});
});

View File

@@ -1,130 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LicenseDisplay snapshots snapshots: renders as expected with default props 1`] = `
<Stack
gap={3}
>
<div
className="x-small"
>
<FormattedMessage
defaultMessage="License Display"
description="Title for license display subsection"
id="authoring.videoeditor.license.displaySubsection.title"
/>
</div>
<div
className="small border border-gray-300 rounded p-4"
>
<injectIntl(ShimmedIntlComponent)
details={{}}
license="all-rights-reserved"
/>
<div
className="x-small mt-3"
>
FormattedMessage component with license description
</div>
</div>
</Stack>
`;
exports[`LicenseDisplay snapshots snapshots: renders as expected with level set to block 1`] = `
<Stack
gap={3}
>
<div
className="x-small"
>
<FormattedMessage
defaultMessage="License Display"
description="Title for license display subsection"
id="authoring.videoeditor.license.displaySubsection.title"
/>
</div>
<div
className="small border border-gray-300 rounded p-4"
>
<injectIntl(ShimmedIntlComponent)
details={{}}
license="all-rights-reserved"
/>
<div
className="x-small mt-3"
>
FormattedMessage component with license description
</div>
</div>
</Stack>
`;
exports[`LicenseDisplay snapshots snapshots: renders as expected with level set to block and license set to Creative Commons 1`] = `
<Stack
gap={3}
>
<div
className="x-small"
>
<FormattedMessage
defaultMessage="License Display"
description="Title for license display subsection"
id="authoring.videoeditor.license.displaySubsection.title"
/>
</div>
<div
className="small border border-gray-300 rounded p-4"
>
<injectIntl(ShimmedIntlComponent)
details={{}}
license="creative-commons"
/>
<div
className="x-small mt-3"
>
FormattedMessage component with license description
</div>
</div>
<Hyperlink
className="text-primary-500 x-small"
destination="https://creativecommons.org/about"
target="_blank"
>
<FormattedMessage
defaultMessage="View license details"
description="Label for view license details button"
id="authoring.videoeditor.license.viewLicenseDetailsLabel.label"
/>
</Hyperlink>
</Stack>
`;
exports[`LicenseDisplay snapshots snapshots: renders as expected with level set to block and license set to select 1`] = `null`;
exports[`LicenseDisplay snapshots snapshots: renders as expected with level set to library 1`] = `
<Stack
gap={3}
>
<div
className="x-small"
>
<FormattedMessage
defaultMessage="License Display"
description="Title for license display subsection"
id="authoring.videoeditor.license.displaySubsection.title"
/>
</div>
<div
className="small border border-gray-300 rounded p-4"
>
<injectIntl(ShimmedIntlComponent)
details={{}}
license="all-rights-reserved"
/>
<div
className="x-small mt-3"
>
FormattedMessage component with license description
</div>
</div>
</Stack>
`;

View File

@@ -34,7 +34,7 @@ exports[`LicenseWidget snapshots snapshots: renders as expected with default pro
level="course"
license="all-rights-reserved"
/>
<injectIntl(ShimmedIntlComponent)
<LicenseDisplay
details={{}}
license="all-rights-reserved"
licenseDescription={
@@ -101,7 +101,7 @@ exports[`LicenseWidget snapshots snapshots: renders as expected with isLibrary t
level="library"
license="all-rights-reserved"
/>
<injectIntl(ShimmedIntlComponent)
<LicenseDisplay
details={{}}
license="all-rights-reserved"
licenseDescription={
@@ -151,7 +151,7 @@ exports[`LicenseWidget snapshots snapshots: renders as expected with licenseType
level="block"
license="all-rights-reserved"
/>
<injectIntl(ShimmedIntlComponent)
<LicenseDisplay
details={{}}
license="all-rights-reserved"
licenseDescription={

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { Provider } from 'react-redux';
import { render as baseRender } from '../testUtils';
import { EditorContextProvider } from './EditorContext';
import editorStore from './data/store';
/**
* Custom render function for testing React components with the editor context and Redux store.
*
* Wraps the provided UI in both the EditorContextProvider and Redux Provider,
* ensuring that components under test have access to the necessary context and store.
*
* @param {React.ReactElement} ui - The React element to render.
* @param {string} [learningContextId='course-v1:Org+COURSE+RUN'] - Optional learning context ID
* for the EditorContextProvider.
* @returns {RenderResult} The result of the render, as returned by RTL render.
*/
const editorRender = (ui, learningContextId = 'course-v1:Org+COURSE+RUN') => baseRender(ui, {
extraWrapper: ({ children }) => (
<EditorContextProvider learningContextId={learningContextId}>
<Provider store={editorStore}>
{children}
</Provider>
</EditorContextProvider>
),
});
export default editorRender;