diff --git a/src/components/FilePreview/__snapshots__/index.test.jsx.snap b/src/components/FilePreview/__snapshots__/index.test.jsx.snap deleted file mode 100644 index cc93a6a7..00000000 --- a/src/components/FilePreview/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` render only supported files 1`] = ` -
- - -
-`; - -exports[` renders nothing when no files are uploaded 1`] = `null`; - -exports[` renders only div when no supported files are uploaded 1`] = `
`; diff --git a/src/components/FilePreview/components/FileRenderer/__snapshots__/index.test.jsx.snap b/src/components/FilePreview/components/FileRenderer/__snapshots__/index.test.jsx.snap deleted file mode 100644 index 6136f6d1..00000000 --- a/src/components/FilePreview/components/FileRenderer/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,47 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FileRenderer Component render default 1`] = ` - - - -`; - -exports[`FileRenderer Component render error banner 1`] = ` - - - -`; - -exports[`FileRenderer Component render loading banner 1`] = ` - - - -`; diff --git a/src/components/FilePreview/components/FileRenderer/hooks.test.js b/src/components/FilePreview/components/FileRenderer/hooks.test.js index 5a7cbc9f..337b2e22 100644 --- a/src/components/FilePreview/components/FileRenderer/hooks.test.js +++ b/src/components/FilePreview/components/FileRenderer/hooks.test.js @@ -1,6 +1,7 @@ import { mockUseKeyedState } from '@edx/react-unit-test-utils'; import { useRenderData, stateKeys } from './hooks'; +import { errorStatuses, errorMessages } from '../constants'; const state = mockUseKeyedState(stateKeys); @@ -43,4 +44,27 @@ describe('useRenderData', () => { state.expectSetStateCalledWith(stateKeys.errorStatus, null); state.expectSetStateCalledWith(stateKeys.isLoading, true); }); + + it('returns correct error message for different error statuses', () => { + // Test notFound error message + state.mockVal(stateKeys.errorStatus, errorStatuses.notFound); + let out = useRenderData(props); + expect(out.error.headerMessage).toBe(errorMessages[errorStatuses.notFound]); + + // Test fallback to serverError message for unknown error status + state.mockVal(stateKeys.errorStatus, errorStatuses.badRequest); + out = useRenderData(props); + expect(out.error.headerMessage).toBe( + errorMessages[errorStatuses.serverError], + ); + }); + + it('handles unknown file types', () => { + const propsWithUnknownFile = { + ...props, + file: { fileName: 'file.unknown', fileUrl: 'http://example.com' }, + }; + const out = useRenderData(propsWithUnknownFile); + expect(out.Renderer).toBeUndefined(); + }); }); diff --git a/src/components/FilePreview/components/FileRenderer/index.test.jsx b/src/components/FilePreview/components/FileRenderer/index.test.jsx index 68ab7178..3fc209a4 100644 --- a/src/components/FilePreview/components/FileRenderer/index.test.jsx +++ b/src/components/FilePreview/components/FileRenderer/index.test.jsx @@ -1,18 +1,45 @@ -import { shallow } from '@edx/react-unit-test-utils'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { useRenderData } from './hooks'; import { FileRenderer } from './index'; -jest.mock('./Banners', () => ({ - ErrorBanner: () => 'ErrorBanner', - LoadingBanner: () => 'LoadingBanner', -})); +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); + +/* eslint-disable react/prop-types */ jest.mock('./hooks', () => ({ useRenderData: jest.fn(), })); +jest.mock('./Banners', () => ({ + ErrorBanner: ({ message, headerMessage, actions }) => ( +
+
{headerMessage}
+
{message}
+ {actions?.map((action) => ( + {action.message} + ))} +
+ ), + LoadingBanner: () =>
Loading...
, +})); + +jest.mock('./FileCard', () => ({ children, file, defaultOpen }) => ( +
+
+ {file.fileName} - {defaultOpen ? 'open' : 'closed'} +
+ {children} +
+)); + describe('FileRenderer Component', () => { + const renderWithIntl = (component) => render({component}); + const props = { file: { fileName: 'some_file', @@ -22,7 +49,7 @@ describe('FileRenderer Component', () => { }; const defaultRenderData = { - Renderer: () => 'Renderer', + Renderer: () =>
Renderer content
, isLoading: false, errorStatus: false, error: null, @@ -31,32 +58,97 @@ describe('FileRenderer Component', () => { }, }; - it('render default', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders default renderer', () => { useRenderData.mockReturnValue(defaultRenderData); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); + renderWithIntl(); - expect(wrapper.instance.findByType('Renderer')).toHaveLength(1); - expect(wrapper.instance.findByType('LoadingBanner')).toHaveLength(0); - expect(wrapper.instance.findByType('ErrorBanner')).toHaveLength(0); + expect(screen.getByTestId('file-card')).toBeInTheDocument(); + expect(screen.getByText('some_file - open')).toBeInTheDocument(); + expect(screen.getByTestId('renderer')).toBeInTheDocument(); + expect(screen.getByText('Renderer content')).toBeInTheDocument(); + expect(screen.queryByTestId('loading-banner')).not.toBeInTheDocument(); + expect(screen.queryByTestId('error-banner')).not.toBeInTheDocument(); }); - it('render loading banner', () => { + it('renders loading banner', () => { useRenderData.mockReturnValue({ ...defaultRenderData, isLoading: true }); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); + renderWithIntl(); + + expect(screen.getByTestId('loading-banner')).toBeInTheDocument(); + expect(screen.getByText('Loading...')).toBeInTheDocument(); + expect(screen.queryByTestId('error-banner')).not.toBeInTheDocument(); + expect(screen.queryByTestId('renderer')).not.toBeInTheDocument(); + }); - expect(wrapper.instance.findByType('LoadingBanner')).toHaveLength(1); - expect(wrapper.instance.findByType('ErrorBanner')).toHaveLength(0); - expect(wrapper.instance.findByType('Renderer')).toHaveLength(0); + it('renders error banner', () => { + useRenderData.mockReturnValue({ + errorStatus: true, + error: { message: 'some_error' }, + }); + renderWithIntl(); + + expect(screen.getByTestId('error-banner')).toBeInTheDocument(); + expect(screen.getByText('some_error')).toBeInTheDocument(); + expect(screen.queryByTestId('loading-banner')).not.toBeInTheDocument(); + expect(screen.queryByTestId('renderer')).not.toBeInTheDocument(); + }); + + it('passes defaultOpen prop correctly', () => { + useRenderData.mockReturnValue(defaultRenderData); + renderWithIntl(); + + expect(screen.getByText('some_file - closed')).toBeInTheDocument(); + }); + + it('passes rendererProps to Renderer component', () => { + const CustomRenderer = jest.fn(() =>
Custom Renderer
); + const customRendererProps = { + prop1: 'value1', + prop2: 'value2', + }; + useRenderData.mockReturnValue({ + ...defaultRenderData, + Renderer: CustomRenderer, + rendererProps: customRendererProps, + }); + + renderWithIntl(); + expect(CustomRenderer).toHaveBeenCalledWith(customRendererProps, {}); + }); + + it('passes all error properties to ErrorBanner', () => { + const errorData = { + headerMessage: 'Error Header', + message: 'Error Message', + actions: [{ id: 'retry', message: 'Retry', onClick: jest.fn() }], + }; + useRenderData.mockReturnValue({ + errorStatus: true, + error: errorData, + }); + + renderWithIntl(); + expect(screen.getByTestId('error-header')).toHaveTextContent( + 'Error Header', + ); + expect(screen.getByTestId('error-message')).toHaveTextContent( + 'Error Message', + ); + expect(screen.getByTestId('error-action-retry')).toHaveTextContent('Retry'); }); - it('render error banner', () => { - useRenderData.mockReturnValue({ errorStatus: true, error: { message: 'some_error' } }); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); + it('handles missing optional file properties', () => { + const minimalProps = { + file: { fileName: 'minimal_file' }, + defaultOpen: true, + }; + useRenderData.mockReturnValue(defaultRenderData); - expect(wrapper.instance.findByType('ErrorBanner')).toHaveLength(1); - expect(wrapper.instance.findByType('LoadingBanner')).toHaveLength(0); + renderWithIntl(); + expect(screen.getByText('minimal_file - open')).toBeInTheDocument(); }); }); diff --git a/src/components/FilePreview/index.test.jsx b/src/components/FilePreview/index.test.jsx index 75b05d1c..09e06dcd 100644 --- a/src/components/FilePreview/index.test.jsx +++ b/src/components/FilePreview/index.test.jsx @@ -1,16 +1,27 @@ -import { shallow } from '@edx/react-unit-test-utils'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; import { useResponse } from 'hooks/app'; import { isSupported } from './components'; import FilePreview from './index'; +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); + jest.mock('hooks/app', () => ({ useResponse: jest.fn(), })); jest.mock('./components', () => ({ - FileRenderer: () => 'FileRenderer', + // eslint-disable-next-line react/prop-types + FileRenderer: ({ file, defaultOpen }) => ( +
+ {/* eslint-disable-next-line react/prop-types */} + {file.fileName} - {defaultOpen ? 'open' : 'closed'} +
+ ), isSupported: jest.fn(), })); @@ -18,40 +29,58 @@ describe('', () => { const props = { defaultCollapsePreview: false, }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + it('renders nothing when no files are uploaded', () => { - useResponse.mockReturnValueOnce({ uploadedFiles: null }); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); - expect(wrapper.isEmptyRender()).toBe(true); + useResponse.mockReturnValue({ uploadedFiles: null }); + const { container } = render(); + expect(container.firstChild).toBeNull(); }); - it('renders only div when no supported files are uploaded', () => { - useResponse.mockReturnValueOnce({ + it('renders empty div when no supported files are uploaded', () => { + useResponse.mockReturnValue({ uploadedFiles: [{ fileName: 'file1.txt' }], }); - isSupported.mockReturnValueOnce(false); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); + isSupported.mockReturnValue(false); + const { container } = render(); - expect(wrapper.instance.findByType('FileRenderer')).toHaveLength(0); + expect(container.firstChild).toBeInTheDocument(); + expect(container.firstChild.tagName).toBe('DIV'); + expect(screen.queryByTestId('file-renderer')).not.toBeInTheDocument(); }); - it('render only supported files', () => { + it('renders only supported files', () => { isSupported .mockReturnValueOnce(false) .mockReturnValueOnce(true) .mockReturnValueOnce(true); - useResponse.mockReturnValueOnce({ + useResponse.mockReturnValue({ uploadedFiles: [ { fileName: 'file1.txt' }, { fileName: 'file2.pdf' }, { fileName: 'file3.jpg' }, ], }); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); + render(); + + const fileRenderers = screen.getAllByTestId('file-renderer'); + expect(fileRenderers).toHaveLength(2); + expect(screen.getByText('file2.pdf - open')).toBeInTheDocument(); + expect(screen.getByText('file3.jpg - open')).toBeInTheDocument(); + expect(screen.queryByText('file1.txt - open')).not.toBeInTheDocument(); + }); + + it('passes defaultCollapsePreview prop correctly', () => { + isSupported.mockReturnValue(true); + useResponse.mockReturnValue({ + uploadedFiles: [{ fileName: 'file1.pdf' }], + }); + render(); - expect(wrapper.instance.findByType('FileRenderer')).toHaveLength(2); + expect(screen.getByText('file1.pdf - closed')).toBeInTheDocument(); }); }); diff --git a/src/components/Rubric/RubricFeedback.test.jsx b/src/components/Rubric/RubricFeedback.test.jsx index e1e491e1..b4804e22 100644 --- a/src/components/Rubric/RubricFeedback.test.jsx +++ b/src/components/Rubric/RubricFeedback.test.jsx @@ -1,10 +1,24 @@ import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import userEvent from '@testing-library/user-event'; import RubricFeedback from './RubricFeedback'; import messages from './messages'; +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); + +// eslint-disable-next-line react/prop-types +jest.mock('components/InfoPopover', () => ({ children }) => ( +
{children}
+)); + describe('', () => { + const renderWithIntl = (component) => render({component}); + const props = { overallFeedbackPrompt: 'overallFeedbackPrompt', overallFeedback: 'overallFeedback', @@ -13,30 +27,65 @@ describe('', () => { onOverallFeedbackChange: jest.fn().mockName('onOverallFeedbackChange'), }; + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('renders', () => { - test('overall feedback is enabled', () => { - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); + it('overall feedback is enabled', () => { + renderWithIntl(); - expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(0); - expect(wrapper.instance.findByType('Form.Control')[0].props.disabled).toBe(false); - expect(wrapper.instance.findByType('Form.Control')[0].props.floatingLabel).toBe(messages.addComments.defaultMessage); + expect( + screen.getByText(messages.overallComments.defaultMessage), + ).toBeInTheDocument(); + expect(screen.getByDisplayValue('overallFeedback')).toBeInTheDocument(); + expect(screen.getByDisplayValue('overallFeedback')).not.toBeDisabled(); + expect( + screen.getByLabelText(messages.addComments.defaultMessage), + ).toBeInTheDocument(); + expect( + screen.queryByText(messages.overallFeedbackError.defaultMessage), + ).not.toBeInTheDocument(); + expect(screen.getByTestId('info-popover')).toBeInTheDocument(); + expect(screen.getByText('overallFeedbackPrompt')).toBeInTheDocument(); }); - test('overall feedback is disabled', () => { - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); + it('overall feedback is disabled', () => { + renderWithIntl(); - expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(0); - expect(wrapper.instance.findByType('Form.Control')[0].props.disabled).toBe(true); - expect(wrapper.instance.findByType('Form.Control')[0].props.floatingLabel).toBe(messages.comments.defaultMessage); + expect( + screen.getByText(messages.overallComments.defaultMessage), + ).toBeInTheDocument(); + expect(screen.getByDisplayValue('overallFeedback')).toBeInTheDocument(); + expect(screen.getByDisplayValue('overallFeedback')).toBeDisabled(); + expect( + screen.getByLabelText(messages.comments.defaultMessage), + ).toBeInTheDocument(); + expect( + screen.queryByText(messages.overallFeedbackError.defaultMessage), + ).not.toBeInTheDocument(); }); - test('overall feedback is invalid', () => { - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); + it('overall feedback is invalid', () => { + renderWithIntl(); + + expect( + screen.getByText(messages.overallComments.defaultMessage), + ).toBeInTheDocument(); + expect(screen.getByDisplayValue('overallFeedback')).toBeInTheDocument(); + expect( + screen.getByText(messages.overallFeedbackError.defaultMessage), + ).toBeInTheDocument(); + }); + + it('handles feedback change', async () => { + const user = userEvent.setup(); + renderWithIntl(); + + const textarea = screen.getByRole('textbox'); + await user.type(textarea, 'new feedback'); - expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(1); + expect(props.onOverallFeedbackChange).toHaveBeenCalled(); }); }); }); diff --git a/src/components/Rubric/__snapshots__/RubricFeedback.test.jsx.snap b/src/components/Rubric/__snapshots__/RubricFeedback.test.jsx.snap deleted file mode 100644 index a5a655c2..00000000 --- a/src/components/Rubric/__snapshots__/RubricFeedback.test.jsx.snap +++ /dev/null @@ -1,94 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` renders overall feedback is disabled 1`] = ` - - - - Overall comments - - -
- overallFeedbackPrompt -
-
-
- -
-`; - -exports[` renders overall feedback is enabled 1`] = ` - - - - Overall comments - - -
- overallFeedbackPrompt -
-
-
- -
-`; - -exports[` renders overall feedback is invalid 1`] = ` - - - - Overall comments - - -
- overallFeedbackPrompt -
-
-
- - - The overall feedback is required - -
-`; diff --git a/src/views/XBlockView/Actions/__snapshots__/index.test.jsx.snap b/src/views/XBlockView/Actions/__snapshots__/index.test.jsx.snap deleted file mode 100644 index 41f0d4d0..00000000 --- a/src/views/XBlockView/Actions/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,101 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` does not render button when step is staff 1`] = ` -
-`; - -exports[` render load next when step peer is not waiting nor waiting for submission 1`] = ` -
- -
-`; - -exports[` render load next when step studentTraining is not waiting nor waiting for submission 1`] = ` -
- -
-`; - -exports[` render message step is not staff and step state done 1`] = ` -
- -
-`; - -exports[` render message step is not staff and step state inProgress 1`] = ` -
- -
-`; diff --git a/src/views/XBlockView/Actions/index.test.jsx b/src/views/XBlockView/Actions/index.test.jsx index d8a0d4f0..7b049171 100644 --- a/src/views/XBlockView/Actions/index.test.jsx +++ b/src/views/XBlockView/Actions/index.test.jsx @@ -1,5 +1,7 @@ -import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import userEvent from '@testing-library/user-event'; import { stepNames, stepStates } from 'constants/index'; import { @@ -11,97 +13,196 @@ import { useOpenModal } from 'hooks/modal'; import SubmissionActions from './index'; +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); + jest.mock('hooks/actions', () => ({ useLoadNextAction: () => ({ action: { labels: { - default: 'default', + default: 'Load Next Assessment', }, }, }), })); + jest.mock('hooks/app', () => ({ useAssessmentStepConfig: jest.fn(), useGlobalState: jest.fn(), useStepInfo: jest.fn(), })); + jest.mock('hooks/modal', () => ({ useOpenModal: jest.fn(), })); describe('', () => { + const renderWithIntl = (component) => render({component}); + const mockOpenModal = jest.fn(); + beforeEach(() => { useOpenModal.mockReturnValue(mockOpenModal); - }); - afterEach(() => { jest.clearAllMocks(); }); - [stepNames.studentTraining, stepNames.peer].forEach((stepName) => { - it(`render load next when step ${stepName} is not waiting nor waiting for submission`, () => { - useGlobalState.mockReturnValue({ - activeStepName: stepName, - stepState: stepStates.notAvailable, - }); - useStepInfo.mockReturnValue({ - [stepName]: { - numberOfAssessmentsCompleted: 1, - isWaitingForSubmissions: false, - }, - }); - useAssessmentStepConfig.mockReturnValue({ - settings: { - [stepName]: { - minNumberToGrade: 1, - }, + it('renders load next button for student training step', async () => { + useGlobalState.mockReturnValue({ + activeStepName: stepNames.studentTraining, + stepState: stepStates.notAvailable, + }); + useStepInfo.mockReturnValue({ + [stepNames.studentTraining]: { + numberOfAssessmentsCompleted: 1, + isWaitingForSubmissions: false, + }, + }); + useAssessmentStepConfig.mockReturnValue({ + settings: { + [stepNames.studentTraining]: { + minNumberToGrade: 1, }, - }); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); + }, + }); + + renderWithIntl(); - expect(wrapper.instance.findByType('Button')).toHaveLength(1); + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveTextContent('Load Next Assessment'); - React.useCallback.mock.calls[0][0](); - expect(mockOpenModal).toHaveBeenCalledWith({ view: stepName, title: stepName }); + const user = userEvent.setup(); + await user.click(button); + + expect(mockOpenModal).toHaveBeenCalledWith({ + view: stepNames.studentTraining, + title: stepNames.studentTraining, }); }); - [stepStates.inProgress, stepNames.done].forEach((stepState) => { - it(`render message step is not staff and step state ${stepState}`, () => { - useGlobalState.mockReturnValue({ - activeStepName: stepNames.studentTraining, - stepState, - }); - useStepInfo.mockReturnValue({ + it('does not render button when step is staff', () => { + useGlobalState.mockReturnValue({ + activeStepName: stepNames.staff, + stepState: stepStates.notAvailable, + }); + useStepInfo.mockReturnValue({}); + useAssessmentStepConfig.mockReturnValue({ settings: {} }); + + renderWithIntl(); + + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); + + it('renders button for in-progress step state', () => { + useGlobalState.mockReturnValue({ + activeStepName: stepNames.studentTraining, + stepState: stepStates.inProgress, + }); + useStepInfo.mockReturnValue({ + [stepNames.studentTraining]: { + numberOfAssessmentsCompleted: 1, + isWaitingForSubmissions: false, + }, + }); + useAssessmentStepConfig.mockReturnValue({ + settings: { [stepNames.studentTraining]: { - numberOfAssessmentsCompleted: 1, - isWaitingForSubmissions: false, + minNumberToGrade: 1, }, - }); - useAssessmentStepConfig.mockReturnValue({ - settings: { - [stepNames.studentTraining]: { - minNumberToGrade: 1, - }, + }, + }); + + renderWithIntl(); + + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + }); + + it('renders container with correct CSS classes', () => { + useGlobalState.mockReturnValue({ + activeStepName: stepNames.staff, + stepState: stepStates.notAvailable, + }); + useStepInfo.mockReturnValue({}); + useAssessmentStepConfig.mockReturnValue({ settings: {} }); + + const { container } = renderWithIntl(); + + const containerDiv = container.querySelector('.text-center.py-2'); + expect(containerDiv).toBeInTheDocument(); + }); + + it('renders optional message for peer assessment when minimum assessments completed', () => { + useGlobalState.mockReturnValue({ + activeStepName: stepNames.peer, + stepState: stepStates.notAvailable, + }); + useStepInfo.mockReturnValue({ + [stepNames.peer]: { + numberOfAssessmentsCompleted: 3, + isWaitingForSubmissions: false, + }, + }); + useAssessmentStepConfig.mockReturnValue({ + settings: { + [stepNames.peer]: { + minNumberToGrade: 3, }, - }); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); + }, + }); + + renderWithIntl(); - expect(wrapper.instance.findByType('Button')).toHaveLength(1); + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveTextContent(/Load Next Assessment.*optional/i); + }); + + it('does not render button when waiting for submissions', () => { + useGlobalState.mockReturnValue({ + activeStepName: stepNames.peer, + stepState: stepStates.notAvailable, + }); + useStepInfo.mockReturnValue({ + [stepNames.peer]: { + numberOfAssessmentsCompleted: 1, + isWaitingForSubmissions: true, + }, + }); + useAssessmentStepConfig.mockReturnValue({ + settings: { + [stepNames.peer]: { + minNumberToGrade: 3, + }, + }, }); + + renderWithIntl(); + + expect(screen.queryByRole('button')).not.toBeInTheDocument(); }); - it('does not render button when step is staff', () => { + it('renders button for done step state', () => { useGlobalState.mockReturnValue({ - activeStepName: stepNames.staff, + activeStepName: stepNames.done, stepState: stepStates.notAvailable, }); + useStepInfo.mockReturnValue({ + [stepNames.done]: { + numberOfAssessmentsCompleted: 0, + isWaitingForSubmissions: false, + }, + }); + useAssessmentStepConfig.mockReturnValue({ + settings: { + [stepNames.done]: {}, + }, + }); - const wrapper = shallow(); - expect(wrapper.snapshot).toMatchSnapshot(); + renderWithIntl(); - expect(wrapper.instance.findByType('Button')).toHaveLength(0); + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); }); }); diff --git a/src/views/XBlockView/__snapshots__/index.test.jsx.snap b/src/views/XBlockView/__snapshots__/index.test.jsx.snap deleted file mode 100644 index 8ffdd92f..00000000 --- a/src/views/XBlockView/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,72 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` does not render prompts and rubric when step is unavailable 1`] = ` -
-

- title -

- - - - - - -
-`; - -exports[` render everything 1`] = ` -
-

- title -

- - - - - - - - - - - -
-`; - -exports[` render everything without rubric 1`] = ` -
-

- title -

- - - - - - - - - - -
-`; diff --git a/src/views/XBlockView/index.test.jsx b/src/views/XBlockView/index.test.jsx index 9bc968f8..717d71e8 100644 --- a/src/views/XBlockView/index.test.jsx +++ b/src/views/XBlockView/index.test.jsx @@ -1,4 +1,6 @@ -import { shallow } from '@edx/react-unit-test-utils'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { useORAConfigData, usePrompts, @@ -8,58 +10,120 @@ import { import XBlockView from './index'; +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); + +// Only mock the hooks that provide data jest.mock('hooks/app', () => ({ useORAConfigData: jest.fn(), usePrompts: jest.fn(), useRubricConfig: jest.fn(), useGlobalState: jest.fn(), })); -jest.mock('components/ProgressBar', () => 'ProgressBar'); -jest.mock('components/Prompt', () => 'Prompt'); -jest.mock('components/Rubric', () => 'Rubric'); -jest.mock('components/Instructions', () => 'Instructions'); -jest.mock('components/StatusAlert', () => 'StatusAlert'); -jest.mock('components/HotjarSurvey', () => 'HotjarSurvey'); -jest.mock('./StatusRow', () => 'StatusRow'); -jest.mock('./Actions', () => 'Actions'); + +// Mock child components to avoid their complex dependencies +jest.mock('components/ProgressBar', () => () => ( +
ProgressBar
+)); +// eslint-disable-next-line react/prop-types +jest.mock('components/Prompt', () => ({ prompt }) => ( +
{prompt}
+)); +// eslint-disable-next-line react/prop-types +jest.mock('components/Rubric', () => ({ isCollapsible }) => ( +
+ {isCollapsible ? 'Collapsible' : 'Not Collapsible'} +
+)); +jest.mock('components/Instructions', () => () => ( +
Instructions
+)); +jest.mock('components/StatusAlert', () => () => ( +
StatusAlert
+)); +jest.mock('components/HotjarSurvey', () => () => ( +
HotjarSurvey
+)); +jest.mock('./StatusRow', () => () => ( +
StatusRow
+)); +jest.mock('./Actions', () => () =>
Actions
); describe('', () => { - it('render everything', () => { - useORAConfigData.mockReturnValue({ title: 'title' }); + const renderWithIntl = (component) => render({component}); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders everything with title and prompts', () => { + useORAConfigData.mockReturnValue({ title: 'Test Title' }); usePrompts.mockReturnValue(['prompt1', 'prompt2']); useRubricConfig.mockReturnValue({ showDuringResponse: true }); useGlobalState.mockReturnValue({ stepIsUnavailable: false }); - const wrapper = shallow(); + renderWithIntl(); - expect(wrapper.snapshot).toMatchSnapshot(); - expect(wrapper.instance.findByType('Prompt')).toHaveLength(2); - expect(wrapper.instance.findByType('Rubric')).toHaveLength(1); + expect(screen.getByText('Test Title')).toBeInTheDocument(); + expect(screen.getByTestId('progress-bar')).toBeInTheDocument(); + expect(screen.getByTestId('status-row')).toBeInTheDocument(); + expect(screen.getByTestId('status-alert')).toBeInTheDocument(); + expect(screen.getByTestId('hotjar-survey')).toBeInTheDocument(); + expect(screen.getByTestId('instructions')).toBeInTheDocument(); + expect(screen.getByTestId('actions')).toBeInTheDocument(); + expect(screen.getAllByTestId('prompt')).toHaveLength(2); + expect(screen.getByText('prompt1')).toBeInTheDocument(); + expect(screen.getByText('prompt2')).toBeInTheDocument(); + expect(screen.getByTestId('rubric')).toBeInTheDocument(); + expect(screen.getByText('Collapsible')).toBeInTheDocument(); }); - it('render everything without rubric', () => { - useORAConfigData.mockReturnValue({ title: 'title' }); + it('renders everything without rubric when showDuringResponse is false', () => { + useORAConfigData.mockReturnValue({ title: 'Test Title' }); usePrompts.mockReturnValue(['prompt1', 'prompt2']); useRubricConfig.mockReturnValue({ showDuringResponse: false }); useGlobalState.mockReturnValue({ stepIsUnavailable: false }); - const wrapper = shallow(); + renderWithIntl(); - expect(wrapper.snapshot).toMatchSnapshot(); - expect(wrapper.instance.findByType('Prompt')).toHaveLength(2); - expect(wrapper.instance.findByType('Rubric')).toHaveLength(0); + expect(screen.getByText('Test Title')).toBeInTheDocument(); + expect(screen.getByTestId('progress-bar')).toBeInTheDocument(); + expect(screen.getAllByTestId('prompt')).toHaveLength(2); + expect(screen.getByText('prompt1')).toBeInTheDocument(); + expect(screen.getByText('prompt2')).toBeInTheDocument(); + expect(screen.queryByTestId('rubric')).not.toBeInTheDocument(); }); it('does not render prompts and rubric when step is unavailable', () => { - useORAConfigData.mockReturnValue({ title: 'title' }); + useORAConfigData.mockReturnValue({ title: 'Test Title' }); usePrompts.mockReturnValue(['prompt1', 'prompt2']); useRubricConfig.mockReturnValue({ showDuringResponse: true }); useGlobalState.mockReturnValue({ stepIsUnavailable: true }); - const wrapper = shallow(); + renderWithIntl(); + + expect(screen.getByText('Test Title')).toBeInTheDocument(); + expect(screen.getByTestId('progress-bar')).toBeInTheDocument(); + expect(screen.getByTestId('status-row')).toBeInTheDocument(); + expect(screen.getByTestId('status-alert')).toBeInTheDocument(); + expect(screen.getByTestId('hotjar-survey')).toBeInTheDocument(); + expect(screen.getByTestId('instructions')).toBeInTheDocument(); + expect(screen.getByTestId('actions')).toBeInTheDocument(); + expect(screen.queryByTestId('prompt')).not.toBeInTheDocument(); + expect(screen.queryByTestId('rubric')).not.toBeInTheDocument(); + }); + + it('renders correct heading structure', () => { + useORAConfigData.mockReturnValue({ title: 'Test Title' }); + usePrompts.mockReturnValue([]); + useRubricConfig.mockReturnValue({ showDuringResponse: false }); + useGlobalState.mockReturnValue({ stepIsUnavailable: false }); + + renderWithIntl(); - expect(wrapper.snapshot).toMatchSnapshot(); - expect(wrapper.instance.findByType('Prompt')).toHaveLength(0); - expect(wrapper.instance.findByType('Rubric')).toHaveLength(0); + const heading = screen.getByRole('heading', { level: 3 }); + expect(heading).toBeInTheDocument(); + expect(heading).toHaveTextContent('Test Title'); }); });