From 48282a32881243a68bca3c6b0344ea974a3502c3 Mon Sep 17 00:00:00 2001 From: Victor Date: Tue, 24 Jun 2025 13:12:57 -0600 Subject: [PATCH] test: deprecate react-unit-test-utils part-9 --- src/App.test.jsx | 126 ++++++++++------ src/__snapshots__/App.test.jsx.snap | 42 ------ .../ReviewErrors/FetchErrors.test.jsx | 58 ++++--- .../__snapshots__/index.test.jsx.snap | 17 --- .../ReviewErrors/SubmitErrors/index.test.jsx | 122 +++++++++++---- .../__snapshots__/FetchErrors.test.jsx.snap | 15 -- .../__snapshots__/index.test.jsx.snap | 10 -- .../ReviewModal/ReviewErrors/index.test.jsx | 82 ++++++++-- .../Rubric/__snapshots__/index.test.jsx.snap | 87 ----------- src/containers/Rubric/index.test.jsx | 142 +++++++++++++++--- 10 files changed, 407 insertions(+), 294 deletions(-) delete mode 100644 src/__snapshots__/App.test.jsx.snap delete mode 100644 src/containers/ReviewModal/ReviewErrors/SubmitErrors/__snapshots__/index.test.jsx.snap delete mode 100644 src/containers/ReviewModal/ReviewErrors/__snapshots__/FetchErrors.test.jsx.snap delete mode 100644 src/containers/ReviewModal/ReviewErrors/__snapshots__/index.test.jsx.snap delete mode 100644 src/containers/Rubric/__snapshots__/index.test.jsx.snap diff --git a/src/App.test.jsx b/src/App.test.jsx index 8b011d6ce..44958d924 100644 --- a/src/App.test.jsx +++ b/src/App.test.jsx @@ -1,63 +1,99 @@ -import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; +import { render, screen } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { selectors } from 'data/redux'; +import { App, mapStateToProps } from './App'; -import { App } from './App'; +jest.unmock('react'); +jest.unmock('@openedx/paragon'); +jest.unmock('@edx/frontend-platform/i18n'); -jest.mock('data/redux', () => ({ - app: { - selectors: { - courseMetadata: (state) => ({ courseMetadata: state }), - isEnabled: (state) => ({ isEnabled: state }), - }, - }, +// we want to scope these tests to the App component, so we mock some child components to reduce complexity +jest.mock('@edx/frontend-component-header', () => ({ + LearningHeader: () =>
Learning Header
, })); -jest.mock('@edx/frontend-component-header', () => ({ - LearningHeader: 'Header', +jest.mock('@edx/frontend-component-footer', () => ({ + FooterSlot: () =>
Footer
, })); -jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'FooterSlot' })); -jest.mock('containers/DemoWarning', () => 'DemoWarning'); -jest.mock('containers/ListView', () => 'ListView'); -jest.mock('components/Head', () => 'Head'); +jest.mock('containers/ListView', () => function ListView() { + return
List View
; +}); + +jest.mock('containers/DemoWarning', () => function DemoWarning() { + return
Demo Warning
; +}); -let el; +jest.mock('data/redux', () => ({ + selectors: { + app: { + courseMetadata: jest.fn((state) => state.courseMetadata || { + org: 'test-org', + number: 'test-101', + title: 'Test Course', + }), + isEnabled: jest.fn((state) => (state.isEnabled !== undefined ? state.isEnabled : true)), + }, + }, +})); + +const renderWithIntl = (component) => render( + + {component} + , +); -describe('App router component', () => { - const props = { +describe('App component', () => { + const defaultProps = { courseMetadata: { - org: 'course-org', - number: 'course-number', - title: 'course-title', + org: 'test-org', + number: 'test-101', + title: 'Test Course', }, isEnabled: true, }; - test('snapshot: enabled', () => { - expect(shallow().snapshot).toMatchSnapshot(); + + beforeEach(() => { + jest.clearAllMocks(); }); - test('snapshot: disabled (show demo warning)', () => { - expect(shallow().snapshot).toMatchSnapshot(); + + it('renders header with course metadata', () => { + renderWithIntl(); + + const header = screen.getByTestId('header'); + expect(header).toBeInTheDocument(); }); - describe('component', () => { - beforeEach(() => { - el = shallow(); - }); - describe('Router', () => { - test('Routing - ListView is only route', () => { - expect(el.instance.findByTestId('main')[0].children).toHaveLength(1); - expect(el.instance.findByTestId('main')[0].children[0].type).toBe('ListView'); - }); - }); - test('Header to use courseMetadata props', () => { - const { - courseTitle, - courseNumber, - courseOrg, - } = el.instance.findByTestId('header')[0].props; - expect(courseTitle).toEqual(props.courseMetadata.title); - expect(courseNumber).toEqual(props.courseMetadata.number); - expect(courseOrg).toEqual(props.courseMetadata.org); + it('renders main content', () => { + renderWithIntl(); + + const main = screen.getByTestId('main'); + expect(main).toBeInTheDocument(); + }); + + it('does not render demo warning when enabled', () => { + renderWithIntl(); + + const demoWarning = screen.queryByRole('alert'); + expect(demoWarning).not.toBeInTheDocument(); + }); + + it('renders demo warning when disabled', () => { + renderWithIntl(); + + const demoWarning = screen.getByRole('alert'); + expect(demoWarning).toBeInTheDocument(); + }); + + describe('mapStateToProps', () => { + it('maps state properties correctly', () => { + const testState = { arbitraryState: 'some data' }; + const mapped = mapStateToProps(testState); + + expect(selectors.app.courseMetadata).toHaveBeenCalledWith(testState); + expect(selectors.app.isEnabled).toHaveBeenCalledWith(testState); + expect(mapped.courseMetadata).toEqual(selectors.app.courseMetadata(testState)); + expect(mapped.isEnabled).toEqual(selectors.app.isEnabled(testState)); }); }); }); diff --git a/src/__snapshots__/App.test.jsx.snap b/src/__snapshots__/App.test.jsx.snap deleted file mode 100644 index 4101d1433..000000000 --- a/src/__snapshots__/App.test.jsx.snap +++ /dev/null @@ -1,42 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`App router component snapshot: disabled (show demo warning) 1`] = ` - -
- -
- -
- -
- -
-
-`; - -exports[`App router component snapshot: enabled 1`] = ` - -
- -
-
- -
- -
-
-`; diff --git a/src/containers/ReviewModal/ReviewErrors/FetchErrors.test.jsx b/src/containers/ReviewModal/ReviewErrors/FetchErrors.test.jsx index d264a1281..39d0aad54 100644 --- a/src/containers/ReviewModal/ReviewErrors/FetchErrors.test.jsx +++ b/src/containers/ReviewModal/ReviewErrors/FetchErrors.test.jsx @@ -1,4 +1,6 @@ import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { selectors, thunkActions } from 'data/redux'; import { RequestKeys } from 'data/constants/requests'; @@ -9,6 +11,10 @@ import { mapDispatchToProps, } from './FetchErrors'; +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); + jest.mock('data/redux', () => ({ selectors: { requests: { @@ -22,39 +28,51 @@ jest.mock('data/redux', () => ({ }, })); -jest.mock('./ReviewError', () => 'ReviewError'); - const requestKey = RequestKeys.fetchSubmission; +const renderWithIntl = (component) => render( + + {component} + , +); + describe('FetchErrors component', () => { const props = { isFailed: false, + reload: jest.fn(), }; - describe('component', () => { - beforeEach(() => { - props.reload = jest.fn(); - }); - describe('snapshots', () => { - test('snapshot: no failure', () => { - expect().toMatchSnapshot(); - }); - test('snapshot: with failure', () => { - expect().toMatchSnapshot(); - }); - }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('does not render when isFailed is false', () => { + const { container } = renderWithIntl(); + expect(container.firstChild).toBeNull(); }); + + it('renders error message when isFailed is true', () => { + renderWithIntl(); + expect(screen.getByText('Error loading submissions')).toBeInTheDocument(); + expect(screen.getByText('An error occurred while loading this submission. Try reloading this submission.')).toBeInTheDocument(); + }); + + it('renders reload button when error occurs', () => { + renderWithIntl(); + expect(screen.getByText('Reload submission')).toBeInTheDocument(); + }); + describe('mapStateToProps', () => { - let mapped; const testState = { some: 'test-state' }; - beforeEach(() => { - mapped = mapStateToProps(testState); - }); - test('isFailed loads from requests.isFailed(fetchSubmission)', () => { + + it('maps isFailed from requests selector', () => { + const mapped = mapStateToProps(testState); expect(mapped.isFailed).toEqual(selectors.requests.isFailed(testState, { requestKey })); }); }); + describe('mapDispatchToProps', () => { - it('loads reload from thunkActions.grading.loadSubmission', () => { + it('maps reload from thunkActions.grading.loadSubmission', () => { expect(mapDispatchToProps.reload).toEqual(thunkActions.grading.loadSubmission); }); }); diff --git a/src/containers/ReviewModal/ReviewErrors/SubmitErrors/__snapshots__/index.test.jsx.snap b/src/containers/ReviewModal/ReviewErrors/SubmitErrors/__snapshots__/index.test.jsx.snap deleted file mode 100644 index 584751319..000000000 --- a/src/containers/ReviewModal/ReviewErrors/SubmitErrors/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SubmitErrors component snapshots snapshot: no failure 1`] = `null`; - -exports[`SubmitErrors component snapshots snapshot: with valid error, loads from hook 1`] = ` - - hooks.content - -`; diff --git a/src/containers/ReviewModal/ReviewErrors/SubmitErrors/index.test.jsx b/src/containers/ReviewModal/ReviewErrors/SubmitErrors/index.test.jsx index 57790a295..97bec0884 100644 --- a/src/containers/ReviewModal/ReviewErrors/SubmitErrors/index.test.jsx +++ b/src/containers/ReviewModal/ReviewErrors/SubmitErrors/index.test.jsx @@ -1,36 +1,106 @@ import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; - -import { keyStore } from 'utils'; -import { formatMessage } from 'testUtils'; +import { render, screen } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import * as hooks from './hooks'; import { SubmitErrors } from '.'; -jest.mock('../ReviewError', () => 'ReviewError'); +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); + +jest.mock('react-redux', () => ({ + useDispatch: jest.fn(() => jest.fn()), +})); + +jest.mock('./hooks', () => ({ + rendererHooks: jest.fn(() => ({ show: false })), +})); + +const renderWithIntl = (component) => render( + + {component} + , +); -const hookKeys = keyStore(hooks); describe('SubmitErrors component', () => { - const props = { intl: { formatMessage } }; - describe('snapshots', () => { - test('snapshot: no failure', () => { - jest.spyOn(hooks, hookKeys.rendererHooks).mockReturnValueOnce({ show: false }); - const el = shallow(); - expect(el.snapshot).toMatchSnapshot(); - expect(el.isEmptyRender()).toEqual(true); - }); - test('snapshot: with valid error, loads from hook', () => { - const mockHook = { - show: true, - reviewActions: { - confirm: 'hooks.reviewActions.confirm', - cancel: 'hooks.reviewActions.cancel', + const props = { + intl: { + formatMessage: jest.fn((message) => message.defaultMessage || message.id), + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('does not render when show is false', () => { + hooks.rendererHooks.mockReturnValueOnce({ show: false }); + const { container } = renderWithIntl(); + expect(container.firstChild).toBeNull(); + }); + + it('renders ReviewError when show is true', () => { + const mockHook = { + show: true, + reviewActions: { + confirm: { + onClick: jest.fn(), + message: { + id: 'ora-grading.ReviewModal.resubmitGrade', + defaultMessage: 'Resubmit grate', + }, + }, + cancel: { + onClick: jest.fn(), + message: { + id: 'ora-grading.ReviewModal.dismiss', + defaultMessage: 'Dismiss', + }, + }, + }, + headingMessage: { + id: 'ora-grading.ReviewModal.gradeNotSubmitted.heading', + defaultMessage: 'Grade not submitted', + }, + content: "We're sorry, something went wrong when we tried to submit this grade. Please try again.", + }; + hooks.rendererHooks.mockReturnValueOnce(mockHook); + + renderWithIntl(); + expect(screen.getByText('Grade not submitted')).toBeInTheDocument(); + expect(screen.getByText("We're sorry, something went wrong when we tried to submit this grade. Please try again.")).toBeInTheDocument(); + }); + + it('renders action buttons when provided', () => { + const mockHook = { + show: true, + reviewActions: { + confirm: { + onClick: jest.fn(), + message: { + id: 'ora-grading.ReviewModal.resubmitGrade', + defaultMessage: 'Resubmit grate', + }, }, - headingMessage: 'hooks.headingMessage', - content: 'hooks.content', - }; - jest.spyOn(hooks, hookKeys.rendererHooks).mockReturnValueOnce(mockHook); - expect(shallow().snapshot).toMatchSnapshot(); - }); + cancel: { + onClick: jest.fn(), + message: { + id: 'ora-grading.ReviewModal.dismiss', + defaultMessage: 'Dismiss', + }, + }, + }, + headingMessage: { + id: 'ora-grading.ReviewModal.gradeNotSubmitted.heading', + defaultMessage: 'Grade not submitted', + }, + content: "We're sorry, something went wrong when we tried to submit this grade. Please try again.", + }; + hooks.rendererHooks.mockReturnValueOnce(mockHook); + + renderWithIntl(); + expect(screen.getByText('Resubmit grate')).toBeInTheDocument(); + expect(screen.getByText('Dismiss')).toBeInTheDocument(); }); }); diff --git a/src/containers/ReviewModal/ReviewErrors/__snapshots__/FetchErrors.test.jsx.snap b/src/containers/ReviewModal/ReviewErrors/__snapshots__/FetchErrors.test.jsx.snap deleted file mode 100644 index abb0d119c..000000000 --- a/src/containers/ReviewModal/ReviewErrors/__snapshots__/FetchErrors.test.jsx.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FetchErrors component component snapshots snapshot: no failure 1`] = ` - -`; - -exports[`FetchErrors component component snapshots snapshot: with failure 1`] = ` - -`; diff --git a/src/containers/ReviewModal/ReviewErrors/__snapshots__/index.test.jsx.snap b/src/containers/ReviewModal/ReviewErrors/__snapshots__/index.test.jsx.snap deleted file mode 100644 index 92d3324c5..000000000 --- a/src/containers/ReviewModal/ReviewErrors/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,10 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ReviewErrors component component snapshot: no failure 1`] = ` - - - - - - -`; diff --git a/src/containers/ReviewModal/ReviewErrors/index.test.jsx b/src/containers/ReviewModal/ReviewErrors/index.test.jsx index 305c3539e..2964280e7 100644 --- a/src/containers/ReviewModal/ReviewErrors/index.test.jsx +++ b/src/containers/ReviewModal/ReviewErrors/index.test.jsx @@ -1,17 +1,77 @@ -import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; - +import { render } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { ReviewErrors } from '.'; -jest.mock('./FetchErrors', () => 'FetchErrors'); -jest.mock('./SubmitErrors', () => 'SubmitErrors'); -jest.mock('./LockErrors', () => 'LockErrors'); -jest.mock('./DownloadErrors', () => 'DownloadErrors'); +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); + +jest.mock('react-redux', () => ({ + useDispatch: jest.fn(() => jest.fn()), + useSelector: jest.fn((selector) => selector({ + requests: { isFailed: false, error: null, errorStatus: null }, + grading: { isLocked: false }, + })), + connect: (mapStateToProps, mapDispatchToProps) => (Component) => { + const MockedComponent = (props) => { + const mockState = {}; + const mockDispatch = jest.fn(); + const stateProps = mapStateToProps ? mapStateToProps(mockState) : {}; + let dispatchProps = {}; + if (mapDispatchToProps) { + if (typeof mapDispatchToProps === 'function') { + dispatchProps = mapDispatchToProps(mockDispatch); + } else { + dispatchProps = mapDispatchToProps; + } + } + return ; + }; + return MockedComponent; + }, +})); + +jest.mock('data/redux', () => ({ + selectors: { + requests: { + isFailed: jest.fn(() => false), + error: jest.fn(() => null), + errorStatus: jest.fn(() => null), + }, + grading: { + selected: { + isLocked: jest.fn(() => false), + }, + }, + }, + thunkActions: { + app: { + initialize: jest.fn(), + }, + grading: { + loadSubmission: jest.fn(), + submitResponse: jest.fn(), + }, + download: { + downloadFiles: jest.fn(), + }, + }, + actions: { + requests: { + clearRequest: jest.fn(), + }, + }, +})); + +const renderWithIntl = (component) => render( + + {component} + , +); describe('ReviewErrors component', () => { - describe('component', () => { - test('snapshot: no failure', () => { - expect(shallow().snapshot).toMatchSnapshot(); - }); + it('renders without errors', () => { + const { container } = renderWithIntl(); + expect(container).toBeInTheDocument(); }); }); diff --git a/src/containers/Rubric/__snapshots__/index.test.jsx.snap b/src/containers/Rubric/__snapshots__/index.test.jsx.snap deleted file mode 100644 index e7b1bb6e5..000000000 --- a/src/containers/Rubric/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,87 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Rubric Container shapshot: hide footer 1`] = ` - - - -

- Rubric -

-
- - - -
- -
-
- -
-`; - -exports[`Rubric Container snapshot: show footer 1`] = ` - - - -

- Rubric -

-
- - - -
- -
-
- -
-
- -
-`; diff --git a/src/containers/Rubric/index.test.jsx b/src/containers/Rubric/index.test.jsx index b3d920bdb..870b6051d 100644 --- a/src/containers/Rubric/index.test.jsx +++ b/src/containers/Rubric/index.test.jsx @@ -1,39 +1,139 @@ -import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; - +import { render } from '@testing-library/react'; import { formatMessage } from 'testUtils'; - -import * as hooks from './hooks'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { Rubric } from '.'; +import * as hooks from './hooks'; + +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); + +jest.mock('react-redux', () => ({ + useDispatch: jest.fn(() => jest.fn()), + connect: jest.fn((mapStateToProps, mapDispatchToProps) => (Component) => { + const ConnectedComponent = (props) => { + const mockState = {}; + const stateProps = mapStateToProps ? mapStateToProps(mockState, props) : {}; + const dispatchProps = mapDispatchToProps || {}; + return ; + }; + return ConnectedComponent; + }), +})); + +jest.mock('data/redux', () => ({ + actions: { + grading: { + setCriterionOption: jest.fn(), + setRubricFeedback: jest.fn(), + }, + }, + selectors: { + app: { + courseId: jest.fn(() => 'test-course-id'), + isEnabled: jest.fn(() => false), + rubric: { + criteriaIndices: jest.fn(() => [0, 1]), + criterionConfig: jest.fn((state, { orderNum }) => ({ + name: `test-criterion-${orderNum}`, + prompt: `Test criterion prompt ${orderNum}`, + options: [ + { + name: 'option1', + label: 'Option 1', + points: 1, + explanation: 'First option', + }, + { + name: 'option2', + label: 'Option 2', + points: 2, + explanation: 'Second option', + }, + ], + })), + criterionFeedbackConfig: jest.fn((state, { orderNum }) => ({ + feedbackEnabled: true, + defaultValue: `Default feedback for criterion ${orderNum}`, + })), + feedbackConfig: jest.fn(() => ({ + enabled: true, + defaultValue: 'Overall feedback default', + })), + feedbackPrompt: jest.fn(() => 'Please provide overall feedback'), + }, + }, + grading: { + selected: { + gradeStatus: jest.fn(() => 'ungraded'), + isGrading: jest.fn(() => true), + criterionSelectedOption: jest.fn(() => 'option1'), + criterionFeedback: jest.fn(() => 'Test feedback'), + overallFeedback: jest.fn(() => 'Test overall feedback'), + }, + validation: { + criterionSelectedOptionIsInvalid: jest.fn(() => false), + criterionFeedbackIsInvalid: jest.fn(() => false), + overallFeedbackIsInvalid: jest.fn(() => false), + }, + }, + requests: { + isPending: jest.fn(() => false), + isCompleted: jest.fn(() => false), + }, + }, + thunkActions: { + grading: { + submitGrade: jest.fn(() => jest.fn()), + }, + }, +})); -jest.mock('containers/CriterionContainer', () => 'CriterionContainer'); -jest.mock('./RubricFeedback', () => 'RubricFeedback'); -jest.mock('components/DemoAlert', () => 'DemoAlert'); jest.mock('./hooks', () => ({ rendererHooks: jest.fn(), ButtonStates: jest.requireActual('./hooks').ButtonStates, })); +const renderWithIntl = (component) => render( + + {component} + , +); + describe('Rubric Container', () => { - const props = { - intl: { formatMessage }, - }; const hookProps = { criteria: [ - { prop: 'hook-criteria-props-1', key: 1 }, - { prop: 'hook-criteria-props-2', key: 2 }, - { prop: 'hook-criteria-props-3', key: 3 }, + { orderNum: 1, key: 1, isGrading: true }, + { orderNum: 2, key: 2, isGrading: true }, ], showFooter: false, - buttonProps: { prop: 'hook-button-props' }, - demoAlertProps: { prop: 'demo-alert-props' }, + buttonProps: { variant: 'primary' }, + demoAlertProps: { show: false }, + }; + + const props = { + intl: { formatMessage }, }; - test('snapshot: show footer', () => { + + beforeEach(() => { + jest.clearAllMocks(); + hooks.rendererHooks.mockReturnValue(hookProps); + }); + + it('renders rubric with footer when showFooter is true', () => { hooks.rendererHooks.mockReturnValueOnce({ ...hookProps, showFooter: true }); - expect(shallow().snapshot).toMatchSnapshot(); + const { container } = renderWithIntl(); + const rubricCard = container.querySelector('.grading-rubric-card'); + const footer = container.querySelector('.grading-rubric-footer'); + expect(rubricCard).toBeInTheDocument(); + expect(footer).toBeInTheDocument(); }); - test('shapshot: hide footer', () => { - hooks.rendererHooks.mockReturnValueOnce(hookProps); - expect(shallow().snapshot).toMatchSnapshot(); + + it('renders rubric without footer when showFooter is false', () => { + const { container } = renderWithIntl(); + const rubricCard = container.querySelector('.grading-rubric-card'); + const footer = container.querySelector('.grading-rubric-footer'); + expect(rubricCard).toBeInTheDocument(); + expect(footer).not.toBeInTheDocument(); }); });