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:
Braden MacDonald
2025-08-05 10:41:44 -07:00
committed by GitHub
parent 2f9566c4f5
commit 591444d72d
11 changed files with 80 additions and 126 deletions

View File

@@ -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 = {

View File

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

View File

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

View File

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

View File

@@ -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 '.';

View File

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

View File

@@ -42,7 +42,7 @@ const ProblemEditor: React.FC<Props> = ({
<Spinner
animation="border"
className="m-3"
screenreadertext="Loading Problem Editor"
screenReaderText="Loading Problem Editor"
/>
</div>
);

View File

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

View File

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

View File

@@ -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 '.';

View File

@@ -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: {