Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { createContext, useContext, useMemo } from 'react';
import PropTypes from 'prop-types';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prop-types is deprecated. Since this is a new file, can you please use TypeScript .tsx and avoid propTypes or defaultProps?


const ProblemEditorContext = createContext({
editorRef: null,
});

export const ProblemEditorContextProvider = ({ editorRef, children }) => {
const contextValue = useMemo(() => ({ editorRef }), [editorRef]);

return (
<ProblemEditorContext.Provider value={contextValue}>
{children}
</ProblemEditorContext.Provider>
);
};

ProblemEditorContextProvider.propTypes = {
children: PropTypes.node.isRequired,
// eslint-disable-next-line react/forbid-prop-types
editorRef: PropTypes.object,
};

ProblemEditorContextProvider.defaultProps = {
editorRef: null,
};

export const useProblemEditorContext = () => useContext(ProblemEditorContext);

export default ProblemEditorContext;
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const SettingsWidget = ({
isMarkdownEditorEnabledForContext,
} = useEditorContext();
const rawMarkdown = useSelector(selectors.problem.rawMarkdown);
const isMarkdownEditorEnabled = useSelector(selectors.problem.isMarkdownEditorEnabled);
const showMarkdownEditorButton = isMarkdownEditorEnabledForContext && rawMarkdown;
const { isAdvancedCardsVisible, showAdvancedCards } = showAdvancedSettingsCards();
const feedbackCard = () => {
Expand Down Expand Up @@ -161,7 +162,7 @@ const SettingsWidget = ({
<div className="my-3">
<SwitchEditorCard problemType={problemType} editorType="advanced" />
</div>
{ showMarkdownEditorButton
{ (showMarkdownEditorButton && !isMarkdownEditorEnabled) // Only show button if not already in markdown editor
&& (
<div className="my-3">
<SwitchEditorCard problemType={problemType} editorType="markdown" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import React from 'react';

import { ProblemTypeKeys } from '@src/editors/data/constants/problem';
import { screen, initializeMocks } from '@src/testUtils';
import { editorRender } from '@src/editors/editorTestRender';
import { editorRender, type PartialEditorState } from '@src/editors/editorTestRender';
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
import * as hooks from './hooks';
import { SettingsWidgetInternal as SettingsWidget } from '.';
import { ProblemEditorContextProvider } from '../ProblemEditorContext';

jest.mock('./settingsComponents/GeneralFeedback', () => 'GeneralFeedback');
jest.mock('./settingsComponents/GroupFeedback', () => 'GroupFeedback');
Expand All @@ -23,7 +24,6 @@ describe('SettingsWidget', () => {
const showAdvancedSettingsCardsBaseProps = {
isAdvancedCardsVisible: false,
showAdvancedCards: jest.fn().mockName('showAdvancedSettingsCards.showAdvancedCards'),
setResetTrue: jest.fn().mockName('showAdvancedSettingsCards.setResetTrue'),
};

const props = {
Expand All @@ -49,22 +49,34 @@ describe('SettingsWidget', () => {

};

const editorRef = { current: null };

const renderSettingsWidget = (
overrideProps = {},
options = {},
) => editorRender(
<ProblemEditorContextProvider editorRef={editorRef}>
<SettingsWidget {...props} {...overrideProps} />
</ProblemEditorContextProvider>,
options,
);

beforeEach(() => {
initializeMocks();
});

describe('behavior', () => {
it('calls showAdvancedSettingsCards when initialized', () => {
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsBaseProps);
editorRender(<SettingsWidget {...props} />);
renderSettingsWidget();
expect(hooks.showAdvancedSettingsCards).toHaveBeenCalled();
});
});

describe('renders', () => {
test('renders Settings widget page', () => {
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsBaseProps);
editorRender(<SettingsWidget {...props} />);
renderSettingsWidget();
expect(screen.getByText('Show advanced settings')).toBeInTheDocument();
});

Expand All @@ -74,7 +86,7 @@ describe('SettingsWidget', () => {
isAdvancedCardsVisible: true,
};
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsProps);
const { container } = editorRender(<SettingsWidget {...props} />);
const { container } = renderSettingsWidget();
expect(screen.queryByText('Show advanced settings')).not.toBeInTheDocument();
expect(container.querySelector('showanswercard')).toBeInTheDocument();
expect(container.querySelector('resetcard')).toBeInTheDocument();
Expand All @@ -86,12 +98,49 @@ describe('SettingsWidget', () => {
isAdvancedCardsVisible: true,
};
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsProps);
const { container } = editorRender(
<SettingsWidget {...props} problemType={ProblemTypeKeys.ADVANCED} />,
);
const { container } = renderSettingsWidget({ problemType: ProblemTypeKeys.ADVANCED });
expect(container.querySelector('randomization')).toBeInTheDocument();
});
});
describe('SwitchEditorCard rendering (markdown vs advanced)', () => {
test('shows two SwitchEditorCard components when markdown is available and not currently enabled', () => {
const showAdvancedSettingsCardsProps = {
...showAdvancedSettingsCardsBaseProps,
isAdvancedCardsVisible: true,
};
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsProps);
const modifiedInitialState: PartialEditorState = {
problem: {
problemType: null, // non-advanced problem
isMarkdownEditorEnabled: false, // currently in advanced/raw (or standard) editor
rawOLX: '<problem></problem>',
rawMarkdown: '## Problem', // markdown content exists so button should appear
isDirty: false,
},
};
const { container } = renderSettingsWidget({}, { initialState: modifiedInitialState });
expect(container.querySelectorAll('switcheditorcard')).toHaveLength(2);
});

test('shows only the advanced SwitchEditorCard when already in markdown mode', () => {
const showAdvancedSettingsCardsProps = {
...showAdvancedSettingsCardsBaseProps,
isAdvancedCardsVisible: true,
};
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsProps);
const modifiedInitialState: PartialEditorState = {
problem: {
problemType: null,
isMarkdownEditorEnabled: true, // already in markdown editor, so markdown button hidden
rawOLX: '<problem></problem>',
rawMarkdown: '## Problem',
isDirty: false,
},
};
const { container } = renderSettingsWidget({}, { initialState: modifiedInitialState });
expect(container.querySelectorAll('switcheditorcard')).toHaveLength(1);
});
});

describe('isLibrary', () => {
const libraryProps = {
Expand All @@ -100,7 +149,7 @@ describe('SettingsWidget', () => {
};
test('renders Settings widget page', () => {
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsBaseProps);
const { container } = editorRender(<SettingsWidget {...libraryProps} />);
const { container } = renderSettingsWidget(libraryProps);
expect(container.querySelector('timercard')).not.toBeInTheDocument();
expect(container.querySelector('resetcard')).not.toBeInTheDocument();
expect(container.querySelector('typecard')).toBeInTheDocument();
Expand All @@ -114,7 +163,7 @@ describe('SettingsWidget', () => {
isAdvancedCardsVisible: true,
};
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsProps);
const { container } = editorRender(<SettingsWidget {...libraryProps} />);
const { container } = renderSettingsWidget(libraryProps);
expect(screen.queryByText('Show advanced settings')).not.toBeInTheDocument();
expect(container.querySelector('showanswearscard')).not.toBeInTheDocument();
expect(container.querySelector('resetcard')).not.toBeInTheDocument();
Expand All @@ -128,7 +177,7 @@ describe('SettingsWidget', () => {
isAdvancedCardsVisible: true,
};
jest.spyOn(hooks, 'showAdvancedSettingsCards').mockReturnValue(showAdvancedSettingsCardsProps);
const { container } = editorRender(<SettingsWidget {...libraryProps} problemType={ProblemTypeKeys.ADVANCED} />);
const { container } = renderSettingsWidget({ ...libraryProps, problemType: ProblemTypeKeys.ADVANCED });
expect(container.querySelector('randomization')).toBeInTheDocument();
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Card } from '@openedx/paragon';
import PropTypes from 'prop-types';
import { useEditorContext } from '@src/editors/EditorContext';
import { selectors, thunkActions } from '@src/editors/data/redux';
import { thunkActions } from '@src/editors/data/redux';
import BaseModal from '@src/editors/sharedComponents/BaseModal';
import Button from '@src/editors/sharedComponents/Button';
import { ProblemTypeKeys } from '@src/editors/data/constants/problem';
import messages from '../messages';
import { handleConfirmEditorSwitch } from '../hooks';
import { useProblemEditorContext } from '../../ProblemEditorContext';

const SwitchEditorCard = ({
editorType,
problemType,
}) => {
const [isConfirmOpen, setConfirmOpen] = React.useState(false);
const { isMarkdownEditorEnabledForContext } = useEditorContext();
const isMarkdownEditorEnabled = useSelector(selectors.problem.isMarkdownEditorEnabled);
const dispatch = useDispatch();

const isMarkdownEditorActive = isMarkdownEditorEnabled && isMarkdownEditorEnabledForContext;
if (isMarkdownEditorActive || problemType === ProblemTypeKeys.ADVANCED) { return null; }
const { editorRef } = useProblemEditorContext();
if (problemType === ProblemTypeKeys.ADVANCED) { return null; }

return (
<Card className="border border-light-700 shadow-none">
Expand All @@ -33,7 +30,7 @@ const SwitchEditorCard = ({
<Button
/* istanbul ignore next */
onClick={() => handleConfirmEditorSwitch({
switchEditor: () => dispatch(thunkActions.problem.switchEditor(editorType)),
switchEditor: () => dispatch(thunkActions.problem.switchEditor(editorType, editorRef)),
setConfirmOpen,
})}
variant="primary"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event';
import { editorRender } from '@src/editors/editorTestRender';
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
import { thunkActions } from '@src/editors/data/redux';
import { ProblemEditorContextProvider } from '../../ProblemEditorContext';
import SwitchEditorCard from './SwitchEditorCard';

const switchEditorSpy = jest.spyOn(thunkActions.problem, 'switchEditor');
Expand All @@ -13,6 +14,13 @@ describe('SwitchEditorCard - markdown', () => {
problemType: 'stringresponse',
editorType: 'markdown',
};
const editorRef = { current: null };

const renderSwitchEditorCard = (overrideProps = {}) => editorRender(
<ProblemEditorContextProvider editorRef={editorRef}>
<SwitchEditorCard {...baseProps} {...overrideProps} />
</ProblemEditorContextProvider>,
);

beforeEach(() => {
initializeMocks();
Expand All @@ -23,7 +31,7 @@ describe('SwitchEditorCard - markdown', () => {
mockWaffleFlags({ useReactMarkdownEditor: true });
// The markdown editor is not currently active (default)

editorRender(<SwitchEditorCard {...baseProps} />);
renderSwitchEditorCard();
const user = userEvent.setup();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
const switchButton = screen.getByRole('button', { name: 'Switch to markdown editor' });
Expand All @@ -38,7 +46,7 @@ describe('SwitchEditorCard - markdown', () => {
mockWaffleFlags({ useReactMarkdownEditor: true });
// The markdown editor is not currently active (default)

editorRender(<SwitchEditorCard {...baseProps} />);
renderSwitchEditorCard();
const user = userEvent.setup();
const switchButton = screen.getByRole('button', { name: 'Switch to markdown editor' });
expect(switchButton).toBeInTheDocument();
Expand All @@ -49,28 +57,12 @@ describe('SwitchEditorCard - markdown', () => {
expect(confirmButton).toBeInTheDocument();
expect(switchEditorSpy).not.toHaveBeenCalled();
await user.click(confirmButton);
expect(switchEditorSpy).toHaveBeenCalledWith('markdown');
expect(switchEditorSpy).toHaveBeenCalledWith('markdown', editorRef);
// Markdown editor would now be active.
});

test('renders nothing for advanced problemType', () => {
const { container } = editorRender(<SwitchEditorCard {...baseProps} problemType="advanced" />);
const reduxWrapper = (container.firstChild as HTMLElement | null);
expect(reduxWrapper?.innerHTML).toBe('');
});

test('returns null when editor is already Markdown', () => {
// Markdown Editor support is on for this course:
mockWaffleFlags({ useReactMarkdownEditor: true });
// The markdown editor *IS* currently active (default)

const { container } = editorRender(<SwitchEditorCard {...baseProps} />, {
initialState: {
problem: {
isMarkdownEditorEnabled: true,
},
},
});
const { container } = renderSwitchEditorCard({ problemType: 'advanced' });
const reduxWrapper = (container.firstChild as HTMLElement | null);
expect(reduxWrapper?.innerHTML).toBe('');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ export const parseState = ({
return {
settings: {
...settings,
...(isMarkdownEditorEnabled && { markdown: contentString }),
// If the save action isn’t triggered from the Markdown editor, the Markdown content might be outdated. Since the
// Markdown editor shouldn't be displayed in future in this case, we’re sending `null` instead.
// TODO: Implement OLX-to-Markdown conversion to properly handle this scenario.
markdown: isMarkdownEditorEnabled ? contentString : null,
markdown_edited: isMarkdownEditorEnabled,
},
olx: isAdvanced || isMarkdownEditorEnabled ? rawOLX : reactBuiltOlx,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ describe('EditProblemView hooks parseState', () => {
assets: {},
})();
expect(res.olx).toBe(mockRawOLX);
expect(res.settings.markdown).toBe(null);
});
it('markdown problem', () => {
const res = hooks.parseState({
Expand Down Expand Up @@ -306,13 +307,16 @@ describe('EditProblemView hooks parseState', () => {
show_reset_button: false,
submission_wait_seconds: 0,
attempts_before_showanswer_button: 0,
markdown: null,
markdown_edited: false,
};
const openSaveWarningModal = jest.fn();

it('default visual save and returns parseState data', () => {
const problem = { ...problemState, problemType: ProblemTypeKeys.NUMERIC, answers: [{ id: 'A', title: 'problem', correct: true }] };
const content = hooks.getContent({
isAdvancedProblemType: false,
isMarkdownEditorEnabled: false,
problemState: problem,
editorRef,
assets,
Expand All @@ -339,6 +343,7 @@ describe('EditProblemView hooks parseState', () => {
};
const { settings } = hooks.getContent({
isAdvancedProblemType: false,
isMarkdownEditorEnabled: false,
problemState: problem,
editorRef,
assets,
Expand All @@ -353,12 +358,15 @@ describe('EditProblemView hooks parseState', () => {
attempts_before_showanswer_button: 0,
submission_wait_seconds: 0,
weight: 1,
markdown: null,
markdown_edited: false,
});
});

it('default advanced save and returns parseState data', () => {
const content = hooks.getContent({
isAdvancedProblemType: true,
isMarkdownEditorEnabled: false,
problemState,
editorRef,
assets,
Expand Down
Loading