diff --git a/src/containers/DemoWarning/DemoWarning.test.jsx b/src/containers/DemoWarning/DemoWarning.test.jsx index 0b74083b0..2542555c2 100644 --- a/src/containers/DemoWarning/DemoWarning.test.jsx +++ b/src/containers/DemoWarning/DemoWarning.test.jsx @@ -1,8 +1,12 @@ -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 { DemoWarning, mapStateToProps } from '.'; +import messages from './messages'; + +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); jest.mock('data/redux', () => ({ selectors: { @@ -10,24 +14,26 @@ jest.mock('data/redux', () => ({ }, })); -let el; - describe('DemoWarning component', () => { - describe('snapshots', () => { - test('does not render if disabled flag is missing', () => { - el = shallow(); - expect(el.snapshot).toMatchSnapshot(); - expect(el.isEmptyRender()).toEqual(true); + describe('behavior', () => { + it('does not render when hide prop is true', () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); }); - test('snapshot: disabled flag is present', () => { - el = shallow(); - expect(el.snapshot).toMatchSnapshot(); - expect(el.isEmptyRender()).toEqual(false); + + it('renders alert with warning message when hide prop is false', () => { + render(); + const alert = screen.getByRole('alert'); + expect(alert).toBeInTheDocument(); + expect(alert).toHaveClass('alert-warning'); + expect(alert).toHaveTextContent(messages.demoModeMessage.defaultMessage); + expect(alert).toHaveTextContent(messages.demoModeHeading.defaultMessage); }); }); + describe('mapStateToProps', () => { - const testState = { some: 'test-state' }; - test('hide is forwarded from app.isEnabled', () => { + it('maps hide prop from app.isEnabled selector', () => { + const testState = { some: 'test-state' }; expect(mapStateToProps(testState).hide).toEqual( selectors.app.isEnabled(testState), ); diff --git a/src/containers/DemoWarning/__snapshots__/DemoWarning.test.jsx.snap b/src/containers/DemoWarning/__snapshots__/DemoWarning.test.jsx.snap deleted file mode 100644 index 537646d10..000000000 --- a/src/containers/DemoWarning/__snapshots__/DemoWarning.test.jsx.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DemoWarning component snapshots does not render if disabled flag is missing 1`] = `null`; - -exports[`DemoWarning component snapshots snapshot: disabled flag is present 1`] = ` - - - - -

- -

-
-`; diff --git a/src/containers/ListView/EmptySubmission.test.jsx b/src/containers/ListView/EmptySubmission.test.jsx index 3e85ef9fe..2bff790d1 100644 --- a/src/containers/ListView/EmptySubmission.test.jsx +++ b/src/containers/ListView/EmptySubmission.test.jsx @@ -1,33 +1,48 @@ -import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; - -import { Hyperlink } from '@openedx/paragon'; - +import { render } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import urls from 'data/services/lms/urls'; - import EmptySubmission from './EmptySubmission'; +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); + jest.mock('data/services/lms/urls', () => ({ openResponse: (courseId) => `openResponseUrl(${courseId})`, })); -jest.mock('./assets/emptyState.svg', () => './assets/emptyState.svg'); - -let el; +jest.mock('./assets/empty-state.svg', () => './assets/empty-state.svg'); describe('EmptySubmission component', () => { - describe('component', () => { - const props = { courseId: 'test-course-id' }; - beforeEach(() => { - el = shallow(); - }); - test('snapshot', () => { - expect(el.snapshot).toMatchSnapshot(); - }); - test('openResponse destination', () => { - expect( - el.instance.findByType(Hyperlink)[0].props.destination, - ).toEqual(urls.openResponse(props.courseId)); - }); + const props = { courseId: 'test-course-id' }; + + const renderWithIntl = (component) => render( + + {component} + , + ); + + it('renders the empty state image with correct alt text', () => { + const { getByAltText } = renderWithIntl(); + expect(getByAltText('empty state')).toBeInTheDocument(); + }); + + it('renders the no results found title message', () => { + const { getByText } = renderWithIntl(); + expect(getByText('Nothing here yet')).toBeInTheDocument(); + }); + + it('renders hyperlink with correct destination URL', () => { + const { container } = renderWithIntl(); + const hyperlink = container.querySelector('a'); + expect(hyperlink).toHaveAttribute( + 'href', + urls.openResponse(props.courseId), + ); + }); + + it('renders the back to responses button', () => { + const { getByText } = renderWithIntl(); + expect(getByText('Back to all open responses')).toBeInTheDocument(); }); }); diff --git a/src/containers/ListView/FilterStatusComponent.test.jsx b/src/containers/ListView/FilterStatusComponent.test.jsx index 99c8fbd01..aae61ef30 100644 --- a/src/containers/ListView/FilterStatusComponent.test.jsx +++ b/src/containers/ListView/FilterStatusComponent.test.jsx @@ -1,54 +1,21 @@ import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; +import PropTypes from 'prop-types'; +import { render } from '@testing-library/react'; +import { DataTableContext } from '@openedx/paragon'; import * as module from './FilterStatusComponent'; -const fieldIds = [ - 'field-id-0', - 'field-id-1', - 'field-id-2', - 'field-id-3', -]; +jest.unmock('@openedx/paragon'); +jest.unmock('react'); + +const fieldIds = ['field-id-0', 'field-id-1', 'field-id-2', 'field-id-3']; const filterOrder = [1, 0, 3, 2]; -const filters = filterOrder.map(v => ({ id: fieldIds[v] })); -const headers = [0, 1, 2, 3].map(v => ({ +const filters = filterOrder.map((v) => ({ id: fieldIds[v] })); +const headers = [0, 1, 2, 3].map((v) => ({ id: fieldIds[v], Header: `HeaDer-${v}`, })); -describe('FilterStatusComponent hooks', () => { - const context = { headers, state: { filters } }; - const mockTableContext = (newContext) => { - React.useContext.mockReturnValueOnce(newContext); - }; - beforeEach(() => { - context.setAllFilters = jest.fn(); - }); - it('returns empty dict if setAllFilters or state.filters is falsey', () => { - mockTableContext({ ...context, setAllFilters: null }); - expect(module.filterHooks()).toEqual({}); - mockTableContext({ ...context, state: { filters: null } }); - expect(module.filterHooks()).toEqual({}); - }); - describe('clearFilters', () => { - it('uses React.useCallback to clear filters, only once', () => { - mockTableContext(context); - const { cb, prereqs } = module.filterHooks().clearFilters.useCallback; - expect(prereqs).toEqual([context.setAllFilters]); - expect(context.setAllFilters).not.toHaveBeenCalled(); - cb(); - expect(context.setAllFilters).toHaveBeenCalledWith([]); - }); - }); - describe('filterNames', () => { - it('returns list of Header values by filter order', () => { - mockTableContext(context); - expect(module.filterHooks().filterNames).toEqual( - filterOrder.map(v => headers[v].Header), - ); - }); - }); -}); describe('FilterStatusComponent component', () => { const props = { className: 'css-class-name', @@ -58,34 +25,98 @@ describe('FilterStatusComponent component', () => { buttonClassName: 'css-class-name-for-button', showFilteredFields: true, }; - const hookProps = { - clearFilters: jest.fn().mockName('hookProps.clearFilters'), - filterNames: ['filter-name-0', 'filter-name-1'], - }; const { FilterStatusComponent } = module; - const mockHooks = (value) => { - jest.spyOn(module, 'filterHooks').mockReturnValueOnce(value); + + const renderWithContext = (contextValue, componentProps = props) => { + const TestWrapper = ({ children }) => ( + + {children} + + ); + TestWrapper.propTypes = { + children: PropTypes.node, + }; + return render( + + + , + ); }; - describe('snapshot', () => { - describe('with filters', () => { - test('showFilteredFields', () => { - mockHooks(hookProps); - const el = shallow(); - expect(el.snapshot).toMatchSnapshot(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('behavior', () => { + it('does not render when there are no filters', () => { + const contextValue = { + headers, + state: { filters: null }, + setAllFilters: jest.fn(), + }; + const { container } = renderWithContext(contextValue); + expect(container.firstChild).toBeNull(); + }); + + it('does not render when setAllFilters is not available', () => { + const contextValue = { headers, state: { filters }, setAllFilters: null }; + const { container } = renderWithContext(contextValue); + expect(container.firstChild).toBeNull(); + }); + + it('renders clear filters button with correct text when filters exist', () => { + const contextValue = { + headers, + state: { filters }, + setAllFilters: jest.fn(), + }; + const { getByText } = renderWithContext(contextValue); + expect(getByText(props.clearFiltersText)).toBeInTheDocument(); + }); + + it('displays filtered field names when showFilteredFields is true', () => { + const contextValue = { + headers, + state: { filters }, + setAllFilters: jest.fn(), + }; + const { getByText } = renderWithContext(contextValue); + const expectedFilterNames = filterOrder.map((v) => headers[v].Header); + expectedFilterNames.forEach((name) => { + expect(getByText(name, { exact: false })).toBeInTheDocument(); }); - test('showFilteredFields=false - hide filterTexts', () => { - mockHooks(hookProps); - const el = shallow( - , - ); - expect(el.snapshot).toMatchSnapshot(); + }); + + it('does not display filtered field names when showFilteredFields is false', () => { + const contextValue = { + headers, + state: { filters }, + setAllFilters: jest.fn(), + }; + const { queryByText } = renderWithContext(contextValue, { + ...props, + showFilteredFields: false, }); + expect(queryByText(/Filtered by/)).not.toBeInTheDocument(); }); - test('without filters', () => { - mockHooks({}); - const el = shallow(); - expect(el.snapshot).toMatchSnapshot(); - expect(el.isEmptyRender()).toEqual(true); + + it('applies correct CSS classes to the component', () => { + const contextValue = { + headers, + state: { filters }, + setAllFilters: jest.fn(), + }; + const { container } = renderWithContext(contextValue); + expect(container.firstChild).toHaveClass(props.className); + }); + + it('calls setAllFilters with empty array when clear button is clicked', () => { + const setAllFilters = jest.fn(); + const contextValue = { headers, state: { filters }, setAllFilters }; + const { getByText } = renderWithContext(contextValue); + const clearButton = getByText(props.clearFiltersText); + clearButton.click(); + expect(setAllFilters).toHaveBeenCalledWith([]); }); }); }); diff --git a/src/containers/ListView/ListError.test.jsx b/src/containers/ListView/ListError.test.jsx index 07edc9169..55dc574ee 100644 --- a/src/containers/ListView/ListError.test.jsx +++ b/src/containers/ListView/ListError.test.jsx @@ -1,19 +1,18 @@ -import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; - +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { selectors, thunkActions } from 'data/redux'; +import { ListError, mapDispatchToProps, mapStateToProps } from './ListError'; +import messages from './messages'; -import { formatMessage } from 'testUtils'; -import { - ListError, - mapDispatchToProps, - mapStateToProps, -} from './ListError'; +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); jest.mock('data/redux', () => ({ selectors: { app: { - courseId: (...args) => ({ courseId: args }), + courseId: jest.fn((state) => state.courseId || 'test-course-id'), }, }, thunkActions: { @@ -27,41 +26,60 @@ jest.mock('data/services/lms/urls', () => ({ openResponse: (courseId) => `api/openResponse/${courseId}`, })); -let el; -jest.useFakeTimers('modern'); - describe('ListError component', () => { - describe('component', () => { - const props = { - courseId: 'test-course-id', - }; - beforeEach(() => { - props.loadSelectionForReview = jest.fn(); - props.intl = { formatMessage }; - props.initializeApp = jest.fn(); + const props = { + courseId: 'test-course-id', + initializeApp: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('behavior', () => { + it('renders error alert with proper styling', () => { + render(); + const alert = screen.getByRole('alert'); + expect(alert).toBeInTheDocument(); + expect(alert).toHaveClass('alert-danger'); }); - describe('render tests', () => { - beforeEach(() => { - el = shallow(); - }); - test('snapshot', () => { - expect(el.snapshot).toMatchSnapshot(); - }); + + it('displays error heading and message', () => { + render(); + const heading = screen.getByRole('alert').querySelector('.alert-heading'); + expect(heading).toBeInTheDocument(); + expect(heading).toHaveTextContent(messages.loadErrorHeading.defaultMessage); + }); + + it('displays try again button', () => { + render(); + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('btn-primary'); + }); + + it('calls initializeApp when try again button is clicked', async () => { + render(); + const user = userEvent.setup(); + const button = screen.getByRole('button'); + await user.click(button); + expect(props.initializeApp).toHaveBeenCalledTimes(1); }); }); + describe('mapStateToProps', () => { - let mapped; const testState = { some: 'test-state' }; - beforeEach(() => { - mapped = mapStateToProps(testState); - }); - test('courseId loads from app.courseId', () => { + it('maps courseId from app.courseId selector', () => { + const mapped = mapStateToProps(testState); expect(mapped.courseId).toEqual(selectors.app.courseId(testState)); }); }); + describe('mapDispatchToProps', () => { - it('loads initializeApp from thunkActions.app.initialize', () => { - expect(mapDispatchToProps.initializeApp).toEqual(thunkActions.app.initialize); + it('maps initializeApp from thunkActions.app.initialize', () => { + expect(mapDispatchToProps.initializeApp).toEqual( + thunkActions.app.initialize, + ); }); }); }); diff --git a/src/containers/ListView/__snapshots__/EmptySubmission.test.jsx.snap b/src/containers/ListView/__snapshots__/EmptySubmission.test.jsx.snap deleted file mode 100644 index 9bf153151..000000000 --- a/src/containers/ListView/__snapshots__/EmptySubmission.test.jsx.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EmptySubmission component component snapshot 1`] = ` -
- empty state -

- -

-

- -

- - - -
-`; diff --git a/src/containers/ListView/__snapshots__/FilterStatusComponent.test.jsx.snap b/src/containers/ListView/__snapshots__/FilterStatusComponent.test.jsx.snap deleted file mode 100644 index d58db79f1..000000000 --- a/src/containers/ListView/__snapshots__/FilterStatusComponent.test.jsx.snap +++ /dev/null @@ -1,37 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FilterStatusComponent component snapshot with filters showFilteredFields 1`] = ` -
-

- Filtered by - filter-name-0, filter-name-1 -

- -
-`; - -exports[`FilterStatusComponent component snapshot with filters showFilteredFields=false - hide filterTexts 1`] = ` -
- -
-`; - -exports[`FilterStatusComponent component snapshot without filters 1`] = `null`; diff --git a/src/containers/ListView/__snapshots__/ListError.test.jsx.snap b/src/containers/ListView/__snapshots__/ListError.test.jsx.snap deleted file mode 100644 index a7f672856..000000000 --- a/src/containers/ListView/__snapshots__/ListError.test.jsx.snap +++ /dev/null @@ -1,48 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ListError component component render tests snapshot 1`] = ` - - - , - ] - } - variant="danger" -> - - - -

- - - , - } - } - /> -

-
-`; diff --git a/src/containers/ListView/__snapshots__/index.test.jsx.snap b/src/containers/ListView/__snapshots__/index.test.jsx.snap deleted file mode 100644 index d7d153c57..000000000 --- a/src/containers/ListView/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,56 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ListView component component snapshots error 1`] = ` - - - - -`; - -exports[`ListView component component snapshots loaded has data 1`] = ` - - - - - - - -`; - -exports[`ListView component component snapshots loaded with no data 1`] = ` - - - - -`; - -exports[`ListView component component snapshots loading 1`] = ` - -
- -

- -

-
- -
-`; diff --git a/src/containers/ListView/index.test.jsx b/src/containers/ListView/index.test.jsx index 3e0845c16..c8bc5ac78 100644 --- a/src/containers/ListView/index.test.jsx +++ b/src/containers/ListView/index.test.jsx @@ -1,47 +1,97 @@ 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, thunkActions } from 'data/redux'; import { RequestKeys } from 'data/constants/requests'; import { formatMessage } from 'testUtils'; import { ListView, mapStateToProps, mapDispatchToProps } from '.'; +import messages from './messages'; -jest.mock('components/StatusBadge', () => 'StatusBadge'); -jest.mock('containers/ReviewModal', () => 'ReviewModal'); -jest.mock('./ListViewBreadcrumb', () => 'ListViewBreadcrumb'); -jest.mock('./ListError', () => 'ListError'); -jest.mock('./SubmissionsTable', () => 'SubmissionsTable'); -jest.mock('./EmptySubmission', () => 'EmptySubmission'); +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); + +jest.mock('containers/ReviewModal', () => { + const ReviewModal = () =>
ReviewModal
; + return ReviewModal; +}); + +jest.mock('./ListViewBreadcrumb', () => { + const ListViewBreadcrumb = () => ( +
Back to all open responses
+ ); + return ListViewBreadcrumb; +}); + +jest.mock('./ListError', () => { + const ListError = () => ( +
+ +
+ ); + return ListError; +}); + +jest.mock('./SubmissionsTable', () => { + const SubmissionsTable = () => ( +
SubmissionsTable
+ ); + return SubmissionsTable; +}); + +jest.mock('./EmptySubmission', () => { + const EmptySubmission = () => ( +
+

Nothing here yet

+

When learners submit responses, they will appear here

+
+ ); + return EmptySubmission; +}); jest.mock('data/redux', () => ({ selectors: { app: { courseId: (...args) => ({ courseId: args }), + isEnabled: () => false, + oraName: () => 'Test ORA Name', }, requests: { isCompleted: (...args) => ({ isCompleted: args }), isFailed: (...args) => ({ isFailed: args }), + allowNavigation: () => true, }, submissions: { isEmptySubmissionData: (...args) => ({ isEmptySubmissionData: args }), }, + grading: { + activeIndex: () => 0, + selectionLength: () => 1, + selected: { + submissionUUID: () => null, + overallFeedback: () => '', + criteria: () => [], + }, + next: { + doesExist: () => false, + }, + prev: { + doesExist: () => false, + }, + }, }, thunkActions: { app: { initialize: (...args) => ({ initialize: args }), }, + grading: { + submitGrade: () => jest.fn(), + }, }, })); -jest.mock('@openedx/paragon', () => ({ - Container: 'Container', - Spinner: 'Spinner', -})); - -let el; -jest.useFakeTimers('modern'); - describe('ListView component', () => { describe('component', () => { const props = { @@ -49,37 +99,75 @@ describe('ListView component', () => { isLoaded: false, hasError: false, isEmptySubmissionData: false, + initializeApp: jest.fn(), + intl: { formatMessage }, }; + beforeEach(() => { - props.initializeApp = jest.fn(); - props.intl = { formatMessage }; + jest.clearAllMocks(); }); - describe('snapshots', () => { - beforeEach(() => { - el = shallow(); - }); - test('loading', () => { - expect(el.snapshot).toMatchSnapshot(); - }); - test('loaded has data', () => { - el = shallow(); - expect(el.snapshot).toMatchSnapshot(); - }); - - test('loaded with no data', () => { - el = shallow(); - expect(el.snapshot).toMatchSnapshot(); - }); - test('error', () => { - el = shallow(); - expect(el.snapshot).toMatchSnapshot(); - }); + + it('displays loading spinner and message when not loaded and no error', () => { + render(); + + // Check for loading message + expect(screen.getByText(messages.loadingResponses.defaultMessage)).toBeInTheDocument(); + + // Check for spinner by finding element with spinner class + const spinner = document.querySelector('.pgn__spinner'); + expect(spinner).toBeInTheDocument(); + }); + + it('displays ListViewBreadcrumb and SubmissionsTable when loaded with data', () => { + render(); + + expect( + screen.getByText('Back to all open responses'), + ).toBeInTheDocument(); + expect(screen.getByTestId('submissions-table')).toBeInTheDocument(); + expect(screen.queryByText('FormattedMessage')).not.toBeInTheDocument(); + }); + + it('displays EmptySubmission component when loaded but has no submission data', () => { + render(); + + expect( + screen.getByRole('heading', { name: 'Nothing here yet' }), + ).toBeInTheDocument(); + expect( + screen.queryByText( + 'When learners submit responses, they will appear here', + ), + ).toBeInTheDocument(); + expect( + screen.queryByText('Back to all open responses'), + ).not.toBeInTheDocument(); + expect(screen.queryByTestId('submissions-table')).not.toBeInTheDocument(); }); - describe('behavior', () => { - it('calls initializeApp on load', () => { - el = shallow(); - expect(props.initializeApp).toHaveBeenCalled(); - }); + + it('displays ListError component when there is an error', () => { + render(); + + expect( + screen.getByRole('button', { name: 'Reload submissions' }), + ).toBeInTheDocument(); + expect(screen.queryByText('FormattedMessage')).not.toBeInTheDocument(); + }); + + it('always displays ReviewModal component regardless of state', () => { + const { rerender } = render(); + expect(screen.getByText('ReviewModal')).toBeInTheDocument(); + + rerender(); + expect(screen.getByText('ReviewModal')).toBeInTheDocument(); + + rerender(); + expect(screen.getByText('ReviewModal')).toBeInTheDocument(); + }); + + it('calls initializeApp on component mount', () => { + render(); + expect(props.initializeApp).toHaveBeenCalledTimes(1); }); }); describe('mapStateToProps', () => { @@ -89,27 +177,27 @@ describe('ListView component', () => { beforeEach(() => { mapped = mapStateToProps(testState); }); - test('courseId loads from app.courseId', () => { + it('maps courseId from app.courseId selector', () => { expect(mapped.courseId).toEqual(selectors.app.courseId(testState)); }); - test('isLoaded loads from requests.isCompleted', () => { + it('maps isLoaded from requests.isCompleted selector', () => { expect(mapped.isLoaded).toEqual( selectors.requests.isCompleted(testState, { requestKey }), ); }); - test('hasError loads from requests.isFailed', () => { + it('maps hasError from requests.isFailed selector', () => { expect(mapped.hasError).toEqual( selectors.requests.isFailed(testState, { requestKey }), ); }); - test('isEmptySubmissionData loads from submissions.isEmptySubmissionData', () => { + it('maps isEmptySubmissionData from submissions.isEmptySubmissionData selector', () => { expect(mapped.isEmptySubmissionData).toEqual( selectors.submissions.isEmptySubmissionData(testState), ); }); }); describe('mapDispatchToProps', () => { - it('loads initializeApp from thunkActions.app.initialize', () => { + it('maps initializeApp to thunkActions.app.initialize', () => { expect(mapDispatchToProps.initializeApp).toEqual( thunkActions.app.initialize, );