Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,31 @@
import React from 'react';

type ProblemEditorRef = React.MutableRefObject<unknown> | React.RefObject<unknown> | null;

export interface ProblemEditorContextValue {
editorRef: ProblemEditorRef;
}

export type ProblemEditorContextInit = {
editorRef?: ProblemEditorRef;
};

const context = React.createContext<ProblemEditorContextValue | undefined>(undefined);

export function useProblemEditorContext() {
const ctx = React.useContext(context);
if (ctx === undefined) {
/* istanbul ignore next */
throw new Error('This component needs to be wrapped in <ProblemEditorContextProvider>');
}
return ctx;
}

export const ProblemEditorContextProvider: React.FC<{ children: React.ReactNode; } & ProblemEditorContextInit> = ({
children,
editorRef = null,
}) => {
const ctx: ProblemEditorContextValue = React.useMemo(() => ({ editorRef }), [editorRef]);

return <context.Provider value={ctx}>{children}</context.Provider>;
};
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
Expand Up @@ -169,13 +169,13 @@ const messages = defineMessages({
},
'ConfirmSwitchMessage-advanced': {
id: 'authoring.problemeditor.settings.switchtoeditor.ConfirmSwitchMessage.advanced',
defaultMessage: 'If you use the advanced editor, this problem will be converted to OLX and you will not be able to return to the simple editor.',
defaultMessage: 'If you use the advanced editor, this problem will be converted to OLX. Depending on what edits you make to the OLX, you may not be able to return to the simple editor.',
description: 'message to confirm that a user wants to use the advanced editor',
},
'ConfirmSwitchMessage-markdown': {
id: 'authoring.problemeditor.settings.switchtoeditor.ConfirmSwitchMessage.markdown',
defaultMessage: 'If you use the markdown editor, this problem will be converted to markdown and you will not be able to return to the simple editor.',
description: 'message to confirm that a user wants to use the advanced editor',
defaultMessage: 'Some edits that are possible with the markdown editor are not supported by the simple editor, so you may not be able to change back to the simple editor.',
description: 'message to confirm that a user wants to use the markdown editor',
},
'ConfirmSwitchMessageTitle-advanced': {
id: 'authoring.problemeditor.settings.switchtoeditor.ConfirmSwitchMessageTitle.advanced',
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