diff --git a/src/components/BulkManagementHistoryView/HistoryTable.test.jsx b/src/components/BulkManagementHistoryView/HistoryTable.test.jsx index 5fa40b9..35369af 100644 --- a/src/components/BulkManagementHistoryView/HistoryTable.test.jsx +++ b/src/components/BulkManagementHistoryView/HistoryTable.test.jsx @@ -1,108 +1,189 @@ -/* eslint-disable import/no-named-as-default */ import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; +import { render, screen, initializeMocks } from 'testUtilsExtra'; import { DataTable } from '@openedx/paragon'; import selectors from 'data/selectors'; import { bulkManagementColumns } from 'data/constants/app'; +import { HistoryTable, mapHistoryRows, mapStateToProps } from './HistoryTable'; import ResultsSummary from './ResultsSummary'; -import { HistoryTable, mapStateToProps } from './HistoryTable'; -jest.mock('@openedx/paragon', () => ({ DataTable: () => 'DataTable' })); +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); +initializeMocks(); -jest.mock('@edx/frontend-platform/i18n', () => ({ - defineMessages: m => m, - FormattedMessage: () => 'FormattedMessage', +jest.mock('@openedx/paragon', () => ({ + DataTable: jest.fn(() =>
DataTable
), })); +jest.mock('./ResultsSummary', () => jest.fn(() =>
ResultsSummary
)); jest.mock('data/selectors', () => ({ __esModule: true, default: { grades: { - bulkManagementHistoryEntries: jest.fn(state => ({ historyEntries: state })), + bulkManagementHistoryEntries: jest.fn(), }, }, })); -jest.mock('./ResultsSummary', () => 'ResultsSummary'); describe('HistoryTable', () => { - describe('component', () => { - const entry1 = { - originalFilename: 'blue.png', - user: 'Eifel', - timeUploaded: '65', + beforeEach(() => { + jest.clearAllMocks(); + }); + + const mockBulkManagementHistory = [ + { + originalFilename: 'test-file-1.csv', + user: 'test-user-1', + timeUploaded: '2025-01-01T10:00:00Z', resultsSummary: { - rowId: 12, - courseId: 'Da Bu Dee', - text: 'Da ba daa', + rowId: 1, + text: 'Download results 1', }, - }; - const entry2 = { - originalFilename: 'allStar.jpg', - user: 'Smashmouth', - timeUploaded: '2000s?', + }, + { + originalFilename: 'test-file-2.csv', + user: 'test-user-2', + timeUploaded: '2025-01-02T10:00:00Z', resultsSummary: { - courseId: 'rockstar', rowId: 2, - text: 'all that glitters is gold', + text: 'Download results 2', }, + }, + ]; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('mapHistoryRows', () => { + const mockRow = { + resultsSummary: { + rowId: 1, + text: 'Download results', + }, + originalFilename: 'test-file.csv', + user: 'test-user', + timeUploaded: '2025-01-01T10:00:00Z', }; - const props = { - bulkManagementHistory: [entry1, entry2], - }; - let el; - describe('snapshot', () => { - beforeEach(() => { - el = shallow(); - }); - test('snapshot - loads formatted table', () => { - expect(el.snapshot).toMatchSnapshot(); - }); - describe('history table', () => { - let table; - beforeEach(() => { - table = el.instance.findByType(DataTable); - }); - describe('data (from bulkManagementHistory.map(this.formatHistoryRow)', () => { - const fieldAssertions = [ - 'maps resultsSummay to ResultsSummary', - 'wraps filename and user', - 'forwards the rest', - ]; - test(`snapshot: ${fieldAssertions.join(', ')}`, () => { - expect(table[0].props.data).toMatchSnapshot(); - }); - test(fieldAssertions.join(', '), () => { - const rows = table[0].props.data; - expect(rows[0].resultsSummary).toEqual(); - expect(rows[0].user).toEqual({entry1.user}); - expect( - rows[0].filename, - ).toEqual({entry1.originalFilename}); - expect(rows[1].resultsSummary).toEqual(); - expect(rows[1].user).toEqual({entry2.user}); - expect( - rows[1].filename, - ).toEqual({entry2.originalFilename}); - }); - }); - test('columns from bulkManagementColumns', () => { - expect(table[0].props.columns).toEqual(bulkManagementColumns); - }); - }); + + it('transforms row data correctly', () => { + const result = mapHistoryRows(mockRow); + + expect(result).toHaveProperty('resultsSummary'); + expect(result).toHaveProperty('filename'); + expect(result).toHaveProperty('user'); + expect(result).toHaveProperty('timeUploaded'); + expect(result.timeUploaded).toBe('2025-01-01T10:00:00Z'); + }); + + it('wraps filename in span with correct class', () => { + const result = mapHistoryRows(mockRow); + render(
{result.filename}
); + + const filenameSpan = screen.getByText('test-file.csv'); + expect(filenameSpan).toBeInTheDocument(); + expect(filenameSpan).toHaveClass('wrap-text-in-cell'); + }); + + it('wraps user in span with correct class', () => { + const result = mapHistoryRows(mockRow); + render(
{result.user}
); + + const userSpan = screen.getByText('test-user'); + expect(userSpan).toBeInTheDocument(); + expect(userSpan).toHaveClass('wrap-text-in-cell'); + }); + + it('renders ResultsSummary component with correct props', () => { + const result = mapHistoryRows(mockRow); + render(
{result.resultsSummary}
); + + expect(ResultsSummary).toHaveBeenCalledWith(mockRow.resultsSummary, {}); + expect(screen.getByTestId('results-summary')).toBeInTheDocument(); + }); + }); + + describe('component', () => { + it('renders DataTable with empty data when no history provided', () => { + render(); + + expect(DataTable).toHaveBeenCalledWith( + { + data: [], + hasFixedColumnWidths: true, + columns: bulkManagementColumns, + className: 'table-striped', + itemCount: 0, + }, + {}, + ); + expect(screen.getByTestId('data-table')).toBeInTheDocument(); + }); + + it('renders DataTable with mapped history data', () => { + render( + , + ); + + expect(DataTable).toHaveBeenCalledWith( + { + data: expect.arrayContaining([ + expect.objectContaining({ + filename: expect.any(Object), + user: expect.any(Object), + resultsSummary: expect.any(Object), + timeUploaded: '2025-01-01T10:00:00Z', + }), + expect.objectContaining({ + filename: expect.any(Object), + user: expect.any(Object), + resultsSummary: expect.any(Object), + timeUploaded: '2025-01-02T10:00:00Z', + }), + ]), + hasFixedColumnWidths: true, + columns: bulkManagementColumns, + className: 'table-striped', + itemCount: 2, + }, + {}, + ); + }); + + it('passes correct props to DataTable', () => { + render( + , + ); + + const dataTableCall = DataTable.mock.calls[0][0]; + expect(dataTableCall.hasFixedColumnWidths).toBe(true); + expect(dataTableCall.columns).toBe(bulkManagementColumns); + expect(dataTableCall.className).toBe('table-striped'); + expect(dataTableCall.itemCount).toBe(mockBulkManagementHistory.length); }); }); describe('mapStateToProps', () => { - const testState = { a: 'simple', test: 'state' }; - let mapped; + const mockState = { test: 'state' }; + const mockHistoryEntries = [ + { originalFilename: 'file1.csv', user: 'user1' }, + { originalFilename: 'file2.csv', user: 'user2' }, + ]; + beforeEach(() => { - mapped = mapStateToProps(testState); + selectors.grades.bulkManagementHistoryEntries.mockReturnValue( + mockHistoryEntries, + ); }); - test('bulkManagementHistory from grades.bulkManagementHistoryEntries', () => { + + it('maps bulkManagementHistory from selector', () => { + const result = mapStateToProps(mockState); + expect( - mapped.bulkManagementHistory, - ).toEqual(selectors.grades.bulkManagementHistoryEntries(testState)); + selectors.grades.bulkManagementHistoryEntries, + ).toHaveBeenCalledWith(mockState); + expect(result.bulkManagementHistory).toBe(mockHistoryEntries); }); }); }); diff --git a/src/components/BulkManagementHistoryView/__snapshots__/HistoryTable.test.jsx.snap b/src/components/BulkManagementHistoryView/__snapshots__/HistoryTable.test.jsx.snap deleted file mode 100644 index 1dd5c74..0000000 --- a/src/components/BulkManagementHistoryView/__snapshots__/HistoryTable.test.jsx.snap +++ /dev/null @@ -1,118 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`HistoryTable component snapshot history table data (from bulkManagementHistory.map(this.formatHistoryRow) snapshot: maps resultsSummay to ResultsSummary, wraps filename and user, forwards the rest 1`] = ` -[ - { - "filename": - blue.png - , - "resultsSummary": , - "timeUploaded": "65", - "user": - Eifel - , - }, - { - "filename": - allStar.jpg - , - "resultsSummary": , - "timeUploaded": "2000s?", - "user": - Smashmouth - , - }, -] -`; - -exports[`HistoryTable component snapshot snapshot - loads formatted table 1`] = ` - - blue.png - , - "resultsSummary": , - "timeUploaded": "65", - "user": - Eifel - , - }, - { - "filename": - allStar.jpg - , - "resultsSummary": , - "timeUploaded": "2000s?", - "user": - Smashmouth - , - }, - ] - } - hasFixedColumnWidths={true} - itemCount={2} -/> -`; diff --git a/src/components/GradebookFilters/StudentGroupsFilter/__snapshots__/index.test.jsx.snap b/src/components/GradebookFilters/StudentGroupsFilter/__snapshots__/index.test.jsx.snap deleted file mode 100644 index 33f137b..0000000 --- a/src/components/GradebookFilters/StudentGroupsFilter/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,72 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`StudentGroupsFilter component render snapshot 1`] = ` - - - Track-All - , - , - , - , - , - ] - } - value="test-track" - /> - - Cohort-All - , - , - , - , - ] - } - value="test-cohort" - /> - -`; diff --git a/src/components/GradebookFilters/StudentGroupsFilter/index.test.jsx b/src/components/GradebookFilters/StudentGroupsFilter/index.test.jsx index 7f2fa52..5992272 100644 --- a/src/components/GradebookFilters/StudentGroupsFilter/index.test.jsx +++ b/src/components/GradebookFilters/StudentGroupsFilter/index.test.jsx @@ -1,84 +1,167 @@ import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; -import { useIntl } from '@edx/frontend-platform/i18n'; +import { render, screen, initializeMocks } from 'testUtilsExtra'; import SelectGroup from '../SelectGroup'; +import { StudentGroupsFilter } from './index'; import useStudentGroupsFilterData from './hooks'; -import StudentGroupsFilter from '.'; -jest.mock('../SelectGroup', () => 'SelectGroup'); -jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() })); +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); +jest.mock('../SelectGroup', () => jest.fn(() =>
SelectGroup
)); +jest.mock('./hooks', () => jest.fn()); -const props = { - cohorts: { - value: 'test-cohort', +initializeMocks(); + +describe('StudentGroupsFilter', () => { + const mockUpdateQueryParams = jest.fn(); + + const mockTracksData = { + value: 'test-track-value', entries: [ - { value: 'v1', name: 'n1' }, - { value: 'v2', name: 'n2' }, - { value: 'v3', name: 'n3' }, + { value: 'track1', name: 'Track 1' }, + { value: 'track2', name: 'Track 2' }, + ], + handleChange: jest.fn(), + }; + + const mockCohortsData = { + value: 'test-cohort-value', + entries: [ + { value: 'cohort1', name: 'Cohort 1' }, + { value: 'cohort2', name: 'Cohort 2' }, ], handleChange: jest.fn(), isDisabled: false, - }, - tracks: { - value: 'test-track', - entries: [ - { value: 'v1', name: 'n1' }, - { value: 'v2', name: 'n2' }, - { value: 'v3', name: 'n3' }, - { value: 'v4', name: 'n4' }, - ], - handleChange: jest.fn(), - }, -}; -useStudentGroupsFilterData.mockReturnValue(props); -const updateQueryParams = jest.fn(); + }; -let el; -describe('StudentGroupsFilter component', () => { - beforeAll(() => { + beforeEach(() => { jest.clearAllMocks(); - el = shallow(); - }); - describe('behavior', () => { - it('initializes hooks', () => { - expect(useStudentGroupsFilterData).toHaveBeenCalledWith({ updateQueryParams }); - expect(useIntl).toHaveBeenCalledWith(); + useStudentGroupsFilterData.mockReturnValue({ + tracks: mockTracksData, + cohorts: mockCohortsData, }); }); - describe('render', () => { - test('snapshot', () => { - expect(el.snapshot).toMatchSnapshot(); + + it('calls useStudentGroupsFilterData hook with updateQueryParams', () => { + render(); + + expect(useStudentGroupsFilterData).toHaveBeenCalledWith({ + updateQueryParams: mockUpdateQueryParams, }); - test('track options', () => { - const { - options, - onChange, - value, - } = el.instance.findByType(SelectGroup)[0].props; - expect(value).toEqual(props.tracks.value); - expect(onChange).toEqual(props.tracks.handleChange); - expect(options.length).toEqual(5); - const testEntry = props.tracks.entries[0]; - const optionProps = options[1].props; - expect(optionProps.value).toEqual(testEntry.value); - expect(optionProps.children).toEqual(testEntry.name); + }); + + it('renders two SelectGroup components', () => { + render(); + + expect(SelectGroup).toHaveBeenCalledTimes(2); + expect(screen.getAllByTestId('select-group')).toHaveLength(2); + }); + + describe('tracks SelectGroup', () => { + it('renders tracks SelectGroup with correct props', () => { + render(); + + const tracksCall = SelectGroup.mock.calls[0][0]; + expect(tracksCall.id).toBe('Tracks'); + expect(tracksCall.value).toBe(mockTracksData.value); + expect(tracksCall.onChange).toBe(mockTracksData.handleChange); }); - test('cohort options', () => { - const { - options, - onChange, - disabled, - value, - } = el.instance.findByType(SelectGroup)[1].props; - expect(value).toEqual(props.cohorts.value); - expect(disabled).toEqual(false); - expect(onChange).toEqual(props.cohorts.handleChange); - expect(options.length).toEqual(4); - const testEntry = props.cohorts.entries[0]; - const optionProps = options[1].props; - expect(optionProps.value).toEqual(testEntry.value); - expect(optionProps.children).toEqual(testEntry.name); + + it('includes trackAll option in tracks SelectGroup', () => { + render(); + + const tracksCall = SelectGroup.mock.calls[0][0]; + const { options } = tracksCall; + + expect(options).toHaveLength(3); + expect(options[0].props.value).toBeDefined(); + expect(options[0].props.children).toBeDefined(); + }); + + it('includes track entries in tracks SelectGroup options', () => { + render(); + + const tracksCall = SelectGroup.mock.calls[0][0]; + const { options } = tracksCall; + + expect(options[1].props.value).toBe('track1'); + expect(options[1].props.children).toBe('Track 1'); + expect(options[2].props.value).toBe('track2'); + expect(options[2].props.children).toBe('Track 2'); + }); + }); + + describe('cohorts SelectGroup', () => { + it('renders cohorts SelectGroup with correct props', () => { + render(); + + const cohortsCall = SelectGroup.mock.calls[1][0]; + expect(cohortsCall.id).toBe('Cohorts'); + expect(cohortsCall.value).toBe(mockCohortsData.value); + expect(cohortsCall.onChange).toBe(mockCohortsData.handleChange); + expect(cohortsCall.disabled).toBe(mockCohortsData.isDisabled); + }); + + it('includes cohortAll option in cohorts SelectGroup', () => { + render(); + + const cohortsCall = SelectGroup.mock.calls[1][0]; + const { options } = cohortsCall; + + expect(options).toHaveLength(3); + expect(options[0].props.value).toBeDefined(); + expect(options[0].props.children).toBeDefined(); + }); + + it('includes cohort entries in cohorts SelectGroup options', () => { + render(); + + const cohortsCall = SelectGroup.mock.calls[1][0]; + const { options } = cohortsCall; + + expect(options[1].props.value).toBe('cohort1'); + expect(options[1].props.children).toBe('Cohort 1'); + expect(options[2].props.value).toBe('cohort2'); + expect(options[2].props.children).toBe('Cohort 2'); + }); + + it('passes disabled state to cohorts SelectGroup', () => { + useStudentGroupsFilterData.mockReturnValue({ + tracks: mockTracksData, + cohorts: { ...mockCohortsData, isDisabled: true }, + }); + + render(); + + const cohortsCall = SelectGroup.mock.calls[1][0]; + expect(cohortsCall.disabled).toBe(true); + }); + }); + + describe('with empty entries', () => { + it('handles empty tracks entries', () => { + useStudentGroupsFilterData.mockReturnValue({ + tracks: { ...mockTracksData, entries: [] }, + cohorts: mockCohortsData, + }); + + render(); + + const tracksCall = SelectGroup.mock.calls[0][0]; + expect(tracksCall.options).toHaveLength(1); + }); + + it('handles empty cohorts entries', () => { + useStudentGroupsFilterData.mockReturnValue({ + tracks: mockTracksData, + cohorts: { ...mockCohortsData, entries: [] }, + }); + + render(); + + const cohortsCall = SelectGroup.mock.calls[1][0]; + expect(cohortsCall.options).toHaveLength(1); }); }); }); diff --git a/src/components/GradebookHeader/__snapshots__/index.test.jsx.snap b/src/components/GradebookHeader/__snapshots__/index.test.jsx.snap deleted file mode 100644 index bf0c9e2..0000000 --- a/src/components/GradebookHeader/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,139 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GradebookHeader component render default view shapshot 1`] = ` -
- - - Back to Dashboard - -

- Gradebook -

-
-

- test-course-id -

-
-
-`; - -exports[`GradebookHeader component render frozen grades snapshot: show frozen warning 1`] = ` -
- - - Back to Dashboard - -

- Gradebook -

-
-

- test-course-id -

-
-
- The grades for this course are now frozen. Editing of grades is no longer allowed. -
-
-`; - -exports[`GradebookHeader component render show bulk management snapshot: show toggle view message button with handleToggleViewClick method 1`] = ` -
- - - Back to Dashboard - -

- Gradebook -

-
-

- test-course-id -

- -
-
-`; - -exports[`GradebookHeader component render user cannot view gradebook snapshot: show unauthorized warning 1`] = ` -
- - - Back to Dashboard - -

- Gradebook -

-
-

- test-course-id -

-
-
- You are not authorized to view the gradebook for this course. -
-
-`; diff --git a/src/components/GradebookHeader/index.test.jsx b/src/components/GradebookHeader/index.test.jsx index 52d0a62..0f93f95 100644 --- a/src/components/GradebookHeader/index.test.jsx +++ b/src/components/GradebookHeader/index.test.jsx @@ -1,77 +1,303 @@ import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; -import { useIntl } from '@edx/frontend-platform/i18n'; -import { Button } from '@openedx/paragon'; +import { render, screen, initializeMocks } from 'testUtilsExtra'; +import userEvent from '@testing-library/user-event'; -import { formatMessage } from 'testUtils'; import { instructorDashboardUrl } from 'data/services/lms/urls'; +import { GradebookHeader } from './index'; import useGradebookHeaderData from './hooks'; -import GradebookHeader from '.'; +import messages from './messages'; -jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() })); +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); jest.mock('data/services/lms/urls', () => ({ instructorDashboardUrl: jest.fn(), })); +jest.mock('./hooks', () => jest.fn()); -instructorDashboardUrl.mockReturnValue('test-dashboard-url'); +initializeMocks(); -const hookProps = { - areGradesFrozen: false, - canUserViewGradebook: true, - courseId: 'test-course-id', - handleToggleViewClick: jest.fn().mockName('hooks.handleToggleViewClick'), - showBulkManagement: false, - toggleViewMessage: { defaultMessage: 'toggle-view-message' }, -}; -useGradebookHeaderData.mockReturnValue(hookProps); +describe('GradebookHeader', () => { + const mockHandleToggleViewClick = jest.fn(); -let el; -describe('GradebookHeader component', () => { - beforeAll(() => { - el = shallow(); + beforeEach(() => { + jest.clearAllMocks(); + instructorDashboardUrl.mockReturnValue('https://example.com/dashboard'); }); - describe('behavior', () => { - it('initializes hooks', () => { - expect(useGradebookHeaderData).toHaveBeenCalledWith(); - expect(useIntl).toHaveBeenCalledWith(); + + describe('basic rendering', () => { + beforeEach(() => { + useGradebookHeaderData.mockReturnValue({ + areGradesFrozen: false, + canUserViewGradebook: true, + courseId: 'course-v1:TestU+CS101+2024', + handleToggleViewClick: mockHandleToggleViewClick, + showBulkManagement: false, + toggleViewMessage: messages.toActivityLog, + }); + }); + + it('renders the main header container', () => { + render(); + const header = screen.getByText('Gradebook').closest('.gradebook-header'); + expect(header).toHaveClass('gradebook-header'); + }); + + it('renders back to dashboard link', () => { + render(); + const dashboardLink = screen.getByRole('link'); + expect(dashboardLink).toHaveAttribute( + 'href', + 'https://example.com/dashboard', + ); + expect(dashboardLink).toHaveClass('mb-3'); + expect(dashboardLink).toHaveTextContent('Back to Dashboard'); + }); + + it('renders gradebook title', () => { + render(); + const title = screen.getByRole('heading', { level: 1 }); + expect(title).toHaveTextContent('Gradebook'); + }); + + it('renders course ID subtitle', () => { + render(); + const subtitle = screen.getByRole('heading', { level: 2 }); + expect(subtitle).toHaveTextContent('course-v1:TestU+CS101+2024'); + expect(subtitle).toHaveClass('text-break'); + }); + + it('renders subtitle row with correct classes', () => { + render(); + const subtitleRow = screen.getByRole('heading', { + level: 2, + }).parentElement; + expect(subtitleRow).toHaveClass( + 'subtitle-row', + 'd-flex', + 'justify-content-between', + 'align-items-center', + ); + }); + + it('calls instructorDashboardUrl to get dashboard URL', () => { + render(); + expect(instructorDashboardUrl).toHaveBeenCalled(); + }); + + it('calls useGradebookHeaderData hook', () => { + render(); + expect(useGradebookHeaderData).toHaveBeenCalled(); }); }); - describe('render', () => { - describe('default view', () => { - test('shapshot', () => { - expect(el.snapshot).toMatchSnapshot(); + + describe('bulk management toggle button', () => { + describe('when showBulkManagement is true', () => { + beforeEach(() => { + useGradebookHeaderData.mockReturnValue({ + areGradesFrozen: false, + canUserViewGradebook: true, + courseId: 'course-v1:TestU+CS101+2024', + handleToggleViewClick: mockHandleToggleViewClick, + showBulkManagement: true, + toggleViewMessage: messages.toActivityLog, + }); + }); + + it('renders toggle view button', () => { + render(); + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + it('displays correct button text from toggleViewMessage', () => { + render(); + const toggleButton = screen.getByRole('button'); + expect(toggleButton).toHaveTextContent('View Bulk Management History'); + }); + + it('calls handleToggleViewClick when button is clicked', async () => { + render(); + const user = userEvent.setup(); + const toggleButton = screen.getByRole('button'); + + await user.click(toggleButton); + expect(mockHandleToggleViewClick).toHaveBeenCalledTimes(1); + }); + + it('displays correct message from toggleViewMessage', () => { + useGradebookHeaderData.mockReturnValue({ + areGradesFrozen: false, + canUserViewGradebook: true, + courseId: 'course-v1:TestU+CS101+2024', + handleToggleViewClick: mockHandleToggleViewClick, + showBulkManagement: true, + toggleViewMessage: messages.toGradesView, + }); + + render(); + const toggleButton = screen.getByRole('button'); + expect(toggleButton).toHaveTextContent('Return to Gradebook'); }); }); - describe('show bulk management', () => { + + describe('when showBulkManagement is false', () => { beforeEach(() => { - useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, showBulkManagement: true }); - el = shallow(); + useGradebookHeaderData.mockReturnValue({ + areGradesFrozen: false, + canUserViewGradebook: true, + courseId: 'course-v1:TestU+CS101+2024', + handleToggleViewClick: mockHandleToggleViewClick, + showBulkManagement: false, + toggleViewMessage: messages.toActivityLog, + }); }); - test('snapshot: show toggle view message button with handleToggleViewClick method', () => { - expect(el.snapshot).toMatchSnapshot(); - const { onClick } = el.instance.findByType(Button)[0].props; - expect(onClick).toEqual(hookProps.handleToggleViewClick); - expect(el.instance.findByType(Button)[0].children[0].el).toEqual(formatMessage(hookProps.toggleViewMessage)); + + it('does not render toggle view button', () => { + render(); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); }); }); - describe('frozen grades', () => { + }); + + describe('frozen grades warning', () => { + describe('when areGradesFrozen is true', () => { beforeEach(() => { - useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, areGradesFrozen: true }); - el = shallow(); + useGradebookHeaderData.mockReturnValue({ + areGradesFrozen: true, + canUserViewGradebook: true, + courseId: 'course-v1:TestU+CS101+2024', + handleToggleViewClick: mockHandleToggleViewClick, + showBulkManagement: false, + toggleViewMessage: messages.toActivityLog, + }); }); - test('snapshot: show frozen warning', () => { - expect(el.snapshot).toMatchSnapshot(); + + it('renders frozen warning alert', () => { + render(); + const alert = screen.getByRole('alert'); + expect(alert).toHaveClass('alert', 'alert-warning'); + expect(alert).toHaveTextContent( + 'The grades for this course are now frozen. Editing of grades is no longer allowed.', + ); }); }); - describe('user cannot view gradebook', () => { + + describe('when areGradesFrozen is false', () => { beforeEach(() => { - useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, canUserViewGradebook: false }); - el = shallow(); + useGradebookHeaderData.mockReturnValue({ + areGradesFrozen: false, + canUserViewGradebook: true, + courseId: 'course-v1:TestU+CS101+2024', + handleToggleViewClick: mockHandleToggleViewClick, + showBulkManagement: false, + toggleViewMessage: messages.toActivityLog, + }); }); - test('snapshot: show unauthorized warning', () => { - expect(el.snapshot).toMatchSnapshot(); + + it('does not render frozen warning alert', () => { + render(); + expect( + screen.queryByText( + 'The grades for this course are now frozen. Editing of grades is no longer allowed.', + ), + ).not.toBeInTheDocument(); }); }); }); + + describe('unauthorized warning', () => { + describe('when canUserViewGradebook is false', () => { + beforeEach(() => { + useGradebookHeaderData.mockReturnValue({ + areGradesFrozen: false, + canUserViewGradebook: false, + courseId: 'course-v1:TestU+CS101+2024', + handleToggleViewClick: mockHandleToggleViewClick, + showBulkManagement: false, + toggleViewMessage: messages.toActivityLog, + }); + }); + + it('renders unauthorized warning alert', () => { + render(); + const alert = screen.getByRole('alert'); + expect(alert).toHaveClass('alert', 'alert-warning'); + expect(alert).toHaveTextContent( + 'You are not authorized to view the gradebook for this course.', + ); + }); + }); + + describe('when canUserViewGradebook is true', () => { + beforeEach(() => { + useGradebookHeaderData.mockReturnValue({ + areGradesFrozen: false, + canUserViewGradebook: true, + courseId: 'course-v1:TestU+CS101+2024', + handleToggleViewClick: mockHandleToggleViewClick, + showBulkManagement: false, + toggleViewMessage: messages.toActivityLog, + }); + }); + + it('does not render unauthorized warning alert', () => { + render(); + expect( + screen.queryByText( + 'You are not authorized to view the gradebook for this course.', + ), + ).not.toBeInTheDocument(); + }); + }); + }); + + describe('multiple warnings', () => { + it('renders both frozen and unauthorized warnings when both conditions are true', () => { + useGradebookHeaderData.mockReturnValue({ + areGradesFrozen: true, + canUserViewGradebook: false, + courseId: 'course-v1:TestU+CS101+2024', + handleToggleViewClick: mockHandleToggleViewClick, + showBulkManagement: false, + toggleViewMessage: messages.toActivityLog, + }); + + render(); + const alerts = screen.getAllByRole('alert'); + expect(alerts).toHaveLength(2); + + expect( + screen.getByText( + 'The grades for this course are now frozen. Editing of grades is no longer allowed.', + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + 'You are not authorized to view the gradebook for this course.', + ), + ).toBeInTheDocument(); + }); + }); + + describe('complete integration', () => { + it('renders all elements when showBulkManagement is true', () => { + useGradebookHeaderData.mockReturnValue({ + areGradesFrozen: false, + canUserViewGradebook: true, + courseId: 'course-v1:TestU+CS101+2024', + handleToggleViewClick: mockHandleToggleViewClick, + showBulkManagement: true, + toggleViewMessage: messages.toActivityLog, + }); + + render(); + + expect(screen.getByRole('link')).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument(); + expect(screen.getByRole('button')).toBeInTheDocument(); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + }); + }); }); diff --git a/src/components/GradesView/BulkManagementControls/__snapshots__/index.test.jsx.snap b/src/components/GradesView/BulkManagementControls/__snapshots__/index.test.jsx.snap deleted file mode 100644 index 64299ed..0000000 --- a/src/components/GradesView/BulkManagementControls/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`BulkManagementControls render snapshot - show - network and import buttons 1`] = ` -
- - -
-`; diff --git a/src/components/GradesView/BulkManagementControls/index.test.jsx b/src/components/GradesView/BulkManagementControls/index.test.jsx index 3a7ca39..27fde69 100644 --- a/src/components/GradesView/BulkManagementControls/index.test.jsx +++ b/src/components/GradesView/BulkManagementControls/index.test.jsx @@ -1,32 +1,163 @@ import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; +import { render, screen, initializeMocks } from 'testUtilsExtra'; +import NetworkButton from 'components/NetworkButton'; +import ImportGradesButton from '../ImportGradesButton'; + +import { BulkManagementControls } from './index'; import useBulkManagementControlsData from './hooks'; -import BulkManagementControls from '.'; - -jest.mock('../ImportGradesButton', () => 'ImportGradesButton'); -jest.mock('components/NetworkButton', () => 'NetworkButton'); +import messages from './messages'; +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); +jest.mock('components/NetworkButton', () => jest.fn(() =>
NetworkButton
)); +jest.mock('../ImportGradesButton', () => jest.fn(() => ( +
ImportGradesButton
+))); jest.mock('./hooks', () => jest.fn()); -const hookProps = { - show: true, - handleClickExportGrades: jest.fn(), -}; -useBulkManagementControlsData.mockReturnValue(hookProps); +initializeMocks(); describe('BulkManagementControls', () => { - describe('behavior', () => { - shallow(); - expect(useBulkManagementControlsData).toHaveBeenCalledWith(); + const mockHandleClickExportGrades = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); }); - describe('render', () => { - test('snapshot - show - network and import buttons', () => { - expect(shallow().snapshot).toMatchSnapshot(); + + describe('when show is false', () => { + beforeEach(() => { + useBulkManagementControlsData.mockReturnValue({ + show: false, + handleClickExportGrades: mockHandleClickExportGrades, + }); }); - test('snapshot - empty if show is not truthy', () => { - useBulkManagementControlsData.mockReturnValueOnce({ ...hookProps, show: false }); - expect(shallow().isEmptyRender()).toEqual(true); + + it('renders nothing when show is false', () => { + render(); + expect(screen.queryByTestId('network-button')).not.toBeInTheDocument(); + expect( + screen.queryByTestId('import-grades-button'), + ).not.toBeInTheDocument(); + }); + + it('does not render NetworkButton when show is false', () => { + render(); + expect(NetworkButton).not.toHaveBeenCalled(); + }); + + it('does not render ImportGradesButton when show is false', () => { + render(); + expect(ImportGradesButton).not.toHaveBeenCalled(); + }); + }); + + describe('when show is true', () => { + beforeEach(() => { + useBulkManagementControlsData.mockReturnValue({ + show: true, + handleClickExportGrades: mockHandleClickExportGrades, + }); + }); + + it('renders the container div with correct class when show is true', () => { + render(); + const containerDiv = screen.getByTestId('network-button').parentElement; + expect(containerDiv).toHaveClass('d-flex'); + }); + + it('renders NetworkButton with correct props', () => { + render(); + + expect(NetworkButton).toHaveBeenCalledWith( + { + label: messages.downloadGradesBtn, + onClick: mockHandleClickExportGrades, + }, + {}, + ); + expect(screen.getByTestId('network-button')).toBeInTheDocument(); + }); + + it('renders ImportGradesButton', () => { + render(); + + expect(ImportGradesButton).toHaveBeenCalledWith({}, {}); + expect(screen.getByTestId('import-grades-button')).toBeInTheDocument(); + }); + + it('calls handleClickExportGrades when NetworkButton is clicked', () => { + render(); + + const networkButtonCall = NetworkButton.mock.calls[0][0]; + const { onClick } = networkButtonCall; + + onClick(); + expect(mockHandleClickExportGrades).toHaveBeenCalledTimes(1); + }); + + it('passes correct label to NetworkButton', () => { + render(); + + const networkButtonCall = NetworkButton.mock.calls[0][0]; + expect(networkButtonCall.label).toBe(messages.downloadGradesBtn); + }); + + it('renders both buttons in the correct order', () => { + render(); + + expect(NetworkButton).toHaveBeenCalled(); + expect(ImportGradesButton).toHaveBeenCalled(); + + const networkButton = screen.getByTestId('network-button'); + const importButton = screen.getByTestId('import-grades-button'); + + expect(networkButton).toBeInTheDocument(); + expect(importButton).toBeInTheDocument(); + }); + }); + + describe('hook integration', () => { + it('calls useBulkManagementControlsData hook', () => { + useBulkManagementControlsData.mockReturnValue({ + show: true, + handleClickExportGrades: mockHandleClickExportGrades, + }); + + render(); + expect(useBulkManagementControlsData).toHaveBeenCalledTimes(1); + }); + + it('uses the show value from hook to determine rendering', () => { + useBulkManagementControlsData.mockReturnValue({ + show: false, + handleClickExportGrades: mockHandleClickExportGrades, + }); + + render(); + expect(screen.queryByTestId('network-button')).not.toBeInTheDocument(); + + useBulkManagementControlsData.mockReturnValue({ + show: true, + handleClickExportGrades: mockHandleClickExportGrades, + }); + + render(); + expect(screen.getByTestId('network-button')).toBeInTheDocument(); + }); + + it('passes handleClickExportGrades from hook to NetworkButton', () => { + const customHandler = jest.fn(); + useBulkManagementControlsData.mockReturnValue({ + show: true, + handleClickExportGrades: customHandler, + }); + + render(); + + const networkButtonCall = NetworkButton.mock.calls[0][0]; + expect(networkButtonCall.onClick).toBe(customHandler); }); }); }); diff --git a/src/components/GradesView/EditModal/HistoryHeader.test.jsx b/src/components/GradesView/EditModal/HistoryHeader.test.jsx index 63b2865..034026c 100644 --- a/src/components/GradesView/EditModal/HistoryHeader.test.jsx +++ b/src/components/GradesView/EditModal/HistoryHeader.test.jsx @@ -1,17 +1,104 @@ import React from 'react'; -import { shallow } from '@edx/react-unit-test-utils'; +import { render, screen, initializeMocks } from 'testUtilsExtra'; import HistoryHeader from './HistoryHeader'; +jest.unmock('@openedx/paragon'); +jest.unmock('react'); +jest.unmock('@edx/frontend-platform/i18n'); + +initializeMocks(); + describe('HistoryHeader', () => { - const props = { - id: 'water', - label: 'Brita', - value: 'hydration', + const defaultProps = { + id: 'test-id', + label: 'Test Label', + value: 'Test Value', }; - describe('Component', () => { - test('snapshot', () => { - expect(shallow().snapshot).toMatchSnapshot(); - }); + + it('renders header with label and value', () => { + render(); + + expect(screen.getByText('Test Label:')).toBeInTheDocument(); + expect(screen.getByText('Test Value')).toBeInTheDocument(); + }); + + it('renders header element with correct classes', () => { + render(); + + const headerElement = screen.getByText('Test Label:'); + expect(headerElement).toHaveClass('grade-history-header'); + expect(headerElement).toHaveClass('grade-history-test-id'); + }); + + it('renders with string value', () => { + const props = { + ...defaultProps, + value: 'String Value', + }; + + render(); + expect(screen.getByText('String Value')).toBeInTheDocument(); + }); + + it('renders with number value', () => { + const props = { + ...defaultProps, + value: 85, + }; + + render(); + expect(screen.getByText('85')).toBeInTheDocument(); + }); + + it('renders with null value (default prop)', () => { + const props = { + id: 'test-id', + label: 'Test Label', + }; + + render(); + expect(screen.getByText('Test Label:')).toBeInTheDocument(); + + const valueDiv = screen.getByText('Test Label:').nextSibling; + expect(valueDiv).toBeInTheDocument(); + expect(valueDiv).toBeEmptyDOMElement(); + }); + + it('renders with React node as label', () => { + const props = { + ...defaultProps, + label: Bold Label, + }; + + render(); + const strongElement = screen.getByText('Bold Label'); + expect(strongElement.tagName).toBe('STRONG'); + }); + + it('generates correct class name based on id', () => { + const props = { + ...defaultProps, + id: 'assignment-name', + }; + + render(); + const headerElement = screen.getByText('Test Label:'); + expect(headerElement).toHaveClass('grade-history-assignment-name'); + }); + + it('renders container structure correctly', () => { + render(); + + const headerElement = screen.getByText('Test Label:'); + const valueElement = screen.getByText('Test Value'); + + expect(headerElement).toBeInTheDocument(); + expect(valueElement).toBeInTheDocument(); + + expect(headerElement).toHaveClass( + 'grade-history-header', + 'grade-history-test-id', + ); }); }); diff --git a/src/components/GradesView/EditModal/__snapshots__/HistoryHeader.test.jsx.snap b/src/components/GradesView/EditModal/__snapshots__/HistoryHeader.test.jsx.snap deleted file mode 100644 index f006b2c..0000000 --- a/src/components/GradesView/EditModal/__snapshots__/HistoryHeader.test.jsx.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`HistoryHeader Component snapshot 1`] = ` -
-
- Brita - : -
-
- hydration -
-
-`;