feat: add label button to question tinyMCE (#221)

This commit is contained in:
Kristin Aoki
2023-02-01 11:35:15 -05:00
committed by GitHub
parent 09d5ce35f3
commit 83d45a249a
13 changed files with 473 additions and 43 deletions

14
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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>
`;

View File

@@ -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>
`;

View File

@@ -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,
})

View File

@@ -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);
});
});
});

View File

@@ -284,7 +284,6 @@ export class OLXParser {
the parsed OLX.
*/
const tagMap = {
label: 'strong',
description: 'em',
};

View File

@@ -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">

View File

@@ -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',
},
});

View 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);
});
});
});
});

View 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
View File

@@ -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",

View File

@@ -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",