test: replacing snapshot tests with RTL tests part 7 (#2181)

This commit is contained in:
jacobo-dominguez-wgu
2025-06-19 10:11:51 -06:00
committed by GitHub
parent 920f4a54e1
commit 08c3d123d8
19 changed files with 420 additions and 982 deletions

View File

@@ -47,5 +47,4 @@ EditableHeader.propTypes = {
cancelEdit: PropTypes.func.isRequired,
};
export const EditableHeaderInternal = EditableHeader; // For testing only
export default EditableHeader;

View File

@@ -1,33 +0,0 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Form } from '@openedx/paragon';
import { EditableHeaderInternal as EditableHeader } from './EditableHeader';
import EditConfirmationButtons from './EditConfirmationButtons';
describe('EditableHeader', () => {
const props = {
handleChange: jest.fn().mockName('args.handleChange'),
updateTitle: jest.fn().mockName('args.updateTitle'),
handleKeyDown: jest.fn().mockName('args.handleKeyDown'),
inputRef: jest.fn().mockName('args.inputRef'),
localTitle: 'test-title-text',
cancelEdit: jest.fn().mockName('args.cancelEdit'),
};
let el;
beforeEach(() => {
el = shallow(<EditableHeader {...props} />);
});
describe('snapshot', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('displays Edit Icon', () => {
const formControl = el.instance.findByType(Form.Control)[0];
expect(formControl.props.trailingElement).toMatchObject(
<EditConfirmationButtons updateTitle={props.updateTitle} cancelEdit={props.cancelEdit} />,
);
});
});
});

View File

@@ -0,0 +1,62 @@
import React from 'react';
import {
render, screen, initializeMocks, fireEvent,
} from '@src/testUtils';
import EditableHeader from './EditableHeader';
describe('EditableHeader', () => {
const props = {
handleChange: jest.fn().mockName('args.handleChange'),
updateTitle: jest.fn().mockName('args.updateTitle'),
handleKeyDown: jest.fn().mockName('args.handleKeyDown'),
inputRef: jest.fn().mockName('args.inputRef'),
localTitle: 'test-title-text',
cancelEdit: jest.fn().mockName('args.cancelEdit'),
};
beforeEach(() => {
initializeMocks();
});
test('renders input with correct value and placeholder', () => {
render(<EditableHeader {...props} />);
const input = screen.getByPlaceholderText('Title');
expect(input).toBeInTheDocument();
expect((input as HTMLInputElement).value).toBe(props.localTitle);
});
test('calls handleChange when input changes', () => {
render(<EditableHeader {...props} />);
const input = screen.getByPlaceholderText('Title');
fireEvent.change(input, { target: { value: 'New title' } });
expect(props.handleChange).toHaveBeenCalled();
});
test('calls handleKeyDown on keydown', () => {
render(<EditableHeader {...props} />);
const input = screen.getByPlaceholderText('Title');
fireEvent.keyDown(input, { target: { value: 'New title' } });
expect(props.handleKeyDown).toHaveBeenCalled();
});
test('calls updateTitle on blur', () => {
render(<EditableHeader {...props} />);
const input = screen.getByPlaceholderText('Title');
fireEvent.blur(input);
expect(props.updateTitle).toHaveBeenCalled();
});
test('calls inputRef if provided', () => {
const inputRef = jest.fn();
render(<EditableHeader {...props} inputRef={inputRef} />);
expect(inputRef).toHaveBeenCalled();
});
test('renders buttons from trailing element EditConfirmationButtons', () => {
render(<EditableHeader {...props} />);
const cancelButton = screen.getByRole('button', { name: /cancel/i });
expect(cancelButton).toBeInTheDocument();
const saveButton = screen.getByRole('button', { name: /save/i });
expect(saveButton).toBeInTheDocument();
});
});

View File

@@ -1,26 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EditableHeader snapshot snapshot 1`] = `
<Form.Group
onBlur={[Function]}
>
<Form.Control
autoFocus={true}
onChange={[MockFunction args.handleChange]}
onKeyDown={[MockFunction args.handleKeyDown]}
placeholder="Title"
style={
{
"paddingInlineEnd": "calc(1rem + 84px)",
}
}
trailingElement={
<EditConfirmationButtons
cancelEdit={[MockFunction args.cancelEdit]}
updateTitle={[MockFunction args.updateTitle]}
/>
}
value="test-title-text"
/>
</Form.Group>
`;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import SettingsOption from '../SettingsOption';
import { ProblemTypeKeys } from '../../../../../../data/constants/problem';
import messages from '../messages';
@@ -15,9 +15,8 @@ const HintsCard = ({
images,
isLibrary,
learningContextId,
// inject
intl,
}) => {
const intl = useIntl();
const { summary, handleAdd } = hintsCardHooks(hints, updateSettings);
if (problemType === ProblemTypeKeys.ADVANCED) { return null; }
@@ -55,7 +54,6 @@ const HintsCard = ({
};
HintsCard.propTypes = {
intl: intlShape.isRequired,
hints: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
@@ -67,5 +65,4 @@ HintsCard.propTypes = {
isLibrary: PropTypes.bool.isRequired,
};
export const HintsCardInternal = HintsCard; // For testing only
export default injectIntl(HintsCard);
export default HintsCard;

View File

@@ -1,83 +0,0 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from '../../../../../../testUtils';
import { HintsCardInternal as HintsCard } from './HintsCard';
import { hintsCardHooks, hintsRowHooks } from '../hooks';
import messages from '../messages';
jest.mock('../hooks', () => ({
hintsCardHooks: jest.fn(),
hintsRowHooks: jest.fn(),
}));
describe('HintsCard', () => {
const hint1 = { id: 1, value: 'hint1' };
const hint2 = { id: 2, value: '' };
const hints0 = [];
const hints1 = [hint1];
const hints2 = [hint1, hint2];
const props = {
intl: { formatMessage },
hints: hints0,
updateSettings: jest.fn().mockName('args.updateSettings'),
};
const hintsRowHooksProps = {
handleChange: jest.fn().mockName('hintsRowHooks.handleChange'),
handleDelete: jest.fn().mockName('hintsRowHooks.handleDelete'),
images: {},
isLibrary: false,
learningContextId: 'course+org+run',
};
hintsRowHooks.mockReturnValue(hintsRowHooksProps);
describe('behavior', () => {
it(' calls hintsCardHooks when initialized', () => {
const hintsCardHooksProps = {
summary: { message: messages.noHintSummary, values: {} },
handleAdd: jest.fn().mockName('hintsCardHooks.handleAdd'),
};
hintsCardHooks.mockReturnValue(hintsCardHooksProps);
shallow(<HintsCard {...props} />);
expect(hintsCardHooks).toHaveBeenCalledWith(hints0, props.updateSettings);
});
});
describe('snapshot', () => {
test('snapshot: renders hints setting card no hints', () => {
const hintsCardHooksProps = {
summary: { message: messages.noHintSummary, values: {} },
handleAdd: jest.fn().mockName('hintsCardHooks.handleAdd'),
};
hintsCardHooks.mockReturnValue(hintsCardHooksProps);
expect(shallow(<HintsCard {...props} />).snapshot).toMatchSnapshot();
});
test('snapshot: renders hints setting card one hint', () => {
const hintsCardHooksProps = {
summary: {
message: messages.hintSummary,
values: { hint: hint1.value, count: 1 },
},
handleAdd: jest.fn().mockName('hintsCardHooks.handleAdd'),
};
hintsCardHooks.mockReturnValue(hintsCardHooksProps);
expect(shallow(<HintsCard {...props} hints={hints1} />).snapshot).toMatchSnapshot();
});
test('snapshot: renders hints setting card multiple hints', () => {
const hintsCardHooksProps = {
summary: {
message: messages.hintSummary,
values: { hint: hint2.value, count: 2 },
},
handleAdd: jest.fn().mockName('hintsCardHooks.handleAdd'),
};
hintsCardHooks.mockReturnValue(hintsCardHooksProps);
expect(shallow(<HintsCard {...props} hints={hints2} />).snapshot).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,51 @@
import React from 'react';
import { render, screen, initializeMocks } from '@src/testUtils';
import HintsCard from './HintsCard';
jest.mock('./HintRow', () => 'HintRow');
describe('HintsCard', () => {
const hint1 = { id: '1', value: 'hint-1' };
const hint2 = { id: '2', value: 'hint-2' };
const hints0 = [];
const hints1 = [hint1];
const hints2 = [hint1, hint2];
const props = {
hints: hints0,
updateSettings: jest.fn().mockName('args.updateSettings'),
problemType: 'multiplechoiceresponse',
images: {},
isLibrary: false,
learningContextId: 'ID+',
};
beforeEach(() => {
initializeMocks();
});
describe('HintsCard', () => {
test('renders component', () => {
render(<HintsCard {...props} />);
expect(screen.getByText('Hints')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Add hint' })).toBeInTheDocument();
});
test('does not render component when problemType is advanced', () => {
render(<HintsCard {...props} problemType="advanced" />);
expect(screen.queryByText('Hints')).not.toBeInTheDocument();
expect(screen.queryByText('button')).not.toBeInTheDocument();
});
test('renders hints setting card one hint', () => {
render(<HintsCard {...props} hints={hints1} />);
expect(document.querySelector('hintrow[value="hint-1"]')).toBeInTheDocument();
});
test('snapshot: renders hints setting card multiple hints', () => {
render(<HintsCard {...props} hints={hints2} />);
expect(document.querySelector('hintrow[value="hint-1"]')).toBeInTheDocument();
expect(document.querySelector('hintrow[value="hint-2"]')).toBeInTheDocument();
});
});
});

View File

@@ -2,7 +2,7 @@ import React from 'react';
import isNil from 'lodash/isNil';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Form, Hyperlink } from '@openedx/paragon';
import { selectors } from '../../../../../../data/redux';
import SettingsOption from '../SettingsOption';
@@ -13,13 +13,12 @@ const ScoringCard = ({
scoring,
defaultValue,
updateSettings,
// inject
intl,
// redux
studioEndpointUrl,
learningContextId,
isLibrary,
}) => {
const intl = useIntl();
const {
handleUnlimitedChange,
handleMaxAttemptChange,
@@ -93,7 +92,6 @@ const ScoringCard = ({
};
ScoringCard.propTypes = {
intl: intlShape.isRequired,
// eslint-disable-next-line
scoring: PropTypes.any.isRequired,
updateSettings: PropTypes.func.isRequired,
@@ -117,5 +115,4 @@ export const mapStateToProps = (state) => ({
export const mapDispatchToProps = {};
export const ScoringCardInternal = ScoringCard; // For testing only
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ScoringCard));
export default connect(mapStateToProps, mapDispatchToProps)(ScoringCard);

View File

@@ -1,13 +1,11 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from '../../../../../../testUtils';
import { scoringCardHooks } from '../hooks';
import { ScoringCardInternal as ScoringCard } from './ScoringCard';
import {
render, screen, initializeMocks, fireEvent,
} from '@src/testUtils';
import ScoringCard from './ScoringCard';
import { selectors } from '../../../../../../data/redux';
jest.mock('../hooks', () => ({
scoringCardHooks: jest.fn(),
}));
const { app } = selectors;
describe('ScoringCard', () => {
const scoring = {
@@ -17,55 +15,69 @@ describe('ScoringCard', () => {
number: 5,
},
updateSettings: jest.fn().mockName('args.updateSettings'),
intl: { formatMessage },
};
const props = {
scoring,
intl: { formatMessage },
defaultValue: 1,
updateSettings: jest.fn(),
};
const scoringCardHooksProps = {
handleMaxAttemptChange: jest.fn().mockName('scoringCardHooks.handleMaxAttemptChange'),
handleWeightChange: jest.fn().mockName('scoringCardHooks.handleWeightChange'),
handleOnChange: jest.fn().mockName('scoringCardHooks.handleOnChange'),
local: 5,
};
scoringCardHooks.mockReturnValue(scoringCardHooksProps);
describe('behavior', () => {
it(' calls scoringCardHooks when initialized', () => {
shallow(<ScoringCard {...props} />);
expect(scoringCardHooks).toHaveBeenCalledWith(scoring, props.updateSettings, props.defaultValue);
});
beforeEach(() => {
jest.spyOn(app, 'studioEndpointUrl').mockReturnValue('studioEndpointUrl');
jest.spyOn(app, 'learningContextId').mockReturnValue('learningContextId');
jest.spyOn(app, 'isLibrary').mockReturnValue(false);
initializeMocks();
});
describe('snapshot', () => {
test('snapshot: scoring setting card', () => {
expect(shallow(<ScoringCard {...props} />).snapshot).toMatchSnapshot();
});
test('snapshot: scoring setting card zero zero weight', () => {
expect(shallow(<ScoringCard
{...props}
scoring={{
...scoring,
weight: 0,
}}
/>).snapshot).toMatchSnapshot();
});
test('snapshot: scoring setting card max attempts', () => {
expect(shallow(<ScoringCard
{...props}
scoring={{
...scoring,
attempts: {
unlimited: true,
number: 0,
},
}}
/>).snapshot).toMatchSnapshot();
});
test('render the component', () => {
render(<ScoringCard {...props} />);
expect(screen.getByText('Scoring')).toBeInTheDocument();
});
test('should not render advance settings link when isLibrary is true', () => {
jest.spyOn(app, 'isLibrary').mockReturnValue(true);
render(<ScoringCard {...props} />);
fireEvent.click(screen.getByText('Scoring'));
expect(screen.queryByText('Set a default value in advanced settings')).not.toBeInTheDocument();
});
test('should render advance settings link when isLibrary is false', () => {
jest.spyOn(app, 'isLibrary').mockReturnValue(false);
render(<ScoringCard {...props} />);
fireEvent.click(screen.getByText('Scoring'));
expect(screen.getByText('Set a default value in advanced settings')).toBeInTheDocument();
});
test('should call updateSettings when clicking points button', () => {
render(<ScoringCard {...props} scoring={{ ...scoring, weight: 0 }} />);
fireEvent.click(screen.getByText('Scoring'));
const pointsButton = screen.getByRole('spinbutton', { name: 'Points' });
expect(pointsButton).toBeInTheDocument();
expect(pointsButton.value).toBe('0');
fireEvent.change(pointsButton, { target: { value: '0.1' } });
expect(props.updateSettings).toHaveBeenCalled();
});
test('should call updateSettings when clicking attempts button', () => {
const scoringUnlimited = { ...scoring, attempts: { unlimited: true, number: 0 } };
render(<ScoringCard {...props} scoring={scoringUnlimited} />);
fireEvent.click(screen.getByText('Scoring'));
fireEvent.click(screen.getByText('Attempts'));
const attemptsButton = screen.getByRole('spinbutton', { name: 'Points' });
expect(attemptsButton).toBeInTheDocument();
expect(attemptsButton.value).toBe('1.5');
fireEvent.change(attemptsButton, { target: { value: '2' } });
expect(props.updateSettings).toHaveBeenCalled();
});
test('should display checked checkbox when unlimited is true', () => {
const scoringUnlimited = { ...scoring, attempts: { unlimited: true, number: 0 } };
render(<ScoringCard {...props} scoring={scoringUnlimited} />);
fireEvent.click(screen.getByText('Scoring'));
const checkbox = screen.getByRole('checkbox', { name: 'Unlimited attempts' });
expect(checkbox).toBeChecked();
fireEvent.click(checkbox);
expect(props.updateSettings).toHaveBeenCalled();
});
});

View File

@@ -1,97 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HintsCard snapshot snapshot: renders hints setting card multiple hints 1`] = `
<SettingsOption
className=""
extraSections={[]}
hasExpandableTextArea={true}
none={false}
summary=" {count, plural, =0 {} other {(+# more)}}"
title="Hints"
>
<HintRow
handleChange={[MockFunction hintsRowHooks.handleChange]}
handleDelete={[MockFunction hintsRowHooks.handleDelete]}
id={1}
key="1"
value="hint1"
/>
<HintRow
handleChange={[MockFunction hintsRowHooks.handleChange]}
handleDelete={[MockFunction hintsRowHooks.handleDelete]}
id={2}
key="2"
value=""
/>
<Button
className="m-0 p-0 font-weight-bold"
onClick={[MockFunction hintsCardHooks.handleAdd]}
size="sm"
text={null}
variant="add"
>
<FormattedMessage
defaultMessage="Add hint"
description="Add hint button text"
id="authoring.problemeditor.settings.hint.addHintButton"
/>
</Button>
</SettingsOption>
`;
exports[`HintsCard snapshot snapshot: renders hints setting card no hints 1`] = `
<SettingsOption
className=""
extraSections={[]}
hasExpandableTextArea={true}
none={true}
summary="None"
title="Hints"
>
<Button
className="m-0 p-0 font-weight-bold"
onClick={[MockFunction hintsCardHooks.handleAdd]}
size="sm"
text={null}
variant="add"
>
<FormattedMessage
defaultMessage="Add hint"
description="Add hint button text"
id="authoring.problemeditor.settings.hint.addHintButton"
/>
</Button>
</SettingsOption>
`;
exports[`HintsCard snapshot snapshot: renders hints setting card one hint 1`] = `
<SettingsOption
className=""
extraSections={[]}
hasExpandableTextArea={true}
none={false}
summary="hint1 {count, plural, =0 {} other {(+# more)}}"
title="Hints"
>
<HintRow
handleChange={[MockFunction hintsRowHooks.handleChange]}
handleDelete={[MockFunction hintsRowHooks.handleDelete]}
id={1}
key="1"
value="hint1"
/>
<Button
className="m-0 p-0 font-weight-bold"
onClick={[MockFunction hintsCardHooks.handleAdd]}
size="sm"
text={null}
variant="add"
>
<FormattedMessage
defaultMessage="Add hint"
description="Add hint button text"
id="authoring.problemeditor.settings.hint.addHintButton"
/>
</Button>
</SettingsOption>
`;

View File

@@ -1,238 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ScoringCard snapshot snapshot: scoring setting card 1`] = `
<SettingsOption
className="scoringCard"
extraSections={[]}
hasExpandableTextArea={false}
summary="{weight, plural, =0 {Ungraded} other {# points}} · {attempts, plural, =1 {# attempt} other {# attempts}}"
title="Scoring"
>
<div
className="mb-4"
>
<FormattedMessage
defaultMessage="Specify point weight and the number of answer attempts"
description="Descriptive text for scoring settings"
id="authoring.problemeditor.settings.scoring.label"
/>
</div>
<Form.Group>
<Form.Control
floatingLabel="Points"
min={0}
onChange={[MockFunction scoringCardHooks.handleWeightChange]}
step={0.1}
type="number"
value={1.5}
/>
<Form.Control.Feedback>
<FormattedMessage
defaultMessage="If a value is not set, the problem is worth one point"
description="Summary text for scoring weight"
id="authoring.problemeditor.settings.scoring.weight.hint"
/>
</Form.Control.Feedback>
</Form.Group>
<Form.Group>
<Form.Control
disabled={false}
floatingLabel="Attempts"
min={0}
onBlur={[MockFunction scoringCardHooks.handleMaxAttemptChange]}
onChange={[MockFunction scoringCardHooks.handleOnChange]}
type="number"
/>
<Form.Control.Feedback>
<FormattedMessage
defaultMessage="If a default value is not set in advanced settings, unlimited attempts are allowed"
description="Summary text for scoring weight"
id="authoring.problemeditor.settings.scoring.attempts.hint"
/>
</Form.Control.Feedback>
<Form.Checkbox
checked={false}
className="mt-3 decoration-control-label"
disabled={true}
>
<div
className="x-small"
>
<FormattedMessage
defaultMessage="Unlimited attempts"
description="Label for unlimited attempts checkbox"
id="authoring.problemeditor.settings.scoring.attempts.unlimitedCheckbox"
/>
</div>
</Form.Checkbox>
</Form.Group>
<Hyperlink
destination="undefined/settings/advanced/null#max_attempts"
target="_blank"
>
<FormattedMessage
defaultMessage="Set a default value in advanced settings"
description="Advanced settings link text"
id="authoring.problemeditor.settings.advancedSettingLink.text"
/>
</Hyperlink>
</SettingsOption>
`;
exports[`ScoringCard snapshot snapshot: scoring setting card max attempts 1`] = `
<SettingsOption
className="scoringCard"
extraSections={[]}
hasExpandableTextArea={false}
summary="{weight, plural, =0 {Ungraded} other {# points}} · Unlimited attempts"
title="Scoring"
>
<div
className="mb-4"
>
<FormattedMessage
defaultMessage="Specify point weight and the number of answer attempts"
description="Descriptive text for scoring settings"
id="authoring.problemeditor.settings.scoring.label"
/>
</div>
<Form.Group>
<Form.Control
floatingLabel="Points"
min={0}
onChange={[MockFunction scoringCardHooks.handleWeightChange]}
step={0.1}
type="number"
value={1.5}
/>
<Form.Control.Feedback>
<FormattedMessage
defaultMessage="If a value is not set, the problem is worth one point"
description="Summary text for scoring weight"
id="authoring.problemeditor.settings.scoring.weight.hint"
/>
</Form.Control.Feedback>
</Form.Group>
<Form.Group>
<Form.Control
disabled={true}
floatingLabel="Attempts"
min={0}
onBlur={[MockFunction scoringCardHooks.handleMaxAttemptChange]}
onChange={[MockFunction scoringCardHooks.handleOnChange]}
type="number"
/>
<Form.Control.Feedback>
<FormattedMessage
defaultMessage="If a default value is not set in advanced settings, unlimited attempts are allowed"
description="Summary text for scoring weight"
id="authoring.problemeditor.settings.scoring.attempts.hint"
/>
</Form.Control.Feedback>
<Form.Checkbox
checked={true}
className="mt-3 decoration-control-label"
disabled={true}
>
<div
className="x-small"
>
<FormattedMessage
defaultMessage="Unlimited attempts"
description="Label for unlimited attempts checkbox"
id="authoring.problemeditor.settings.scoring.attempts.unlimitedCheckbox"
/>
</div>
</Form.Checkbox>
</Form.Group>
<Hyperlink
destination="undefined/settings/advanced/null#max_attempts"
target="_blank"
>
<FormattedMessage
defaultMessage="Set a default value in advanced settings"
description="Advanced settings link text"
id="authoring.problemeditor.settings.advancedSettingLink.text"
/>
</Hyperlink>
</SettingsOption>
`;
exports[`ScoringCard snapshot snapshot: scoring setting card zero zero weight 1`] = `
<SettingsOption
className="scoringCard"
extraSections={[]}
hasExpandableTextArea={false}
summary="{weight, plural, =0 {Ungraded} other {# points}} · {attempts, plural, =1 {# attempt} other {# attempts}}"
title="Scoring"
>
<div
className="mb-4"
>
<FormattedMessage
defaultMessage="Specify point weight and the number of answer attempts"
description="Descriptive text for scoring settings"
id="authoring.problemeditor.settings.scoring.label"
/>
</div>
<Form.Group>
<Form.Control
floatingLabel="Points"
min={0}
onChange={[MockFunction scoringCardHooks.handleWeightChange]}
step={0.1}
type="number"
value={0}
/>
<Form.Control.Feedback>
<FormattedMessage
defaultMessage="If a value is not set, the problem is worth one point"
description="Summary text for scoring weight"
id="authoring.problemeditor.settings.scoring.weight.hint"
/>
</Form.Control.Feedback>
</Form.Group>
<Form.Group>
<Form.Control
disabled={false}
floatingLabel="Attempts"
min={0}
onBlur={[MockFunction scoringCardHooks.handleMaxAttemptChange]}
onChange={[MockFunction scoringCardHooks.handleOnChange]}
type="number"
/>
<Form.Control.Feedback>
<FormattedMessage
defaultMessage="If a default value is not set in advanced settings, unlimited attempts are allowed"
description="Summary text for scoring weight"
id="authoring.problemeditor.settings.scoring.attempts.hint"
/>
</Form.Control.Feedback>
<Form.Checkbox
checked={false}
className="mt-3 decoration-control-label"
disabled={true}
>
<div
className="x-small"
>
<FormattedMessage
defaultMessage="Unlimited attempts"
description="Label for unlimited attempts checkbox"
id="authoring.problemeditor.settings.scoring.attempts.unlimitedCheckbox"
/>
</div>
</Form.Checkbox>
</Form.Group>
<Hyperlink
destination="undefined/settings/advanced/null#max_attempts"
target="_blank"
>
<FormattedMessage
defaultMessage="Set a default value in advanced settings"
description="Advanced settings link text"
id="authoring.problemeditor.settings.advancedSettingLink.text"
/>
</Hyperlink>
</SettingsOption>
`;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Form } from '@openedx/paragon';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import * as hooks from './hooks';
import messages from './messages';
@@ -22,39 +22,40 @@ const AltTextControls = ({
setValue,
validation,
value,
// inject
intl,
}) => (
<Form.Group className="mt-4.5">
<Form.Label as="h4">
<FormattedMessage {...messages.accessibilityLabel} />
</Form.Label>
<Form.Control
className="mt-4.5"
disabled={isDecorative}
floatingLabel={intl.formatMessage(messages.altTextFloatingLabel)}
isInvalid={validation.show}
onChange={hooks.onInputChange(setValue)}
type="input"
value={value}
/>
{validation.show
}) => {
const intl = useIntl();
return (
<Form.Group className="mt-4.5">
<Form.Label as="h4">
<FormattedMessage {...messages.accessibilityLabel} />
</Form.Label>
<Form.Control
className="mt-4.5"
disabled={isDecorative}
floatingLabel={intl.formatMessage(messages.altTextFloatingLabel)}
isInvalid={validation.show}
onChange={hooks.onInputChange(setValue)}
type="input"
value={value}
/>
{validation.show
&& (
<Form.Control.Feedback type="invalid">
<FormattedMessage {...messages.altTextLocalFeedback} />
</Form.Control.Feedback>
)}
<Form.Checkbox
checked={isDecorative}
className="mt-4.5 decorative-control-label"
onChange={hooks.onCheckboxChange(setIsDecorative)}
>
<Form.Label>
<FormattedMessage {...messages.decorativeAltTextCheckboxLabel} />
</Form.Label>
</Form.Checkbox>
</Form.Group>
);
<Form.Checkbox
checked={isDecorative}
className="mt-4.5 decorative-control-label"
onChange={hooks.onCheckboxChange(setIsDecorative)}
>
<Form.Label>
<FormattedMessage {...messages.decorativeAltTextCheckboxLabel} />
</Form.Label>
</Form.Checkbox>
</Form.Group>
);
};
AltTextControls.propTypes = {
error: PropTypes.shape({
show: PropTypes.bool,
@@ -66,9 +67,7 @@ AltTextControls.propTypes = {
show: PropTypes.bool,
}).isRequired,
value: PropTypes.string.isRequired,
// inject
intl: intlShape.isRequired,
};
export const AltTextControlsInternal = AltTextControls; // For testing only
export default injectIntl(AltTextControls);
export default AltTextControls;

View File

@@ -1,34 +0,0 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from '../../../testUtils';
import { AltTextControlsInternal as AltTextControls } from './AltTextControls';
jest.mock('./hooks', () => ({
onInputChange: (handler) => ({ 'hooks.onInputChange': handler }),
onCheckboxChange: (handler) => ({ 'hooks.onCheckboxChange': handler }),
}));
describe('AltTextControls', () => {
const props = {
isDecorative: true,
value: 'props.value',
// inject
intl: { formatMessage },
};
beforeEach(() => {
props.setValue = jest.fn().mockName('props.setValue');
props.setIsDecorative = jest.fn().mockName('props.setIsDecorative');
props.validation = { show: true };
});
describe('render', () => {
test('snapshot: isDecorative=true errorProps.showAltTextSubmissionError=true', () => {
expect(shallow(<AltTextControls {...props} />).snapshot).toMatchSnapshot();
});
test('snapshot: isDecorative=true errorProps.showAltTextSubmissionError=false', () => {
props.validation.show = false;
expect(shallow(<AltTextControls {...props} />).snapshot).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,62 @@
import React from 'react';
import {
render, screen, initializeMocks, fireEvent,
} from '@src/testUtils';
import AltTextControls from './AltTextControls';
describe('AltTextControls', () => {
const props = {
isDecorative: true,
value: 'props.value',
setValue: jest.fn().mockName('props.setValue'),
setIsDecorative: jest.fn().mockName('props.setIsDecorative'),
validation: { show: false },
error: { show: false },
};
beforeEach(() => {
initializeMocks();
});
test('renders component on screen', () => {
render(<AltTextControls {...props} />);
expect(screen.getByRole('checkbox', { name: 'This image is decorative (no alt text required).' })).toBeInTheDocument();
expect(screen.getByRole('textbox', { name: 'Accessibility' })).toBeInTheDocument();
});
test('renders validation feedback when validation.show is true', () => {
const feedbackMessage = 'Enter alt text';
render(<AltTextControls {...props} validation={{ show: true }} />);
expect(screen.getByText(feedbackMessage)).toBeInTheDocument();
});
test('does not render validation feedback when validation.show is false', () => {
const feedbackMessage = 'Enter alt text';
render(<AltTextControls {...props} validation={{ show: false }} />);
expect(screen.queryByText(feedbackMessage)).not.toBeInTheDocument();
});
test('disables textbox when isDecorative is true', () => {
render(<AltTextControls {...props} isDecorative />);
expect(screen.getByRole('textbox', { name: 'Accessibility' })).toBeDisabled();
});
test('enables textbox when isDecorative is false', () => {
render(<AltTextControls {...props} isDecorative={false} />);
expect(screen.getByRole('textbox', { name: 'Accessibility' })).not.toBeDisabled();
});
test('calls setValue on textbox change', () => {
render(<AltTextControls {...props} isDecorative={false} />);
const textbox = screen.getByRole('textbox', { name: 'Accessibility' });
fireEvent.change(textbox, { target: { value: 'new alt text' } });
expect(props.setValue).toHaveBeenCalled();
});
test('calls setIsDecorative on checkbox change', () => {
render(<AltTextControls {...props} />);
const checkbox = screen.getByRole('checkbox', { name: 'This image is decorative (no alt text required).' });
checkbox.click();
expect(props.setIsDecorative).toHaveBeenCalled();
});
});

View File

@@ -9,7 +9,7 @@ import {
Locked,
Unlocked,
} from '@openedx/paragon/icons';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import * as hooks from './hooks';
import messages from './messages';
@@ -32,42 +32,44 @@ const DimensionControls = ({
unlock,
updateDimensions,
value,
// inject
intl,
}) => ((value !== null) && (
<Form.Group>
<Form.Label as="h4">
<FormattedMessage {...messages.imageDimensionsLabel} />
</Form.Label>
<div className="mt-4.5">
<Form.Control
className="dimension-input"
value={value.width}
onChange={hooks.onInputChange(setWidth)}
onBlur={updateDimensions}
floatingLabel={intl.formatMessage(messages.widthFloatingLabel)}
/>
<Form.Control
className="dimension-input"
value={value.height}
onChange={hooks.onInputChange(setHeight)}
onBlur={updateDimensions}
floatingLabel={intl.formatMessage(messages.heightFloatingLabel)}
/>
<IconButton
className="d-inline-block"
alt={
}) => {
const intl = useIntl();
if (!value) { return null; }
return (
<Form.Group>
<Form.Label as="h4">
<FormattedMessage {...messages.imageDimensionsLabel} />
</Form.Label>
<div className="mt-4.5">
<Form.Control
className="dimension-input"
value={value.width}
onChange={hooks.onInputChange(setWidth)}
onBlur={updateDimensions}
floatingLabel={intl.formatMessage(messages.widthFloatingLabel)}
/>
<Form.Control
className="dimension-input"
value={value.height}
onChange={hooks.onInputChange(setHeight)}
onBlur={updateDimensions}
floatingLabel={intl.formatMessage(messages.heightFloatingLabel)}
/>
<IconButton
className="d-inline-block"
alt={
isLocked
? intl.formatMessage(messages.unlockDimensionsLabel)
: intl.formatMessage(messages.lockDimensionsLabel)
}
iconAs={Icon}
src={isLocked ? Locked : Unlocked}
onClick={isLocked ? unlock : lock}
/>
</div>
</Form.Group>
));
iconAs={Icon}
src={isLocked ? Locked : Unlocked}
onClick={isLocked ? unlock : lock}
/>
</div>
</Form.Group>
);
};
DimensionControls.defaultProps = {
value: {
height: '100',
@@ -85,9 +87,6 @@ DimensionControls.propTypes = ({
lock: PropTypes.func.isRequired,
unlock: PropTypes.func.isRequired,
updateDimensions: PropTypes.func.isRequired,
// inject
intl: intlShape.isRequired,
});
export const DimensionControlsInternal = DimensionControls; // For testing only
export default injectIntl(DimensionControls);
export default DimensionControls;

View File

@@ -1,141 +0,0 @@
import 'CourseAuthoring/editors/setupEditorTest';
import React, { useEffect } from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import * as paragon from '@openedx/paragon';
import * as icons from '@openedx/paragon/icons';
import {
fireEvent, render, screen, waitFor,
} from '@testing-library/react';
import { formatMessage } from '../../../testUtils';
import { DimensionControlsInternal as DimensionControls } from './DimensionControls';
import * as hooks from './hooks';
const WrappedDimensionControls = () => {
const dimensions = hooks.dimensionHooks('altText');
useEffect(() => {
dimensions.onImgLoad({ })({ target: { naturalWidth: 1517, naturalHeight: 803 } });
}, []);
return <DimensionControls {...dimensions} intl={{ formatMessage }} />;
};
const UnlockedDimensionControls = () => {
const dimensions = hooks.dimensionHooks('altText');
useEffect(() => {
dimensions.onImgLoad({ })({ target: { naturalWidth: 1517, naturalHeight: 803 } });
dimensions.unlock();
}, []);
return <DimensionControls {...dimensions} intl={{ formatMessage }} />;
};
describe('DimensionControls', () => {
describe('render', () => {
const props = {
lockAspectRatio: { width: 4, height: 5 },
locked: { 'props.locked': 'lockedValue' },
isLocked: true,
value: { width: 20, height: 40 },
// inject
intl: { formatMessage },
};
beforeEach(() => {
jest.spyOn(hooks, 'onInputChange').mockImplementation((handler) => ({ 'hooks.onInputChange': handler }));
props.setWidth = jest.fn().mockName('props.setWidth');
props.setHeight = jest.fn().mockName('props.setHeight');
props.lock = jest.fn().mockName('props.lock');
props.unlock = jest.fn().mockName('props.unlock');
props.updateDimensions = jest.fn().mockName('props.updateDimensions');
});
afterEach(() => {
jest.spyOn(hooks, 'onInputChange').mockRestore();
});
test('snapshot', () => {
expect(shallow(<DimensionControls {...props} />).snapshot).toMatchSnapshot();
});
test('null value: empty snapshot', () => {
const el = shallow(<DimensionControls {...props} value={null} />);
expect(el.snapshot).toMatchSnapshot();
expect(el.isEmptyRender()).toEqual(true);
});
test('unlocked dimensions', () => {
const el = shallow(<DimensionControls {...props} isLocked={false} />);
expect(el.snapshot).toMatchSnapshot();
});
});
describe('component tests for dimensions', () => {
beforeEach(() => {
paragon.Form.Group = jest.fn().mockImplementation(({ children }) => (
<div>{children}</div>
));
paragon.Form.Label = jest.fn().mockImplementation(({ children }) => (
<div>{children}</div>
));
// eslint-disable-next-line no-import-assign
paragon.Icon = jest.fn().mockImplementation(({ children }) => (
<div>{children}</div>
));
// eslint-disable-next-line no-import-assign
paragon.IconButton = jest.fn().mockImplementation(({ children }) => (
<div>{children}</div>
));
paragon.Form.Control = jest.fn().mockImplementation(({ value, onChange, onBlur }) => (
<input className="formControl" onChange={onChange} onBlur={onBlur} value={value} />
));
// eslint-disable-next-line no-import-assign
icons.Locked = jest.fn().mockImplementation(() => {});
// eslint-disable-next-line no-import-assign
icons.Unlocked = jest.fn().mockImplementation(() => {});
});
afterEach(() => {
paragon.Form.Group.mockRestore();
paragon.Form.Label.mockRestore();
paragon.Form.Control.mockRestore();
paragon.Icon.mockRestore();
paragon.IconButton.mockRestore();
icons.Locked.mockRestore();
icons.Unlocked.mockRestore();
});
it('renders with initial dimensions', () => {
const { container } = render(<WrappedDimensionControls />);
const widthInput = container.querySelector('.formControl');
expect(widthInput.value).toBe('1517');
});
it('resizes dimensions proportionally', async () => {
const { container } = render(<WrappedDimensionControls />);
const widthInput = container.querySelector('.formControl');
expect(widthInput.value).toBe('1517');
fireEvent.change(widthInput, { target: { value: 758 } });
await waitFor(() => {
expect(container.querySelectorAll('.formControl')[0].value).toBe('758');
});
fireEvent.blur(widthInput);
await waitFor(() => {
expect(container.querySelectorAll('.formControl')[0].value).toBe('758');
expect(container.querySelectorAll('.formControl')[1].value).toBe('401');
});
screen.debug();
});
it('resizes only changed dimension when unlocked', async () => {
const { container } = render(<UnlockedDimensionControls />);
const widthInput = container.querySelector('.formControl');
expect(widthInput.value).toBe('1517');
fireEvent.change(widthInput, { target: { value: 758 } });
await waitFor(() => {
expect(container.querySelectorAll('.formControl')[0].value).toBe('758');
});
fireEvent.blur(widthInput);
await waitFor(() => {
expect(container.querySelectorAll('.formControl')[0].value).toBe('758');
expect(container.querySelectorAll('.formControl')[1].value).toBe('803');
});
screen.debug();
});
});
});

View File

@@ -0,0 +1,111 @@
import React, { useEffect } from 'react';
import {
fireEvent, render, screen, waitFor, initializeMocks,
} from '@src/testUtils';
import DimensionControls from './DimensionControls';
import * as hooks from './hooks';
const WrappedDimensionControls = () => {
const dimensions = hooks.dimensionHooks('altText');
useEffect(() => {
dimensions.onImgLoad({ })({ target: { naturalWidth: 1517, naturalHeight: 803 } });
}, []);
return <DimensionControls {...dimensions} />;
};
const UnlockedDimensionControls = () => {
const dimensions = hooks.dimensionHooks('altText');
useEffect(() => {
dimensions.onImgLoad({ })({ target: { naturalWidth: 1517, naturalHeight: 803 } });
dimensions.unlock();
}, []);
return <DimensionControls {...dimensions} />;
};
describe('DimensionControls', () => {
describe('render', () => {
const props = {
lockAspectRatio: { width: 4, height: 5 },
locked: { 'props.locked': 'lockedValue' },
isLocked: true,
value: { width: '20', height: '40' },
setWidth: jest.fn(),
setHeight: jest.fn(),
lock: jest.fn(),
unlock: jest.fn(),
updateDimensions: jest.fn(),
};
beforeEach(() => {
jest.spyOn(hooks, 'onInputChange').mockImplementation((handler) => ({ 'hooks.onInputChange': handler }));
initializeMocks();
});
afterEach(() => {
jest.spyOn(hooks, 'onInputChange').mockRestore();
});
test('renders component', () => {
render(<DimensionControls {...props} />);
expect(screen.getByText('Image Dimensions')).toBeInTheDocument();
});
test('renders nothing with null value', () => {
const reduxProviderWrapper = '<div data-testid="redux-provider"></div>';
const { container } = render(<DimensionControls {...props} value={null} />);
expect(screen.queryByText('Image Dimensions')).not.toBeInTheDocument();
expect(container.innerHTML).toBe(reduxProviderWrapper);
expect(container.firstChild?.textContent).toBe('');
});
test('renders locked and unlocked icon button according to isLocked prop', () => {
const { rerender } = render(<DimensionControls {...props} isLocked={false} />);
expect(screen.getByRole('button', { name: 'lock dimensions' })).toBeInTheDocument();
rerender(<DimensionControls {...props} isLocked />);
expect(screen.getByRole('button', { name: 'unlock dimensions' })).toBeInTheDocument();
});
});
describe('component tests for dimensions', () => {
beforeEach(() => {
initializeMocks();
});
it('renders with initial dimensions', () => {
const { container } = render(<WrappedDimensionControls />);
const widthInput = container.querySelector('input.form-control');
expect(widthInput).not.toBeNull();
expect((widthInput as HTMLInputElement).value).toBe('1517');
});
it('resizes dimensions proportionally', async () => {
const { container } = render(<WrappedDimensionControls />);
const widthInput = container.querySelector('input.form-control') as HTMLInputElement;
expect((widthInput as HTMLInputElement).value).toBe('1517');
fireEvent.change(widthInput, { target: { value: 758 } });
await waitFor(() => {
expect((container.querySelectorAll('input.form-control')[0] as HTMLInputElement).value).toBe('758');
});
fireEvent.blur(widthInput);
await waitFor(() => {
expect((container.querySelectorAll('input.form-control')[0] as HTMLInputElement).value).toBe('758');
expect((container.querySelectorAll('input.form-control')[1] as HTMLInputElement).value).toBe('401');
});
});
it('resizes only changed dimension when unlocked', async () => {
const { container } = render(<UnlockedDimensionControls />);
const widthInput = container.querySelector('input.form-control') as HTMLInputElement;
expect(widthInput.value).toBe('1517');
fireEvent.change(widthInput, { target: { value: 758 } });
await waitFor(() => {
expect((container.querySelectorAll('input.form-control')[0] as HTMLInputElement).value).toBe('758');
});
fireEvent.blur(widthInput);
await waitFor(() => {
expect((container.querySelectorAll('input.form-control')[0] as HTMLInputElement).value).toBe('758');
expect((container.querySelectorAll('input.form-control')[1] as HTMLInputElement).value).toBe('803');
});
});
});
});

View File

@@ -1,102 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AltTextControls render snapshot: isDecorative=true errorProps.showAltTextSubmissionError=false 1`] = `
<Form.Group
className="mt-4.5"
>
<Form.Label
as="h4"
>
<FormattedMessage
defaultMessage="Accessibility"
description="Label title for accessibility section."
id="authoring.texteditor.imagesettingsmodal.accessibilityLabel"
/>
</Form.Label>
<Form.Control
className="mt-4.5"
disabled={true}
floatingLabel="Alt Text"
isInvalid={false}
onChange={
{
"hooks.onInputChange": [MockFunction props.setValue],
}
}
type="input"
value="props.value"
/>
<Form.Checkbox
checked={true}
className="mt-4.5 decorative-control-label"
onChange={
{
"hooks.onCheckboxChange": [MockFunction props.setIsDecorative],
}
}
>
<Form.Label>
<FormattedMessage
defaultMessage="This image is decorative (no alt text required)."
description="Checkbox label for whether or not an image is decorative."
id="authoring.texteditor.imagesettingsmodal.decorativeAltTextCheckboxLabel"
/>
</Form.Label>
</Form.Checkbox>
</Form.Group>
`;
exports[`AltTextControls render snapshot: isDecorative=true errorProps.showAltTextSubmissionError=true 1`] = `
<Form.Group
className="mt-4.5"
>
<Form.Label
as="h4"
>
<FormattedMessage
defaultMessage="Accessibility"
description="Label title for accessibility section."
id="authoring.texteditor.imagesettingsmodal.accessibilityLabel"
/>
</Form.Label>
<Form.Control
className="mt-4.5"
disabled={true}
floatingLabel="Alt Text"
isInvalid={true}
onChange={
{
"hooks.onInputChange": [MockFunction props.setValue],
}
}
type="input"
value="props.value"
/>
<Form.Control.Feedback
type="invalid"
>
<FormattedMessage
defaultMessage="Enter alt text"
description="Message feedback for user below the alt text field."
id="authoring.texteditor.imagesettingsmodal.error.altTextLocalFeedback"
/>
</Form.Control.Feedback>
<Form.Checkbox
checked={true}
className="mt-4.5 decorative-control-label"
onChange={
{
"hooks.onCheckboxChange": [MockFunction props.setIsDecorative],
}
}
>
<Form.Label>
<FormattedMessage
defaultMessage="This image is decorative (no alt text required)."
description="Checkbox label for whether or not an image is decorative."
id="authoring.texteditor.imagesettingsmodal.decorativeAltTextCheckboxLabel"
/>
</Form.Label>
</Form.Checkbox>
</Form.Group>
`;

View File

@@ -1,97 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DimensionControls render null value: empty snapshot 1`] = `false`;
exports[`DimensionControls render snapshot 1`] = `
<Form.Group>
<Form.Label
as="h4"
>
<FormattedMessage
defaultMessage="Image Dimensions"
description="Label title for the image dimensions section."
id="authoring.texteditor.imagesettingsmodal.imageDimensionsLabel"
/>
</Form.Label>
<div
className="mt-4.5"
>
<Form.Control
className="dimension-input"
floatingLabel="Width"
onBlur={[MockFunction props.updateDimensions]}
onChange={
{
"hooks.onInputChange": [MockFunction props.setWidth],
}
}
value={20}
/>
<Form.Control
className="dimension-input"
floatingLabel="Height"
onBlur={[MockFunction props.updateDimensions]}
onChange={
{
"hooks.onInputChange": [MockFunction props.setHeight],
}
}
value={40}
/>
<IconButton
alt="unlock dimensions"
className="d-inline-block"
iconAs="Icon"
onClick={[MockFunction props.unlock]}
src={[MockFunction icons.Locked]}
/>
</div>
</Form.Group>
`;
exports[`DimensionControls render unlocked dimensions 1`] = `
<Form.Group>
<Form.Label
as="h4"
>
<FormattedMessage
defaultMessage="Image Dimensions"
description="Label title for the image dimensions section."
id="authoring.texteditor.imagesettingsmodal.imageDimensionsLabel"
/>
</Form.Label>
<div
className="mt-4.5"
>
<Form.Control
className="dimension-input"
floatingLabel="Width"
onBlur={[MockFunction props.updateDimensions]}
onChange={
{
"hooks.onInputChange": [MockFunction props.setWidth],
}
}
value={20}
/>
<Form.Control
className="dimension-input"
floatingLabel="Height"
onBlur={[MockFunction props.updateDimensions]}
onChange={
{
"hooks.onInputChange": [MockFunction props.setHeight],
}
}
value={40}
/>
<IconButton
alt="lock dimensions"
className="d-inline-block"
iconAs="Icon"
onClick={[MockFunction props.lock]}
src={[MockFunction icons.Unlocked]}
/>
</div>
</Form.Group>
`;