From 591444d72d101617fd0dd4b7a8b27ba2c3c127df Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 5 Aug 2025 10:41:44 -0700 Subject: [PATCH] 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 --- src/editors/VideoSelector.test.tsx | 2 +- .../components/EditProblemView/index.jsx | 5 +- .../{index.test.jsx => index.test.tsx} | 47 +++----- .../SelectTypeWrapper/index.test.tsx | 4 +- .../components/SelectTypeModal/index.test.tsx | 4 +- .../containers/ProblemEditor/index.test.tsx | 101 +++++++----------- .../containers/ProblemEditor/index.tsx | 2 +- src/editors/data/store.ts | 2 +- ...torTestRender.jsx => editorTestRender.tsx} | 29 ++--- .../TinyMceWidget/index.test.tsx | 2 +- src/testUtils.tsx | 8 +- 11 files changed, 80 insertions(+), 126 deletions(-) rename src/editors/containers/ProblemEditor/components/EditProblemView/{index.test.jsx => index.test.tsx} (79%) rename src/editors/{editorTestRender.jsx => editorTestRender.tsx} (50%) diff --git a/src/editors/VideoSelector.test.tsx b/src/editors/VideoSelector.test.tsx index f839bf475..d60cf5d6f 100644 --- a/src/editors/VideoSelector.test.tsx +++ b/src/editors/VideoSelector.test.tsx @@ -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 = { diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/index.jsx index 0b55a6b12..e0521d55f 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/index.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/index.jsx @@ -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 ( getContent({ - problemState: problemStateWithoutComplete, + problemState, openSaveWarningModal, isAdvancedProblemType, isMarkdownEditorEnabled, diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/index.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/index.test.tsx similarity index 79% rename from src/editors/containers/ProblemEditor/components/EditProblemView/index.test.jsx rename to src/editors/containers/ProblemEditor/components/EditProblemView/index.test.tsx index 58671ab90..dc59110c2 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/index.test.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/index.test.tsx @@ -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: '', - rawMarkdown: '## Problem', - }, + rawOLX: '', + 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(); + editorRender(, { 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(, { 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: '', - rawMarkdown: '## Problem', - }, + rawOLX: '', + rawMarkdown: '## Problem', isDirty: false, }, }; - initializeMocks({ - initializeStore, - initialState: modifiedInitialState, - }); editorRender(, { initialState: modifiedInitialState }); expect(screen.getByText('markdown:## Problem')).toBeInTheDocument(); }); diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/index.test.tsx b/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/index.test.tsx index 155afb94d..ee99b3a21 100644 --- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/index.test.tsx +++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/index.test.tsx @@ -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'; diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/index.test.tsx b/src/editors/containers/ProblemEditor/components/SelectTypeModal/index.test.tsx index ade0843a0..8eb2a7c5f 100644 --- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/index.test.tsx +++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/index.test.tsx @@ -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 '.'; diff --git a/src/editors/containers/ProblemEditor/index.test.tsx b/src/editors/containers/ProblemEditor/index.test.tsx index a7201012e..32dcf1038 100644 --- a/src/editors/containers/ProblemEditor/index.test.tsx +++ b/src/editors/containers/ProblemEditor/index.test.tsx @@ -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
SelectTypeModal {props.onClose && 'withOnClose'}
; @@ -11,18 +13,10 @@ jest.mock('./components/SelectTypeModal', () => function mockSelectTypeModal(pro jest.mock('./components/EditProblemView', () => function mockEditProblemView(props: any) { return
EditProblemView {props.onClose && 'withOnClose'} {props.returnFunction && 'withReturnFunction'}
; }); - -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(, { initialState }); - const spinner = container.querySelector('.pgn__spinner'); - expect(spinner).toBeInTheDocument(); - expect(spinner).toHaveAttribute('screenreadertext', 'Loading Problem Editor'); + editorRender(, { 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(, { initialState }); - const spinner = container.querySelector('.pgn__spinner'); - expect(spinner).toBeInTheDocument(); - expect(spinner).toHaveAttribute('screenreadertext', 'Loading Problem Editor'); + editorRender(, { 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(, { 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(, { 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(, { initialState }); expect(screen.getByText(/EditProblemView/)).toBeInTheDocument(); diff --git a/src/editors/containers/ProblemEditor/index.tsx b/src/editors/containers/ProblemEditor/index.tsx index 1de4ece03..66b136091 100644 --- a/src/editors/containers/ProblemEditor/index.tsx +++ b/src/editors/containers/ProblemEditor/index.tsx @@ -42,7 +42,7 @@ const ProblemEditor: React.FC = ({ ); diff --git a/src/editors/data/store.ts b/src/editors/data/store.ts index deee26bf4..ce1850527 100755 --- a/src/editors/data/store.ts +++ b/src/editors/data/store.ts @@ -10,7 +10,7 @@ export const createStore = () => { const middleware = [thunkMiddleware, loggerMiddleware]; - const store = redux.createStore( + const store: redux.Store = redux.createStore( reducer as any, composeWithDevToolsLogOnlyInProduction(redux.applyMiddleware(...middleware)), ); diff --git a/src/editors/editorTestRender.jsx b/src/editors/editorTestRender.tsx similarity index 50% rename from src/editors/editorTestRender.jsx rename to src/editors/editorTestRender.tsx index b04917e0a..f422b2176 100644 --- a/src/editors/editorTestRender.jsx +++ b/src/editors/editorTestRender.tsx @@ -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 = { + [P in keyof T]?: RecursivePartial; +}; + +export type PartialEditorState = RecursivePartial; /** * 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 & { 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 }) => ( @@ -35,5 +40,3 @@ const editorRender = ( ), }); }; - -export default editorRender; diff --git a/src/editors/sharedComponents/TinyMceWidget/index.test.tsx b/src/editors/sharedComponents/TinyMceWidget/index.test.tsx index c85bc869f..5f2d53afa 100644 --- a/src/editors/sharedComponents/TinyMceWidget/index.test.tsx +++ b/src/editors/sharedComponents/TinyMceWidget/index.test.tsx @@ -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 '.'; diff --git a/src/testUtils.tsx b/src/testUtils.tsx index b18abe54f..3cb64361a 100644 --- a/src/testUtils.tsx +++ b/src/testUtils.tsx @@ -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, // TODO: proper typing for our redux state - initializeStore?: (initialState?: Record) => Store, // add this line } = {}) { initializeMockApp({ authenticatedUser: user, }); - reduxStore = initializeStore(initialState as any); + reduxStore = initializeReduxStore(initialState as any); queryClient = new QueryClient({ defaultOptions: { queries: {