diff --git a/src/App.test.jsx b/src/App.test.jsx index 78042244..dc313f58 100644 --- a/src/App.test.jsx +++ b/src/App.test.jsx @@ -1,117 +1,68 @@ import React from 'react'; -import { when } from 'jest-when'; -import { Route, Routes } from 'react-router-dom'; - -import { ErrorPage } from '@edx/frontend-platform/react'; -import { useIntl } from '@edx/frontend-platform/i18n'; -import { formatMessage, shallow } from '@edx/react-unit-test-utils'; - -import AssessmentView from 'views/AssessmentView'; -import SubmissionView from 'views/SubmissionView'; -import XBlockView from 'views/XBlockView'; -import XBlockStudioView from 'views/XBlockStudioView'; -import GradeView from 'views/GradeView'; - -import PageRoute from 'components/PageRoute'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { MemoryRouter } from 'react-router-dom'; import { useHandleModalCloseEvent } from 'hooks/modal'; -import messages from './messages'; -import routes from './routes'; - import App from './App'; -jest.mock('react-router-dom', () => ({ - Routes: 'Routes', - Route: 'Route', -})); +/* eslint-disable react/prop-types */ -jest.mock('@edx/frontend-platform/react', () => ({ - AuthenticatedPageRoute: 'AuthenticatedPageRoute', - ErrorPage: 'ErrorPage', -})); -jest.mock('views/AssessmentView', () => 'AssessmentView'); -jest.mock('views/SubmissionView', () => 'SubmissionView'); -jest.mock('views/XBlockView', () => 'XBlockView'); -jest.mock('views/XBlockStudioView', () => 'XBlockStudioView'); -jest.mock('views/GradeView', () => 'GradeView'); -jest.mock('components/PageRoute', () => 'PageRoute'); +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); jest.mock('hooks/modal', () => ({ useHandleModalCloseEvent: jest.fn(), })); +jest.mock('@edx/frontend-platform/react', () => ({ + ErrorPage: ({ message }) =>
{message || 'Error Page'}
, +})); + const handleModalClose = jest.fn(); -when(useHandleModalCloseEvent).calledWith().mockReturnValue(handleModalClose); const addEventListener = jest.fn(); const removeEventListener = jest.fn(); -let el; describe('App component', () => { + const renderWithProviders = (component) => render( + + + {component} + + , + ); + beforeEach(() => { jest.clearAllMocks(); + useHandleModalCloseEvent.mockReturnValue(handleModalClose); jest.spyOn(window, 'addEventListener').mockImplementation(addEventListener); - jest.spyOn(window, 'removeEventListener').mockImplementation(removeEventListener); - el = shallow(); + jest + .spyOn(window, 'removeEventListener') + .mockImplementation(removeEventListener); + }); + + it('renders app with accessible error page fallback', () => { + renderWithProviders(); + + expect(screen.getByRole('alert')).toHaveTextContent('Page not found'); }); - describe('behavior', () => { - it('initializes i18n and refresh event from hooks', () => { - expect(useIntl).toHaveBeenCalled(); - expect(useHandleModalCloseEvent).toHaveBeenCalled(); - }); - it('adds handler for modal close event that refreshes page data', () => { - expect(React.useEffect.mock.calls.length).toEqual(1); - const [[effect, prereqs]] = React.useEffect.mock.calls; - expect(prereqs).toEqual([handleModalClose]); - const out = effect(); - expect(addEventListener).toHaveBeenCalledWith('message', handleModalClose); - out(); - expect(removeEventListener).toHaveBeenCalledWith('message', handleModalClose); - }); + + it('calls useHandleModalCloseEvent hook and sets up event listeners', () => { + renderWithProviders(); + expect(useHandleModalCloseEvent).toHaveBeenCalled(); + expect(addEventListener).toHaveBeenCalledWith('message', handleModalClose); }); - describe('render', () => { - test('snapshot', () => { - expect(el.snapshot).toMatchSnapshot(); - }); - const testComponent = (toTest, { route, Component, isModal }) => { - expect(toTest.type).toEqual(Route); - expect(toTest.props.path).toEqual(route); - const { element } = toTest.props; - expect(toTest.props.element.type).toEqual(PageRoute); - if (isModal) { - expect(toTest.props.element.props.isModal).toEqual(true); - } - const expectedElement = shallow(); - expect(shallow(element)).toMatchObject(expectedElement); - }; - const testAssessmentRoute = (toTest, { route }) => { - testComponent(toTest, { route, Component: AssessmentView, isModal: true }); - }; - test('route order', () => { - const renderedRoutes = el.instance.findByType(Routes)[0].children; - testComponent(renderedRoutes[0], { route: routes.xblock, Component: XBlockView }); - testComponent(renderedRoutes[1], { route: routes.xblockStudio, Component: XBlockStudioView }); - testComponent(renderedRoutes[2], { route: routes.xblockPreview, Component: XBlockView }); - testAssessmentRoute(renderedRoutes[3], { route: routes.peerAssessment }); - testAssessmentRoute(renderedRoutes[4], { route: routes.selfAssessment }); - testAssessmentRoute(renderedRoutes[5], { route: routes.studentTraining }); - testComponent(renderedRoutes[6], { - route: routes.submission, - Component: SubmissionView, - isModal: true, - }); - testComponent(renderedRoutes[7], { - route: routes.graded, - Component: GradeView, - isModal: true, - }); - expect(renderedRoutes[8].matches(shallow( - } - />, - ))); - }); + + it('removes event listener on unmount', () => { + const { unmount } = renderWithProviders(); + + unmount(); + expect(removeEventListener).toHaveBeenCalledWith( + 'message', + handleModalClose, + ); }); }); diff --git a/src/__snapshots__/App.test.jsx.snap b/src/__snapshots__/App.test.jsx.snap deleted file mode 100644 index e688a807..00000000 --- a/src/__snapshots__/App.test.jsx.snap +++ /dev/null @@ -1,89 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`App component render snapshot 1`] = ` - - - - - } - path="xblock/:courseId/:xblockId/:progressKey?" - /> - - - - } - path="xblock_studio/:courseId/:xblockId/:progressKey?" - /> - - - - } - path="xblock_preview/:courseId/:xblockId/:progressKey?" - /> - - - - } - path="peer_assessment/:courseId/:xblockId/:progressKey?" - /> - - - - } - path="self_assessment/:courseId/:xblockId/:progressKey?" - /> - - - - } - path="student_training/:courseId/:xblockId/:progressKey?" - /> - - - - } - path="submission/:courseId/:xblockId/:progressKey?" - /> - - - - } - path="graded/:courseId/:xblockId/:progressKey?" - /> - - } - key="error" - path="/*" - /> - -`; diff --git a/src/components/Assessment/EditableAssessment/OverallFeedback/__snapshots__/index.test.jsx.snap b/src/components/Assessment/EditableAssessment/OverallFeedback/__snapshots__/index.test.jsx.snap deleted file mode 100644 index cf61a365..00000000 --- a/src/components/Assessment/EditableAssessment/OverallFeedback/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` render default 1`] = ` - - - - Overall comments - - -
- useOverallFeedbackPrompt -
-
-
- -
-`; - -exports[` render empty on studentTraining 1`] = `null`; diff --git a/src/components/Assessment/EditableAssessment/OverallFeedback/index.test.jsx b/src/components/Assessment/EditableAssessment/OverallFeedback/index.test.jsx index 8583a9dd..e2481c40 100644 --- a/src/components/Assessment/EditableAssessment/OverallFeedback/index.test.jsx +++ b/src/components/Assessment/EditableAssessment/OverallFeedback/index.test.jsx @@ -1,58 +1,83 @@ -import { shallow } from '@edx/react-unit-test-utils'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { useOverallFeedbackPrompt, useOverallFeedbackFormFields } from 'hooks/assessment'; import { useViewStep } from 'hooks/routing'; import { stepNames } from 'constants/index'; import OverallFeedback from '.'; +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); + jest.mock('hooks/assessment', () => ({ useOverallFeedbackPrompt: jest.fn(), useOverallFeedbackFormFields: jest.fn(), })); -jest.mock('components/InfoPopover', () => 'InfoPopover'); +jest.mock('components/InfoPopover', () => ({ + __esModule: true, + default: ({ children }) => ( +
+ {children} +
+ ), +})); jest.mock('hooks/routing', () => ({ - useViewStep: jest.fn().mockReturnValue('step'), + useViewStep: jest.fn(), })); +const messages = { + 'frontend-app-ora.EditableAssessment.overallComments': 'Overall comments', + 'frontend-app-ora.EditableAssessment.addComments': 'Add comments (Optional)', +}; + describe('', () => { - const mockOnChange = jest - .fn() - .mockName('useOverallFeedbackFormFields.onChange'); - const mockFeedbackValue = 'useOverallFeedbackFormFields.value'; - const mockPrompt = 'useOverallFeedbackPrompt'; + const renderWithIntl = (component) => render( + + {component} + , + ); + + const mockOnChange = jest.fn(); + const mockFeedbackValue = 'Test feedback content'; + const mockPrompt = 'Please provide overall feedback'; - beforeAll(() => { + beforeEach(() => { + jest.clearAllMocks(); useOverallFeedbackPrompt.mockReturnValue(mockPrompt); useOverallFeedbackFormFields.mockReturnValue({ value: mockFeedbackValue, onChange: mockOnChange, }); + useViewStep.mockReturnValue('assessment'); }); - it('render default', () => { - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); + it('renders overall feedback form with prompt and textarea', () => { + renderWithIntl(); + + expect(screen.getByText('Overall comments')).toBeInTheDocument(); + expect(screen.getByText('Please provide overall feedback')).toBeInTheDocument(); + expect(screen.getByRole('textbox')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Help information' })).toBeInTheDocument(); }); - it('render empty on studentTraining', () => { - useViewStep.mockReturnValueOnce(stepNames.studentTraining); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); + it('renders nothing when step is studentTraining', () => { + useViewStep.mockReturnValue(stepNames.studentTraining); - expect(wrapper.isEmptyRender()).toBe(true); - }); + const { container } = renderWithIntl(); - it('has correct mock value', () => { - const wrapper = shallow(); + expect(container.firstChild).toBeNull(); + }); - expect(wrapper.instance.findByTestId('prompt-test-id')[0].children[0].el).toBe( - mockPrompt, - ); + it('displays correct form field values from hooks', () => { + renderWithIntl(); - const { props } = wrapper.instance.findByType('Form.Control')[0]; - expect(props.value).toBe(mockFeedbackValue); - expect(props.onChange).toBe(mockOnChange); + const textarea = screen.getByRole('textbox'); + expect(textarea).toHaveValue(mockFeedbackValue); + expect(screen.getByText('Please provide overall feedback')).toBeInTheDocument(); }); }); diff --git a/src/components/Assessment/EditableAssessment/__snapshots__/index.test.jsx.snap b/src/components/Assessment/EditableAssessment/__snapshots__/index.test.jsx.snap deleted file mode 100644 index a582ee80..00000000 --- a/src/components/Assessment/EditableAssessment/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,125 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` render empty criteria 1`] = ` - - -

- Rubric -

-
-
- -
- -
-`; - -exports[` render with criteria 1`] = ` - - -

- Rubric -

-
- - } - input={ - - } - key="criterion1" - /> - - } - input={ - - } - key="criterion2" - /> - - } - input={ - - } - key="criterion3" - /> -
- -
- -
-`; diff --git a/src/components/Assessment/EditableAssessment/index.test.jsx b/src/components/Assessment/EditableAssessment/index.test.jsx index 592c8750..df25a43d 100644 --- a/src/components/Assessment/EditableAssessment/index.test.jsx +++ b/src/components/Assessment/EditableAssessment/index.test.jsx @@ -1,37 +1,66 @@ -import { shallow } from '@edx/react-unit-test-utils'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { useCriteriaConfig } from 'hooks/assessment'; import EditableAssessment from '.'; +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); + jest.mock('hooks/assessment', () => ({ useCriteriaConfig: jest.fn(), })); -jest.mock('components/CriterionContainer', () => 'CriterionContainer'); -jest.mock('components/CriterionContainer/RadioCriterion', () => 'RadioCriterion'); -jest.mock('components/CriterionContainer/CriterionFeedback', () => 'CriterionFeedback'); -jest.mock('./OverallFeedback', () => 'OverallFeedback'); -jest.mock('./AssessmentActions', () => 'AssessmentActions'); +jest.mock('components/CriterionContainer', () => () => ( +
Criterion Container
+)); +jest.mock('./OverallFeedback', () => () => ( +
Overall Feedback
+)); +jest.mock('./AssessmentActions', () => () => ( +
Assessment Actions
+)); + +const messages = { + 'frontend-app-ora.EditableAssessment.rubric': 'Rubric', +}; describe('', () => { - it('render empty criteria', () => { + const renderWithIntl = (component) => render( + + {component} + , + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders assessment card structure with all core components', () => { useCriteriaConfig.mockReturnValue([]); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - expect(wrapper.instance.findByType('CriterionContainer')).toHaveLength(0); + renderWithIntl(); + + expect(screen.getByText('Rubric')).toBeInTheDocument(); + expect(screen.getByRole('region', { name: 'Overall Feedback' })).toBeInTheDocument(); + expect(screen.getByRole('group', { name: 'Assessment Actions' })).toBeInTheDocument(); }); - it('render with criteria', () => { + it('renders criterion containers based on criteria config', () => { const mockCriteria = [ { name: 'criterion1' }, { name: 'criterion2' }, - { name: 'criterion3' }, ]; useCriteriaConfig.mockReturnValue(mockCriteria); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - expect(wrapper.instance.findByType('CriterionContainer')).toHaveLength(mockCriteria.length); + renderWithIntl(); + + expect(screen.getByText('Rubric')).toBeInTheDocument(); + expect(screen.getAllByText('Criterion Container')).toHaveLength(2); + expect(screen.getByRole('region', { name: 'Overall Feedback' })).toBeInTheDocument(); + expect(screen.getByRole('group', { name: 'Assessment Actions' })).toBeInTheDocument(); }); }); diff --git a/src/components/ModalActions/__snapshots__/index.test.jsx.snap b/src/components/ModalActions/__snapshots__/index.test.jsx.snap deleted file mode 100644 index 614e5c99..00000000 --- a/src/components/ModalActions/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,50 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` can render primary and secondary with confirm 1`] = ` -
- - - - -
-`; - -exports[` can render primary and secondary without confirm 1`] = ` -
- - -
-`; - -exports[` render empty when no actions 1`] = ` -
-`; - -exports[` render skeleton when page data is loading 1`] = ` - -`; diff --git a/src/components/ModalActions/index.test.jsx b/src/components/ModalActions/index.test.jsx index a191a383..c75ab366 100644 --- a/src/components/ModalActions/index.test.jsx +++ b/src/components/ModalActions/index.test.jsx @@ -1,19 +1,25 @@ -import { shallow } from '@edx/react-unit-test-utils'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { useIsPageDataLoading } from 'hooks/app'; - import useModalActionConfig from './hooks/useModalActionConfig'; import ModalActions from './index'; -jest.mock('components/ActionButton', () => 'ActionButton'); -jest.mock('components/ConfirmDialog', () => 'ConfirmDialog'); +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); + jest.mock('hooks/app', () => ({ useIsPageDataLoading: jest.fn(), })); jest.mock('./hooks/useModalActionConfig', () => jest.fn()); describe('', () => { + const renderWithIntl = (component) => render({component}); + const props = { options: {}, }; @@ -21,50 +27,84 @@ describe('', () => { useIsPageDataLoading.mockReturnValue(false); }); - it('render skeleton when page data is loading', () => { + it('renders skeleton when page data is loading', () => { useIsPageDataLoading.mockReturnValueOnce(true); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - expect(wrapper.instance.findByType('Skeleton')).toHaveLength(1); + useModalActionConfig.mockReturnValue({}); + const { container } = renderWithIntl(); + expect( + container.querySelector('.react-loading-skeleton'), + ).toBeInTheDocument(); }); - it('render empty when no actions', () => { + it('renders empty actions container when no actions are configured', () => { useModalActionConfig.mockReturnValue({}); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - expect(wrapper.instance.findByType('ActionButton')).toHaveLength(0); - expect(wrapper.instance.findByType('ConfirmDialog')).toHaveLength(0); + const { container } = renderWithIntl(); + const actionDiv = container.querySelector('.mt-2'); + expect(actionDiv).toBeInTheDocument(); + expect(actionDiv).toBeEmptyDOMElement(); }); - it('can render primary and secondary without confirm', () => { + it('renders primary and secondary buttons without confirm dialogs', () => { useModalActionConfig.mockReturnValue({ primary: { - action: {}, + action: { + children: 'Primary Action', + onClick: jest.fn(), + }, }, secondary: { - action: {}, + action: { + children: 'Secondary Action', + onClick: jest.fn(), + }, }, }); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - expect(wrapper.instance.findByType('ActionButton')).toHaveLength(2); - expect(wrapper.instance.findByType('ConfirmDialog')).toHaveLength(0); + renderWithIntl(); + + expect( + screen.getByRole('button', { name: 'Primary Action' }), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Secondary Action' }), + ).toBeInTheDocument(); }); - it('can render primary and secondary with confirm', () => { + it('renders primary and secondary buttons with confirm dialogs', () => { useModalActionConfig.mockReturnValue({ primary: { - action: {}, - confirmProps: {}, + action: { + children: 'Primary Action', + onClick: jest.fn(), + }, + confirmProps: { + title: 'Confirm Primary', + description: 'Are you sure?', + action: { onClick: jest.fn() }, + isOpen: false, + close: jest.fn(), + }, }, secondary: { - action: {}, - confirmProps: {}, + action: { + children: 'Secondary Action', + onClick: jest.fn(), + }, + confirmProps: { + title: 'Confirm Secondary', + description: 'Are you sure?', + action: { onClick: jest.fn() }, + isOpen: false, + close: jest.fn(), + }, }, }); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - expect(wrapper.instance.findByType('ActionButton')).toHaveLength(2); - expect(wrapper.instance.findByType('ConfirmDialog')).toHaveLength(2); + renderWithIntl(); + + expect( + screen.getByRole('button', { name: 'Primary Action' }), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Secondary Action' }), + ).toBeInTheDocument(); }); }); diff --git a/src/views/GradeView/__snapshots__/index.test.jsx.snap b/src/views/GradeView/__snapshots__/index.test.jsx.snap deleted file mode 100644 index c52ca321..00000000 --- a/src/views/GradeView/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,133 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` renders correctly 1`] = ` -
-
- - - - - - - - - - - - - -
-
-`; diff --git a/src/views/GradeView/index.test.jsx b/src/views/GradeView/index.test.jsx index 9cf2486d..9b7ca43c 100644 --- a/src/views/GradeView/index.test.jsx +++ b/src/views/GradeView/index.test.jsx @@ -1,18 +1,37 @@ -import { shallow } from '@edx/react-unit-test-utils'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; import GradeView from './index'; -jest.mock('components/ModalActions', () => 'ModalActions'); -jest.mock('./FinalGrade', () => 'FinalGrade'); -jest.mock('./Content', () => 'Content'); +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); + +jest.mock('./FinalGrade', () => () => ( +
Final Grade Component
+)); +jest.mock('./Content', () => () => ( +
Grade Content Component
+)); +jest.mock('components/ModalActions', () => () => ( +
Modal Actions Component
+)); describe('', () => { - it('renders correctly', () => { - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); + it('renders grade view with proper layout structure and responsive design', () => { + const { container } = render(); + + expect(screen.getByText('Final Grade Component')).toBeInTheDocument(); + expect(screen.getByText('Grade Content Component')).toBeInTheDocument(); + expect(screen.getByText('Modal Actions Component')).toBeInTheDocument(); + + const gradeViewBody = container.querySelector('.grade-view-body'); + expect(gradeViewBody).toBeInTheDocument(); - expect(wrapper.instance.findByType('ModalActions')).toHaveLength(1); - expect(wrapper.instance.findByType('FinalGrade')).toHaveLength(1); - expect(wrapper.instance.findByType('Content')).toHaveLength(1); + const mainContainer = container.querySelector( + '.m-0.d-flex.justify-content-center', + ); + expect(mainContainer).toBeInTheDocument(); }); });