test: Clean up editor tests (#2343)
* test: improve the editorRender helper * fix: redux state bug introduced in #2326 * test: add note for future reference about accessing the editor redux store
This commit is contained in:
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import * as reactRedux from 'react-redux';
|
||||
import * as hooks from './hooks';
|
||||
import VideoSelector from './VideoSelector';
|
||||
import editorRender from './editorTestRender';
|
||||
import { editorRender } from './editorTestRender';
|
||||
import { initializeMocks, screen } from '../testUtils';
|
||||
|
||||
const defaultProps = {
|
||||
|
||||
@@ -37,8 +37,7 @@ const EditProblemView = ({ returnFunction }) => {
|
||||
const lmsEndpointUrl = useSelector(selectors.app.lmsEndpointUrl);
|
||||
const returnUrl = useSelector(selectors.app.returnUrl);
|
||||
const problemType = useSelector(selectors.problem.problemType);
|
||||
const problemState = useSelector(selectors.problem.completeState)?.completeState;
|
||||
const problemStateWithoutComplete = useSelector(selectors.problem.completeState);
|
||||
const problemState = useSelector(selectors.problem.completeState);
|
||||
const isDirty = useSelector(selectors.problem.isDirty);
|
||||
|
||||
const isMarkdownEditorEnabledSelector = useSelector(selectors.problem.isMarkdownEditorEnabled);
|
||||
@@ -60,7 +59,7 @@ const EditProblemView = ({ returnFunction }) => {
|
||||
return (
|
||||
<EditorContainer
|
||||
getContent={() => getContent({
|
||||
problemState: problemStateWithoutComplete,
|
||||
problemState,
|
||||
openSaveWarningModal,
|
||||
isAdvancedProblemType,
|
||||
isMarkdownEditorEnabled,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import { screen, fireEvent, initializeMocks } from '../../../../../testUtils';
|
||||
import editorRender from '../../../../editorTestRender';
|
||||
import { screen, fireEvent, initializeMocks } from '@src/testUtils';
|
||||
import { editorRender, type PartialEditorState } from '@src/editors/editorTestRender';
|
||||
import { ProblemTypeKeys } from '@src/editors/data/constants/problem';
|
||||
import EditProblemView from './index';
|
||||
import { initializeStore } from '../../../../data/redux';
|
||||
import { ProblemTypeKeys } from '../../../../data/constants/problem';
|
||||
|
||||
const { saveBlock } = require('../../../../hooks');
|
||||
const { saveWarningModalToggle } = require('./hooks');
|
||||
@@ -41,20 +40,16 @@ jest.mock('./hooks', () => ({
|
||||
}));
|
||||
|
||||
// 🗂️ Initial state based on baseProps
|
||||
const initialState = {
|
||||
const initialState: PartialEditorState = {
|
||||
app: {
|
||||
analytics: {},
|
||||
lmsEndpointUrl: null,
|
||||
returnUrl: '/return',
|
||||
isMarkdownEditorEnabledForCourse: false,
|
||||
},
|
||||
problem: {
|
||||
problemType: 'standard',
|
||||
problemType: null,
|
||||
isMarkdownEditorEnabled: false,
|
||||
completeState: {
|
||||
rawOLX: '<problem></problem>',
|
||||
rawMarkdown: '## Problem',
|
||||
},
|
||||
rawOLX: '<problem></problem>',
|
||||
rawMarkdown: '## Problem',
|
||||
isDirty: false,
|
||||
},
|
||||
};
|
||||
@@ -63,11 +58,11 @@ describe('EditProblemView', () => {
|
||||
const returnFunction = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
initializeMocks({ initialState, initializeStore });
|
||||
initializeMocks();
|
||||
});
|
||||
|
||||
it('renders standard problem widgets', () => {
|
||||
editorRender(<EditProblemView returnFunction={returnFunction} />);
|
||||
editorRender(<EditProblemView returnFunction={returnFunction} />, { initialState });
|
||||
expect(screen.getByText('QuestionWidget')).toBeInTheDocument();
|
||||
expect(screen.getByText('ExplanationWidget')).toBeInTheDocument();
|
||||
expect(screen.getByText('AnswerWidget')).toBeInTheDocument();
|
||||
@@ -77,14 +72,6 @@ describe('EditProblemView', () => {
|
||||
});
|
||||
|
||||
it('renders advanced problem with RawEditor', () => {
|
||||
initializeMocks({
|
||||
initializeStore,
|
||||
...initialState,
|
||||
problem: {
|
||||
...initialState.problem,
|
||||
problemType: ProblemTypeKeys.ADVANCED,
|
||||
},
|
||||
});
|
||||
editorRender(<EditProblemView returnFunction={returnFunction} />, {
|
||||
initialState: {
|
||||
...initialState,
|
||||
@@ -99,27 +86,19 @@ describe('EditProblemView', () => {
|
||||
});
|
||||
|
||||
it('renders markdown editor with RawEditor', () => {
|
||||
const modifiedInitialState = {
|
||||
const modifiedInitialState: PartialEditorState = {
|
||||
app: {
|
||||
analytics: {},
|
||||
lmsEndpointUrl: null,
|
||||
returnUrl: '/return',
|
||||
isMarkdownEditorEnabledForCourse: true,
|
||||
},
|
||||
problem: {
|
||||
problemType: 'standard',
|
||||
problemType: null,
|
||||
isMarkdownEditorEnabled: true,
|
||||
completeState: {
|
||||
rawOLX: '<problem></problem>',
|
||||
rawMarkdown: '## Problem',
|
||||
},
|
||||
rawOLX: '<problem></problem>',
|
||||
rawMarkdown: '## Problem',
|
||||
isDirty: false,
|
||||
},
|
||||
};
|
||||
initializeMocks({
|
||||
initializeStore,
|
||||
initialState: modifiedInitialState,
|
||||
});
|
||||
editorRender(<EditProblemView returnFunction={returnFunction} />, { initialState: modifiedInitialState });
|
||||
expect(screen.getByText('markdown:## Problem')).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
screen, fireEvent, initializeMocks,
|
||||
} from '../../../../../../testUtils';
|
||||
import editorRender from '../../../../../editorTestRender';
|
||||
} from '@src/testUtils';
|
||||
import { editorRender } from '@src/editors/editorTestRender';
|
||||
import SelectTypeWrapper from './index';
|
||||
import * as hooks from '../hooks';
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import {
|
||||
fireEvent,
|
||||
screen,
|
||||
initializeMocks,
|
||||
} from '../../../../../testUtils';
|
||||
import editorRender from '../../../../editorTestRender';
|
||||
} from '@src/testUtils';
|
||||
import { editorRender } from '@src/editors/editorTestRender';
|
||||
import * as hooks from './hooks';
|
||||
import SelectTypeModal from '.';
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import { screen, initializeMocks } from '../../../testUtils';
|
||||
import editorRender from '../../editorTestRender';
|
||||
import { initializeStore } from '../../data/redux';
|
||||
import { screen, initializeMocks } from '@src/testUtils';
|
||||
import { editorRender, type PartialEditorState } from '@src/editors/editorTestRender';
|
||||
import { thunkActions } from '@src/editors/data/redux';
|
||||
|
||||
import ProblemEditor from './index';
|
||||
import messages from './messages';
|
||||
|
||||
// Mock child components for easy selection
|
||||
jest.mock('./components/SelectTypeModal', () => function mockSelectTypeModal(props: any) {
|
||||
return <div>SelectTypeModal {props.onClose && 'withOnClose'}</div>;
|
||||
@@ -11,18 +13,10 @@ jest.mock('./components/SelectTypeModal', () => function mockSelectTypeModal(pro
|
||||
jest.mock('./components/EditProblemView', () => function mockEditProblemView(props: any) {
|
||||
return <div>EditProblemView {props.onClose && 'withOnClose'} {props.returnFunction && 'withReturnFunction'}</div>;
|
||||
});
|
||||
|
||||
jest.mock('../../data/redux', () => ({
|
||||
__esModule: true,
|
||||
...jest.requireActual('../../data/redux'),
|
||||
thunkActions: {
|
||||
...jest.requireActual('../../data/redux').thunkActions,
|
||||
problem: {
|
||||
...jest.requireActual('../../data/redux').thunkActions.problem,
|
||||
initializeProblem: jest.fn(() => () => Promise.resolve()),
|
||||
},
|
||||
},
|
||||
}));
|
||||
// Mock the initializeProblem method:
|
||||
jest.spyOn(thunkActions.problem, 'initializeProblem').mockImplementation(
|
||||
() => () => Promise.resolve(),
|
||||
);
|
||||
|
||||
describe('ProblemEditor', () => {
|
||||
const baseProps = {
|
||||
@@ -31,76 +25,67 @@ describe('ProblemEditor', () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
initializeMocks();
|
||||
});
|
||||
|
||||
it('renders Spinner when blockFinished is false', () => {
|
||||
const initialState = {
|
||||
app: { shouldCreateBlock: false },
|
||||
problem: { problemType: 'standard' },
|
||||
const initialState: PartialEditorState = {
|
||||
app: {
|
||||
blockId: 'problem1',
|
||||
blockType: 'problem',
|
||||
},
|
||||
problem: { problemType: 'multiplechoiceresponse' },
|
||||
requests: {
|
||||
fetchBlock: { status: 'completed' },
|
||||
fetchAdvancedSettings: { status: 'pending' },
|
||||
|
||||
fetchBlock: { status: 'pending' },
|
||||
fetchAdvancedSettings: { status: 'completed' },
|
||||
},
|
||||
};
|
||||
initializeMocks({
|
||||
initializeStore,
|
||||
initialState,
|
||||
});
|
||||
|
||||
const { container } = editorRender(<ProblemEditor {...baseProps} />, { initialState });
|
||||
const spinner = container.querySelector('.pgn__spinner');
|
||||
expect(spinner).toBeInTheDocument();
|
||||
expect(spinner).toHaveAttribute('screenreadertext', 'Loading Problem Editor');
|
||||
editorRender(<ProblemEditor {...baseProps} />, { initialState });
|
||||
const spinnerText = screen.getByText('Loading Problem Editor');
|
||||
expect(spinnerText.parentElement).toHaveClass('pgn__spinner');
|
||||
});
|
||||
|
||||
it('renders Spinner when advancedSettingsFinished is false', () => {
|
||||
const initialState = {
|
||||
app: { shouldCreateBlock: false },
|
||||
const initialState: PartialEditorState = {
|
||||
app: {
|
||||
blockId: 'problem1',
|
||||
blockType: 'problem',
|
||||
},
|
||||
problem: { problemType: null },
|
||||
requests: {
|
||||
fetchBlock: { status: 'pending' },
|
||||
fetchAdvancedSettings: { status: 'completed' },
|
||||
},
|
||||
};
|
||||
initializeMocks({
|
||||
initializeStore,
|
||||
initialState,
|
||||
});
|
||||
|
||||
const { container } = editorRender(<ProblemEditor {...baseProps} />, { initialState });
|
||||
const spinner = container.querySelector('.pgn__spinner');
|
||||
expect(spinner).toBeInTheDocument();
|
||||
expect(spinner).toHaveAttribute('screenreadertext', 'Loading Problem Editor');
|
||||
editorRender(<ProblemEditor {...baseProps} />, { initialState });
|
||||
const spinnerText = screen.getByText('Loading Problem Editor');
|
||||
expect(spinnerText.parentElement).toHaveClass('pgn__spinner');
|
||||
});
|
||||
|
||||
it('renders block failed message when blockFailed is true', () => {
|
||||
const initialState = {
|
||||
const initialState: PartialEditorState = {
|
||||
app: {
|
||||
blockId: '',
|
||||
blockType: true,
|
||||
blockId: 'problem1',
|
||||
blockType: 'problem',
|
||||
},
|
||||
problem: { problemType: 'standard' },
|
||||
problem: { problemType: 'multiplechoiceresponse' },
|
||||
requests: {
|
||||
fetchBlock: { status: 'failed' },
|
||||
fetchAdvancedSettings: { status: 'completed' },
|
||||
},
|
||||
};
|
||||
|
||||
initializeMocks({
|
||||
initializeStore,
|
||||
initialState,
|
||||
});
|
||||
editorRender(<ProblemEditor {...baseProps} />, { initialState });
|
||||
expect(screen.getByText(messages.blockFailed.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders SelectTypeModal when problemType is null', () => {
|
||||
const initialState = {
|
||||
const initialState: PartialEditorState = {
|
||||
app: {
|
||||
blockId: '',
|
||||
blockType: true,
|
||||
blockId: 'problem1',
|
||||
blockType: 'problem',
|
||||
},
|
||||
problem: { problemType: null },
|
||||
requests: {
|
||||
@@ -109,19 +94,15 @@ describe('ProblemEditor', () => {
|
||||
|
||||
},
|
||||
};
|
||||
initializeMocks({
|
||||
initializeStore,
|
||||
initialState,
|
||||
});
|
||||
editorRender(<ProblemEditor {...baseProps} />, { initialState });
|
||||
expect(screen.getByText(/SelectTypeModal/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders EditProblemView when problemType is not null', () => {
|
||||
const initialState = {
|
||||
const initialState: PartialEditorState = {
|
||||
app: {
|
||||
blockId: '',
|
||||
blockType: true,
|
||||
blockId: 'problem1',
|
||||
blockType: 'problem',
|
||||
},
|
||||
problem: { problemType: 'advanced' },
|
||||
requests: {
|
||||
@@ -130,10 +111,6 @@ describe('ProblemEditor', () => {
|
||||
},
|
||||
|
||||
};
|
||||
initializeMocks({
|
||||
initializeStore,
|
||||
initialState,
|
||||
});
|
||||
|
||||
editorRender(<ProblemEditor {...baseProps} />, { initialState });
|
||||
expect(screen.getByText(/EditProblemView/)).toBeInTheDocument();
|
||||
|
||||
@@ -42,7 +42,7 @@ const ProblemEditor: React.FC<Props> = ({
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="m-3"
|
||||
screenreadertext="Loading Problem Editor"
|
||||
screenReaderText="Loading Problem Editor"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ export const createStore = () => {
|
||||
|
||||
const middleware = [thunkMiddleware, loggerMiddleware];
|
||||
|
||||
const store = redux.createStore<EditorState, any, any, any>(
|
||||
const store: redux.Store<EditorState> = redux.createStore<EditorState, any, any, any>(
|
||||
reducer as any,
|
||||
composeWithDevToolsLogOnlyInProduction(redux.applyMiddleware(...middleware)),
|
||||
);
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { render as baseRender } from '../testUtils';
|
||||
import { render as baseRender, WrapperOptions } from '../testUtils';
|
||||
import { EditorContextProvider } from './EditorContext';
|
||||
import { initializeStore } from './data/redux'; // adjust path if needed
|
||||
import { type EditorState, initializeStore } from './data/redux'; // adjust path if needed
|
||||
|
||||
type RecursivePartial<T> = {
|
||||
[P in keyof T]?: RecursivePartial<T[P]>;
|
||||
};
|
||||
|
||||
export type PartialEditorState = RecursivePartial<EditorState>;
|
||||
|
||||
/**
|
||||
* Custom render function for testing React components with the editor context and Redux store.
|
||||
@@ -10,22 +16,21 @@ import { initializeStore } from './data/redux'; // adjust path if needed
|
||||
* Wraps the provided UI in both the EditorContextProvider and Redux Provider,
|
||||
* ensuring that components under test have access to the necessary context and store.
|
||||
*
|
||||
* @param {React.ReactElement} ui - The React element to render.
|
||||
* @param {object} [options] - Optional parameters.
|
||||
* @param {object} [options.initialState] - Optional initial state for the store.
|
||||
* @param {string} [options.learningContextId] - Optional learning context ID.
|
||||
* @returns {RenderResult} The result of the render, as returned by RTL render.
|
||||
*/
|
||||
const editorRender = (
|
||||
ui,
|
||||
export const editorRender = (
|
||||
ui: React.ReactElement,
|
||||
{
|
||||
initialState = {},
|
||||
learningContextId = 'course-v1:Org+COURSE+RUN',
|
||||
} = {},
|
||||
...options
|
||||
}: Omit<WrapperOptions, 'extraWrapper'> & { initialState?: PartialEditorState, learningContextId?: string } = {},
|
||||
) => {
|
||||
const store = initializeStore(initialState);
|
||||
// We might need a way for the test cases to access this store directly. In that case we could allow either an
|
||||
// initialState parameter OR an editorStore parameter.
|
||||
const store = initializeStore(initialState as any);
|
||||
|
||||
return baseRender(ui, {
|
||||
...options,
|
||||
extraWrapper: ({ children }) => (
|
||||
<EditorContextProvider learningContextId={learningContextId}>
|
||||
<Provider store={store}>
|
||||
@@ -35,5 +40,3 @@ const editorRender = (
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
export default editorRender;
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import {
|
||||
screen, initializeMocks,
|
||||
} from '@src/testUtils';
|
||||
import editorRender from '@src/editors/editorTestRender';
|
||||
import { editorRender } from '@src/editors/editorTestRender';
|
||||
import * as hooks from './hooks';
|
||||
import TinyMceWidget from '.';
|
||||
|
||||
|
||||
@@ -155,18 +155,14 @@ const defaultUser = {
|
||||
*
|
||||
* Returns the new `axiosMock` in case you need to mock out axios requests.
|
||||
*/
|
||||
export function initializeMocks({
|
||||
user = defaultUser, initialState = undefined,
|
||||
initializeStore = initializeReduxStore,
|
||||
}: {
|
||||
export function initializeMocks({ user = defaultUser, initialState = undefined }: {
|
||||
user?: { userId: number, username: string },
|
||||
initialState?: Record<string, any>, // TODO: proper typing for our redux state
|
||||
initializeStore?: (initialState?: Record<string, any>) => Store, // add this line
|
||||
} = {}) {
|
||||
initializeMockApp({
|
||||
authenticatedUser: user,
|
||||
});
|
||||
reduxStore = initializeStore(initialState as any);
|
||||
reduxStore = initializeReduxStore(initialState as any);
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
|
||||
Reference in New Issue
Block a user