feat: add label button to question tinyMCE (#221)
This commit is contained in:
14
package-lock.json
generated
14
package-lock.json
generated
@@ -36,7 +36,7 @@
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "^11.0.2",
|
||||
"@edx/frontend-platform": "2.4.0",
|
||||
"@edx/paragon": "^20.27.0",
|
||||
"@edx/paragon": "^20.28.0",
|
||||
"@testing-library/dom": "^8.13.0",
|
||||
"@testing-library/react": "^12.1.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
@@ -4327,9 +4327,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/paragon": {
|
||||
"version": "20.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.27.0.tgz",
|
||||
"integrity": "sha512-jy62ZEBdAVlsP6tAm1/YDyMtc9fiD47H00whoW+y2Z+lLZqPsv6D5boIPQIcdBeg0W4f2gCU4TEy2+b2q8mYGA==",
|
||||
"version": "20.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.28.0.tgz",
|
||||
"integrity": "sha512-ydM3DK2aqdYRXyNEC0+P9tFpckH9kx/b/HtgSGrNMreRGAJ90QNkI/zAdlwj8U9mmoaE9jfIpjsO4g84qnrk1A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
@@ -37695,9 +37695,9 @@
|
||||
}
|
||||
},
|
||||
"@edx/paragon": {
|
||||
"version": "20.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.27.0.tgz",
|
||||
"integrity": "sha512-jy62ZEBdAVlsP6tAm1/YDyMtc9fiD47H00whoW+y2Z+lLZqPsv6D5boIPQIcdBeg0W4f2gCU4TEy2+b2q8mYGA==",
|
||||
"version": "20.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.28.0.tgz",
|
||||
"integrity": "sha512-ydM3DK2aqdYRXyNEC0+P9tFpckH9kx/b/HtgSGrNMreRGAJ90QNkI/zAdlwj8U9mmoaE9jfIpjsO4g84qnrk1A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "^11.0.2",
|
||||
"@edx/frontend-platform": "2.4.0",
|
||||
"@edx/paragon": "^20.27.0",
|
||||
"@edx/paragon": "^20.28.0",
|
||||
"@testing-library/dom": "^8.13.0",
|
||||
"@testing-library/react": "^12.1.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ProblemEditor snapshots block not yet loaded, Spinner appears 1`] = `
|
||||
<div
|
||||
className="text-center p-6"
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="m-3"
|
||||
screenreadertext="Loading Problem Editor"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ProblemEditor snapshots renders EditProblemView 1`] = `
|
||||
<EditProblemView
|
||||
onClose={[MockFunction props.onClose]}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`ProblemEditor snapshots renders SelectTypeModal 1`] = `
|
||||
<SelectTypeModal
|
||||
onClose={[MockFunction props.onClose]}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`ProblemEditor snapshots renders as expected with default behavior 1`] = `
|
||||
<div
|
||||
className="text-center p-6"
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="m-3"
|
||||
screenreadertext="Loading Problem Editor"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ProblemEditor snapshots studio view not yet loaded, Spinner appears 1`] = `
|
||||
<div
|
||||
className="text-center p-6"
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="m-3"
|
||||
screenreadertext="Loading Problem Editor"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`QuestionWidget render snapshot: renders correct default 1`] = `
|
||||
<div
|
||||
className="question-widget"
|
||||
>
|
||||
<div
|
||||
className="h4 mb-3"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Question"
|
||||
description="Question Title"
|
||||
id="authoring.questionwidget.question.questionWidgetTitle"
|
||||
/>
|
||||
</div>
|
||||
<Editor
|
||||
problemEditorConfig={
|
||||
Object {
|
||||
"question": "This is my question",
|
||||
"setEditorRef": [MockFunction hooks.prepareEditorRef.setEditorRef],
|
||||
"updateQuestion": [MockFunction],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -8,12 +8,29 @@ import { selectors, actions } from '../../../../../data/redux';
|
||||
import { messages } from './messages';
|
||||
import './index.scss';
|
||||
|
||||
import 'tinymce';
|
||||
import 'tinymce/themes/silver';
|
||||
import 'tinymce/skins/ui/oxide/skin.css';
|
||||
import 'tinymce/icons/default';
|
||||
import 'tinymce/plugins/link';
|
||||
import 'tinymce/plugins/lists';
|
||||
import 'tinymce/plugins/table';
|
||||
import 'tinymce/plugins/hr';
|
||||
import 'tinymce/plugins/codesample';
|
||||
import 'tinymce/plugins/emoticons';
|
||||
import 'tinymce/plugins/emoticons/js/emojis';
|
||||
import 'tinymce/plugins/charmap';
|
||||
import 'tinymce/plugins/code';
|
||||
import 'tinymce/plugins/autoresize';
|
||||
import 'tinymce/plugins/image';
|
||||
import 'tinymce/plugins/imagetools';
|
||||
|
||||
// This widget should be connected, grab all questions from store, update them as needed.
|
||||
export const QuestionWidget = ({
|
||||
question,
|
||||
updateQuestion,
|
||||
}) => {
|
||||
const { editorRef, refReady, setEditorRef } = hooks.prepareEditorRef();
|
||||
const { refReady, setEditorRef } = hooks.prepareEditorRef();
|
||||
if (!refReady) { return null; }
|
||||
return (
|
||||
<div className="question-widget">
|
||||
@@ -23,7 +40,6 @@ export const QuestionWidget = ({
|
||||
<Editor {
|
||||
...hooks.problemEditorConfig({
|
||||
setEditorRef,
|
||||
editorRef,
|
||||
question,
|
||||
updateQuestion,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { actions, selectors } from '../../../../../data/redux';
|
||||
import { QuestionWidget, mapStateToProps, mapDispatchToProps } from '.';
|
||||
|
||||
jest.mock('../../../../../data/redux', () => ({
|
||||
actions: {
|
||||
problem: {
|
||||
updateQuestion: jest.fn().mockName('actions.problem.updateQuestion'),
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
problem: {
|
||||
question: jest.fn(state => ({ question: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Per https://github.com/tinymce/tinymce-react/issues/91 React unit testing in JSDOM is not supported by tinymce.
|
||||
// Consequently, mock the Editor out.
|
||||
jest.mock('@tinymce/tinymce-react', () => {
|
||||
const originalModule = jest.requireActual('@tinymce/tinymce-react');
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
Editor: () => 'TiNYmCE EDitOR',
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../hooks', () => ({
|
||||
prepareEditorRef: jest.fn(() => ({
|
||||
refReady: true,
|
||||
setEditorRef: jest.fn().mockName('hooks.prepareEditorRef.setEditorRef'),
|
||||
})),
|
||||
problemEditorConfig: jest.fn(args => ({ problemEditorConfig: args })),
|
||||
}));
|
||||
|
||||
describe('QuestionWidget', () => {
|
||||
const props = {
|
||||
question: 'This is my question',
|
||||
updateQuestion: jest.fn(),
|
||||
};
|
||||
describe('render', () => {
|
||||
test('snapshot: renders correct default', () => {
|
||||
expect(shallow(<QuestionWidget {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
test('question from problem.question', () => {
|
||||
expect(mapStateToProps(testState).question).toEqual(selectors.problem.question(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('updateField from actions.problem.updateQuestion', () => {
|
||||
expect(mapDispatchToProps.updateQuestion).toEqual(actions.problem.updateQuestion);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -284,7 +284,6 @@ export class OLXParser {
|
||||
the parsed OLX.
|
||||
*/
|
||||
const tagMap = {
|
||||
label: 'strong',
|
||||
description: 'em',
|
||||
};
|
||||
|
||||
|
||||
@@ -82,11 +82,11 @@ export const checkboxesOLXWithFeedbackAndHintsOLX = {
|
||||
},
|
||||
],
|
||||
},
|
||||
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><strong>Add the question text, or prompt, here. This text is required.</strong><em>You can add an optional tip or note related to the prompt like this.</em>',
|
||||
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><label>Add the question text, or prompt, here. This text is required.</label><em>You can add an optional tip or note related to the prompt like this.</em>',
|
||||
buildOLX: `<problem>
|
||||
<choiceresponse>
|
||||
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
|
||||
<strong>Add the question text, or prompt, here. This text is required.</strong>
|
||||
<label>Add the question text, or prompt, here. This text is required.</label>
|
||||
<em>You can add an optional tip or note related to the prompt like this.</em>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">
|
||||
@@ -160,11 +160,11 @@ export const dropdownOLXWithFeedbackAndHintsOLX = {
|
||||
},
|
||||
],
|
||||
},
|
||||
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><strong>Add the question text, or prompt, here. This text is required.</strong><em>You can add an optional tip or note related to the prompt like this.</em>',
|
||||
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><label>Add the question text, or prompt, here. This text is required.</label><em>You can add an optional tip or note related to the prompt like this.</em>',
|
||||
buildOLX: `<problem>
|
||||
<optionresponse>
|
||||
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
|
||||
<strong>Add the question text, or prompt, here. This text is required.</strong>
|
||||
<label>Add the question text, or prompt, here. This text is required.</label>
|
||||
<em>You can add an optional tip or note related to the prompt like this.</em>
|
||||
<optioninput>
|
||||
<option correct="false">
|
||||
@@ -233,11 +233,11 @@ export const mutlipleChoiceWithFeedbackAndHintsOLX = {
|
||||
},
|
||||
],
|
||||
},
|
||||
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for multiple choice with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><strong>Add the question text, or prompt, here. This text is required.</strong><em>You can add an optional tip or note related to the prompt like this.</em>',
|
||||
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for multiple choice with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><label>Add the question text, or prompt, here. This text is required.</label><em>You can add an optional tip or note related to the prompt like this.</em>',
|
||||
buildOLX: `<problem>
|
||||
<multiplechoiceresponse>
|
||||
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for multiple choice with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
|
||||
<strong>Add the question text, or prompt, here. This text is required.</strong>
|
||||
<label>Add the question text, or prompt, here. This text is required.</label>
|
||||
<em>You can add an optional tip or note related to the prompt like this.</em>
|
||||
<choicegroup>
|
||||
<choice correct="false">
|
||||
@@ -299,10 +299,10 @@ export const numericInputWithFeedbackAndHintsOLX = {
|
||||
},
|
||||
],
|
||||
},
|
||||
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><strong>Add the question text, or prompt, here. This text is required.</strong><em>You can add an optional tip or note related to the prompt like this.</em>',
|
||||
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><label>Add the question text, or prompt, here. This text is required.</label><em>You can add an optional tip or note related to the prompt like this.</em>',
|
||||
buildOLX: `<problem>
|
||||
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
|
||||
<strong>Add the question text, or prompt, here. This text is required.</strong>
|
||||
<label>Add the question text, or prompt, here. This text is required.</label>
|
||||
<em>You can add an optional tip or note related to the prompt like this.</em>
|
||||
<numericalresponse answer="100">
|
||||
<responseparam type="tolerance" default="5"></responseparam>
|
||||
@@ -373,11 +373,11 @@ export const textInputWithFeedbackAndHintsOLX = {
|
||||
},
|
||||
},
|
||||
},
|
||||
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><strong>Add the question text, or prompt, here. This text is required.</strong><em>You can add an optional tip or note related to the prompt like this.</em>',
|
||||
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><label>Add the question text, or prompt, here. This text is required.</label><em>You can add an optional tip or note related to the prompt like this.</em>',
|
||||
buildOLX: `<problem>
|
||||
<stringresponse answer="the correct answer" type="ci">
|
||||
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
|
||||
<strong>Add the question text, or prompt, here. This text is required.</strong>
|
||||
<label>Add the question text, or prompt, here. This text is required.</label>
|
||||
<em>You can add an optional tip or note related to the prompt like this.</em>
|
||||
<correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint>
|
||||
<additional_answer answer="optional acceptable variant of the correct answer"></additional_answer>
|
||||
@@ -452,11 +452,11 @@ export const textInputWithFeedbackAndHintsOLXWithMultipleAnswers = {
|
||||
},
|
||||
},
|
||||
},
|
||||
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><strong>Add the question text, or prompt, here. This text is required.</strong><em>You can add an optional tip or note related to the prompt like this.</em>',
|
||||
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><label>Add the question text, or prompt, here. This text is required.</label><em>You can add an optional tip or note related to the prompt like this.</em>',
|
||||
buildOLX: `<problem>
|
||||
<stringresponse answer="the correct answer" type="ci">
|
||||
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
|
||||
<strong>Add the question text, or prompt, here. This text is required.</strong>
|
||||
<label>Add the question text, or prompt, here. This text is required.</label>
|
||||
<em>You can add an optional tip or note related to the prompt like this.</em>
|
||||
<correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint>
|
||||
<additional_answer answer="300">
|
||||
@@ -531,10 +531,10 @@ export const numericInputWithFeedbackAndHintsOLXException = {
|
||||
},
|
||||
],
|
||||
},
|
||||
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><strong>Add the question text, or prompt, here. This text is required.</strong><em>You can add an optional tip or note related to the prompt like this.</em>',
|
||||
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><label>Add the question text, or prompt, here. This text is required.</label><em>You can add an optional tip or note related to the prompt like this.</em>',
|
||||
buildOLX: `<problem>
|
||||
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
|
||||
<strong>Add the question text, or prompt, here. This text is required.</strong>
|
||||
<label>Add the question text, or prompt, here. This text is required.</label>
|
||||
<em>You can add an optional tip or note related to the prompt like this.</em>
|
||||
<numericalresponse answer="300">
|
||||
<additional_answer answer="100">
|
||||
|
||||
@@ -3,15 +3,65 @@ import {
|
||||
} from 'react';
|
||||
import tinyMCEStyles from '../../data/constants/tinyMCEStyles';
|
||||
import { StrictDict } from '../../utils';
|
||||
import pluginConfig from '../TextEditor/pluginConfig';
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = StrictDict({
|
||||
refReady: (val) => useState(val),
|
||||
});
|
||||
|
||||
export const parseContentForLabels = ({ editor, updateQuestion }) => {
|
||||
let content = editor.getContent();
|
||||
const parsedLabels = content.split(/<label>|<\/label>/gm);
|
||||
let updatedContent;
|
||||
parsedLabels.forEach((label, i) => {
|
||||
let updatedLabel = label;
|
||||
if (!label.startsWith('<') && !label.endsWith('>')) {
|
||||
let previousLabel = parsedLabels[i - 1];
|
||||
let nextLabel = parsedLabels[i + 1];
|
||||
if (!previousLabel.endsWith('<p>')) {
|
||||
previousLabel = `${previousLabel}</p><p>`;
|
||||
updatedContent = content.replace(parsedLabels[i - 1], previousLabel);
|
||||
content = updatedContent;
|
||||
}
|
||||
if (previousLabel.endsWith('</p>') && !label.startWith('<p>')) {
|
||||
updatedLabel = `<p>${label}`;
|
||||
updatedContent = content.replace(label, updatedLabel);
|
||||
content = updatedContent;
|
||||
}
|
||||
if (!nextLabel.startsWith('</p>')) {
|
||||
nextLabel = `</p><p>${nextLabel}`;
|
||||
updatedContent = content.replace(parsedLabels[i + 1], nextLabel);
|
||||
content = updatedContent;
|
||||
}
|
||||
}
|
||||
});
|
||||
updateQuestion(content);
|
||||
};
|
||||
|
||||
export const setupCustomBehavior = ({ updateQuestion }) => (editor) => {
|
||||
// add a custom simple inline label formatter.
|
||||
const toggleLabelFormatting = () => {
|
||||
editor.execCommand('mceToggleFormat', false, 'label');
|
||||
};
|
||||
editor.ui.registry.addIcon('textToSpeech',
|
||||
'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.5 22C3.08333 22 2.72917 21.8542 2.4375 21.5625C2.14583 21.2708 2 20.9167 2 20.5V3.5C2 3.08333 2.14583 2.72917 2.4375 2.4375C2.72917 2.14583 3.08333 2 3.5 2H13L11.5 3.5H3.5V20.5H15.5V17H17V20.5C17 20.9167 16.8542 21.2708 16.5625 21.5625C16.2708 21.8542 15.9167 22 15.5 22H3.5ZM6 17.75V16.25H13V17.75H6ZM6 14.75V13.25H11V14.75H6ZM15.5 15L11.5 11H8V6H11.5L15.5 2V15ZM17 12.7V4.05C17.9333 4.4 18.6667 5.01667 19.2 5.9C19.7333 6.78333 20 7.65 20 8.5C20 9.35 19.7083 10.1917 19.125 11.025C18.5417 11.8583 17.8333 12.4167 17 12.7ZM17 16.25V14.7C18.1667 14.2833 19.2083 13.5333 20.125 12.45C21.0417 11.3667 21.5 10.05 21.5 8.5C21.5 6.95 21.0417 5.63333 20.125 4.55C19.2083 3.46667 18.1667 2.71667 17 2.3V0.75C18.7 1.2 20.125 2.1375 21.275 3.5625C22.425 4.9875 23 6.63333 23 8.5C23 10.3667 22.425 12.0125 21.275 13.4375C20.125 14.8625 18.7 15.8 17 16.25Z" fill="black"/></svg>');
|
||||
editor.ui.registry.addButton('customLabelButton', {
|
||||
icon: 'textToSpeech',
|
||||
text: 'Label',
|
||||
tooltip: 'Apply a "Question" label to specific text, recognized by screen readers. Recommended to improve accessibility.',
|
||||
onAction: toggleLabelFormatting,
|
||||
});
|
||||
editor.on('blur', () => {
|
||||
module.parseContentForLabels({
|
||||
editor,
|
||||
updateQuestion,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const problemEditorConfig = ({
|
||||
setEditorRef,
|
||||
editorRef,
|
||||
question,
|
||||
updateQuestion,
|
||||
}) => ({
|
||||
@@ -27,10 +77,10 @@ export const problemEditorConfig = ({
|
||||
branding: false,
|
||||
min_height: 150,
|
||||
placeholder: 'Enter your question',
|
||||
},
|
||||
onFocusOut: () => {
|
||||
const content = editorRef.current.getContent();
|
||||
updateQuestion(content);
|
||||
formats: { label: { inline: 'label' } },
|
||||
setup: module.setupCustomBehavior({ updateQuestion }),
|
||||
toolbar: `${pluginConfig().toolbar} | customLabelButton`,
|
||||
plugins: 'autoresize',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
135
src/editors/containers/ProblemEditor/hooks.test.js
Normal file
135
src/editors/containers/ProblemEditor/hooks.test.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import { useEffect } from 'react';
|
||||
import { MockUseState } from '../../../testUtils';
|
||||
|
||||
// import tinyMCE from '../../data/constants/tinyMCE';
|
||||
import { keyStore } from '../../utils';
|
||||
import pluginConfig from '../TextEditor/pluginConfig';
|
||||
import * as module from './hooks';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
createRef: jest.fn(val => ({ ref: val })),
|
||||
useRef: jest.fn(val => ({ current: val })),
|
||||
useEffect: jest.fn(),
|
||||
useCallback: (cb, prereqs) => ({ cb, prereqs }),
|
||||
}));
|
||||
|
||||
const state = new MockUseState(module);
|
||||
const moduleKeys = keyStore(module);
|
||||
|
||||
let hook;
|
||||
let output;
|
||||
|
||||
describe('Problem editor hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('state hooks', () => {
|
||||
state.testGetter(state.keys.refReady);
|
||||
});
|
||||
|
||||
describe('non-state hooks', () => {
|
||||
beforeEach(() => { state.mock(); });
|
||||
afterEach(() => { state.restore(); });
|
||||
|
||||
describe('setupCustomBehavior', () => {
|
||||
test('It calls addButton and addToggleButton in the editor, but openModal is not called', () => {
|
||||
const addButton = jest.fn();
|
||||
const addIcon = jest.fn();
|
||||
const updateQuestion = jest.fn();
|
||||
const editor = {
|
||||
ui: { registry: { addButton, addIcon } },
|
||||
on: jest.fn(),
|
||||
};
|
||||
const toggleLabelFormatting = expect.any(Function);
|
||||
output = module.setupCustomBehavior({ updateQuestion })(editor);
|
||||
expect(addIcon.mock.calls).toEqual([['textToSpeech',
|
||||
'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.5 22C3.08333 22 2.72917 21.8542 2.4375 21.5625C2.14583 21.2708 2 20.9167 2 20.5V3.5C2 3.08333 2.14583 2.72917 2.4375 2.4375C2.72917 2.14583 3.08333 2 3.5 2H13L11.5 3.5H3.5V20.5H15.5V17H17V20.5C17 20.9167 16.8542 21.2708 16.5625 21.5625C16.2708 21.8542 15.9167 22 15.5 22H3.5ZM6 17.75V16.25H13V17.75H6ZM6 14.75V13.25H11V14.75H6ZM15.5 15L11.5 11H8V6H11.5L15.5 2V15ZM17 12.7V4.05C17.9333 4.4 18.6667 5.01667 19.2 5.9C19.7333 6.78333 20 7.65 20 8.5C20 9.35 19.7083 10.1917 19.125 11.025C18.5417 11.8583 17.8333 12.4167 17 12.7ZM17 16.25V14.7C18.1667 14.2833 19.2083 13.5333 20.125 12.45C21.0417 11.3667 21.5 10.05 21.5 8.5C21.5 6.95 21.0417 5.63333 20.125 4.55C19.2083 3.46667 18.1667 2.71667 17 2.3V0.75C18.7 1.2 20.125 2.1375 21.275 3.5625C22.425 4.9875 23 6.63333 23 8.5C23 10.3667 22.425 12.0125 21.275 13.4375C20.125 14.8625 18.7 15.8 17 16.25Z" fill="black"/></svg>',
|
||||
]]);
|
||||
expect(addButton.mock.calls).toEqual([
|
||||
['customLabelButton', {
|
||||
icon: 'textToSpeech',
|
||||
text: 'Label',
|
||||
tooltip: 'Apply a "Question" label to specific text, recognized by screen readers. Recommended to improve accessibility.',
|
||||
onAction: toggleLabelFormatting,
|
||||
}],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseContentForLabels', () => {
|
||||
test('it calls getContent and setContent', () => {
|
||||
const editor = { getContent: jest.fn(() => '<p><label>Some question label</label></p><p>some content <label>around a label</label> followed by more text</p><img src="/static/soMEImagEURl1.jpeg"/>') };
|
||||
const updateQuestion = jest.fn();
|
||||
const content = '<p><label>Some question label</label></p><p>some content </p><p><label>around a label</label></p><p> followed by more text</p><img src="/static/soMEImagEURl1.jpeg"/>';
|
||||
module.parseContentForLabels({ editor, updateQuestion });
|
||||
expect(editor.getContent).toHaveBeenCalled();
|
||||
expect(updateQuestion).toHaveBeenCalledWith(content);
|
||||
});
|
||||
});
|
||||
|
||||
describe('problemEditorConfig', () => {
|
||||
const props = {
|
||||
question: '',
|
||||
};
|
||||
const evt = 'fakeEvent';
|
||||
const editor = 'myEditor';
|
||||
const setupCustomBehavior = args => ({ setupCustomBehavior: args });
|
||||
beforeEach(() => {
|
||||
props.setEditorRef = jest.fn();
|
||||
props.updateQuestion = jest.fn();
|
||||
jest.spyOn(module, moduleKeys.setupCustomBehavior)
|
||||
.mockImplementationOnce(setupCustomBehavior);
|
||||
output = module.problemEditorConfig(props);
|
||||
});
|
||||
test('It creates an onInit which calls setEditorRef', () => {
|
||||
output.onInit(evt, editor);
|
||||
expect(props.setEditorRef).toHaveBeenCalledWith(editor);
|
||||
});
|
||||
test('It sets the blockvalue to be empty string by default', () => {
|
||||
expect(output.initialValue).toBe('');
|
||||
});
|
||||
test('It sets the blockvalue to be the blockvalue if nonempty', () => {
|
||||
const questionText = 'SomE hTML content';
|
||||
output = module.problemEditorConfig({ ...props, question: questionText });
|
||||
expect(output.initialValue).toBe(questionText);
|
||||
});
|
||||
test('It configures plugins and toolbars correctly', () => {
|
||||
expect(output.init.plugins).toEqual('autoresize');
|
||||
expect(output.init.toolbar).toEqual(`${pluginConfig().toolbar} | customLabelButton`);
|
||||
});
|
||||
|
||||
it('calls setupCustomBehavior on setup', () => {
|
||||
expect(output.init.setup).toEqual(
|
||||
setupCustomBehavior({
|
||||
updateQuestion: props.updateQuestion,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareEditorRef', () => {
|
||||
beforeEach(() => {
|
||||
hook = module.prepareEditorRef();
|
||||
});
|
||||
const key = state.keys.refReady;
|
||||
test('sets refReady to false by default, ref is null', () => {
|
||||
expect(state.stateVals[key]).toEqual(false);
|
||||
expect(hook.editorRef.current).toBe(null);
|
||||
});
|
||||
test('when useEffect triggers, refReady is set to true', () => {
|
||||
expect(state.setState[key]).not.toHaveBeenCalled();
|
||||
const [cb, prereqs] = useEffect.mock.calls[0];
|
||||
expect(prereqs).toStrictEqual([state.setState[key]]);
|
||||
cb();
|
||||
expect(state.setState[key]).toHaveBeenCalledWith(true);
|
||||
});
|
||||
test('calling setEditorRef sets the ref value', () => {
|
||||
const fakeEditor = { editor: 'faKe Editor' };
|
||||
expect(hook.editorRef.current).not.toBe(fakeEditor);
|
||||
hook.setEditorRef.cb(fakeEditor);
|
||||
expect(hook.editorRef.current).toBe(fakeEditor);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
95
src/editors/containers/ProblemEditor/index.test.jsx
Normal file
95
src/editors/containers/ProblemEditor/index.test.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { thunkActions, selectors } from '../../data/redux';
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
import { ProblemEditor, mapStateToProps, mapDispatchToProps } from '.';
|
||||
|
||||
jest.mock('./components/EditProblemView', () => 'EditProblemView');
|
||||
jest.mock('./components/SelectTypeModal', () => 'SelectTypeModal');
|
||||
|
||||
jest.mock('react', () => {
|
||||
const updateState = jest.fn();
|
||||
return {
|
||||
...jest.requireActual('react'),
|
||||
updateState,
|
||||
useState: jest.fn(val => ([{ state: val }, jest.fn().mockName('setState')])),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../data/redux', () => ({
|
||||
thunkActions: {
|
||||
problem: {
|
||||
initializeProblemEditor: jest.fn().mockName('thunkActions.problem.initializeProblem'),
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
app: {
|
||||
blockValue: jest.fn(state => ({ blockValue: state })),
|
||||
},
|
||||
problem: {
|
||||
problemType: jest.fn(state => ({ problemType: state })),
|
||||
},
|
||||
requests: {
|
||||
isFinished: jest.fn((state, params) => ({ isFailed: { state, params } })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ProblemEditor', () => {
|
||||
const props = {
|
||||
onClose: jest.fn().mockName('props.onClose'),
|
||||
// redux
|
||||
problemType: null,
|
||||
blockValue: { data: { data: 'eDiTablE Text' } },
|
||||
blockFinished: false,
|
||||
studioViewFinished: false,
|
||||
initializeProblemEditor: jest.fn().mockName('args.intializeProblemEditor'),
|
||||
};
|
||||
describe('snapshots', () => {
|
||||
test('renders as expected with default behavior', () => {
|
||||
expect(shallow(<ProblemEditor {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
test('block not yet loaded, Spinner appears', () => {
|
||||
expect(shallow(<ProblemEditor {...props} blockFinished />)).toMatchSnapshot();
|
||||
});
|
||||
test('studio view not yet loaded, Spinner appears', () => {
|
||||
expect(shallow(<ProblemEditor {...props} studioViewFinished />)).toMatchSnapshot();
|
||||
});
|
||||
test('renders SelectTypeModal', () => {
|
||||
expect(shallow(<ProblemEditor {...props} blockFinished studioViewFinished />)).toMatchSnapshot();
|
||||
});
|
||||
test('renders EditProblemView', () => {
|
||||
expect(shallow(<ProblemEditor {...props} problemType="multiplechoiceresponse" blockFinished studioViewFinished />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
test('blockValue from app.blockValue', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).blockValue,
|
||||
).toEqual(selectors.app.blockValue(testState));
|
||||
});
|
||||
test('problemType from problem.problemType', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).problemType,
|
||||
).toEqual(selectors.problem.problemType(testState));
|
||||
});
|
||||
test('blockFinished from requests.isFinished', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).blockFinished,
|
||||
).toEqual(selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchBlock }));
|
||||
});
|
||||
test('studioViewFinished from requests.isFinished', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).studioViewFinished,
|
||||
).toEqual(selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchStudioView }));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('initializeProblemEditor from thunkActions.problem.initializeProblem', () => {
|
||||
expect(mapDispatchToProps.initializeProblemEditor).toEqual(thunkActions.problem.initializeProblem);
|
||||
});
|
||||
});
|
||||
});
|
||||
24
www/package-lock.json
generated
24
www/package-lock.json
generated
@@ -14,7 +14,7 @@
|
||||
"@edx/frontend-build": "^11.0.0",
|
||||
"@edx/frontend-lib-content-components": "file:..",
|
||||
"@edx/frontend-platform": "2.5.1",
|
||||
"@edx/paragon": "^20.27.0",
|
||||
"@edx/paragon": "^20.28.0",
|
||||
"core-js": "^3.21.1",
|
||||
"dotenv": "^16.0.0",
|
||||
"prop-types": "^15.5.10",
|
||||
@@ -57,9 +57,9 @@
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "^11.0.2",
|
||||
"@edx/frontend-platform": "2.4.0",
|
||||
"@edx/paragon": "^20.27.0",
|
||||
"@edx/paragon": "^20.28.0",
|
||||
"@testing-library/dom": "^8.13.0",
|
||||
"@testing-library/react": "12.1.1",
|
||||
"@testing-library/react": "^12.1.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"codecov": "3.8.3",
|
||||
"enzyme": "3.11.0",
|
||||
@@ -79,7 +79,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edx/frontend-platform": ">1.15.0",
|
||||
"@edx/paragon": "^20.21.0",
|
||||
"@edx/paragon": "^20.27.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0"
|
||||
@@ -1988,9 +1988,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/paragon": {
|
||||
"version": "20.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.27.0.tgz",
|
||||
"integrity": "sha512-jy62ZEBdAVlsP6tAm1/YDyMtc9fiD47H00whoW+y2Z+lLZqPsv6D5boIPQIcdBeg0W4f2gCU4TEy2+b2q8mYGA==",
|
||||
"version": "20.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.28.0.tgz",
|
||||
"integrity": "sha512-ydM3DK2aqdYRXyNEC0+P9tFpckH9kx/b/HtgSGrNMreRGAJ90QNkI/zAdlwj8U9mmoaE9jfIpjsO4g84qnrk1A==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||
@@ -22133,10 +22133,10 @@
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@edx/frontend-build": "^11.0.2",
|
||||
"@edx/frontend-platform": "2.4.0",
|
||||
"@edx/paragon": "^20.27.0",
|
||||
"@edx/paragon": "^20.28.0",
|
||||
"@reduxjs/toolkit": "^1.8.1",
|
||||
"@testing-library/dom": "^8.13.0",
|
||||
"@testing-library/react": "12.1.1",
|
||||
"@testing-library/react": "^12.1.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@tinymce/tinymce-react": "^3.14.0",
|
||||
"babel-polyfill": "6.26.0",
|
||||
@@ -22231,9 +22231,9 @@
|
||||
}
|
||||
},
|
||||
"@edx/paragon": {
|
||||
"version": "20.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.27.0.tgz",
|
||||
"integrity": "sha512-jy62ZEBdAVlsP6tAm1/YDyMtc9fiD47H00whoW+y2Z+lLZqPsv6D5boIPQIcdBeg0W4f2gCU4TEy2+b2q8mYGA==",
|
||||
"version": "20.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.28.0.tgz",
|
||||
"integrity": "sha512-ydM3DK2aqdYRXyNEC0+P9tFpckH9kx/b/HtgSGrNMreRGAJ90QNkI/zAdlwj8U9mmoaE9jfIpjsO4g84qnrk1A==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"@edx/frontend-build": "^11.0.0",
|
||||
"@edx/frontend-lib-content-components": "file:..",
|
||||
"@edx/frontend-platform": "2.5.1",
|
||||
"@edx/paragon": "^20.27.0",
|
||||
"@edx/paragon": "^20.28.0",
|
||||
"core-js": "^3.21.1",
|
||||
"dotenv": "^16.0.0",
|
||||
"prop-types": "^15.5.10",
|
||||
|
||||
Reference in New Issue
Block a user