test: Deprecate react-unit-test-utils 5/9 (#490)

* test: deprecate react-unit-test-utils 5/9

* fix: lint issues

* test: change fireEvent for userEvent

---------

Co-authored-by: diana-villalvazo-wgu <diana.villalvazo@wgu.edu>
This commit is contained in:
Victor Navarro
2025-08-28 09:05:42 -06:00
committed by GitHub
parent 1f16468bee
commit 99573f1d93
10 changed files with 820 additions and 575 deletions

View File

@@ -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(() => <div data-testid="data-table">DataTable</div>),
}));
jest.mock('./ResultsSummary', () => jest.fn(() => <div data-testid="results-summary">ResultsSummary</div>));
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(<HistoryTable {...props} />);
});
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(<ResultsSummary {...entry1.resultsSummary} />);
expect(rows[0].user).toEqual(<span className="wrap-text-in-cell">{entry1.user}</span>);
expect(
rows[0].filename,
).toEqual(<span className="wrap-text-in-cell">{entry1.originalFilename}</span>);
expect(rows[1].resultsSummary).toEqual(<ResultsSummary {...entry2.resultsSummary} />);
expect(rows[1].user).toEqual(<span className="wrap-text-in-cell">{entry2.user}</span>);
expect(
rows[1].filename,
).toEqual(<span className="wrap-text-in-cell">{entry2.originalFilename}</span>);
});
});
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(<div>{result.filename}</div>);
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(<div>{result.user}</div>);
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(<div>{result.resultsSummary}</div>);
expect(ResultsSummary).toHaveBeenCalledWith(mockRow.resultsSummary, {});
expect(screen.getByTestId('results-summary')).toBeInTheDocument();
});
});
describe('component', () => {
it('renders DataTable with empty data when no history provided', () => {
render(<HistoryTable />);
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(
<HistoryTable bulkManagementHistory={mockBulkManagementHistory} />,
);
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(
<HistoryTable bulkManagementHistory={mockBulkManagementHistory} />,
);
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);
});
});
});

View File

@@ -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": <span
className="wrap-text-in-cell"
>
blue.png
</span>,
"resultsSummary": <ResultsSummary
courseId="Da Bu Dee"
rowId={12}
text="Da ba daa"
/>,
"timeUploaded": "65",
"user": <span
className="wrap-text-in-cell"
>
Eifel
</span>,
},
{
"filename": <span
className="wrap-text-in-cell"
>
allStar.jpg
</span>,
"resultsSummary": <ResultsSummary
courseId="rockstar"
rowId={2}
text="all that glitters is gold"
/>,
"timeUploaded": "2000s?",
"user": <span
className="wrap-text-in-cell"
>
Smashmouth
</span>,
},
]
`;
exports[`HistoryTable component snapshot snapshot - loads formatted table 1`] = `
<DataTable
className="table-striped"
columns={
[
{
"Header": "Gradebook",
"accessor": "filename",
"columnSortable": false,
"width": "col-5",
},
{
"Header": "Download Summary",
"accessor": "resultsSummary",
"columnSortable": false,
"width": "col",
},
{
"Header": "Who",
"accessor": "user",
"columnSortable": false,
"width": "col-1",
},
{
"Header": "When",
"accessor": "timeUploaded",
"columnSortable": false,
"width": "col",
},
]
}
data={
[
{
"filename": <span
className="wrap-text-in-cell"
>
blue.png
</span>,
"resultsSummary": <ResultsSummary
courseId="Da Bu Dee"
rowId={12}
text="Da ba daa"
/>,
"timeUploaded": "65",
"user": <span
className="wrap-text-in-cell"
>
Eifel
</span>,
},
{
"filename": <span
className="wrap-text-in-cell"
>
allStar.jpg
</span>,
"resultsSummary": <ResultsSummary
courseId="rockstar"
rowId={2}
text="all that glitters is gold"
/>,
"timeUploaded": "2000s?",
"user": <span
className="wrap-text-in-cell"
>
Smashmouth
</span>,
},
]
}
hasFixedColumnWidths={true}
itemCount={2}
/>
`;

View File

@@ -1,72 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StudentGroupsFilter component render snapshot 1`] = `
<Fragment>
<SelectGroup
id="Tracks"
label="Tracks"
onChange={[MockFunction]}
options={
[
<option
value="Track-All"
>
Track-All
</option>,
<option
value="v1"
>
n1
</option>,
<option
value="v2"
>
n2
</option>,
<option
value="v3"
>
n3
</option>,
<option
value="v4"
>
n4
</option>,
]
}
value="test-track"
/>
<SelectGroup
disabled={false}
id="Cohorts"
label="Cohorts"
onChange={[MockFunction]}
options={
[
<option
value="Cohort-All"
>
Cohort-All
</option>,
<option
value="v1"
>
n1
</option>,
<option
value="v2"
>
n2
</option>,
<option
value="v3"
>
n3
</option>,
]
}
value="test-cohort"
/>
</Fragment>
`;

View File

@@ -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(() => <div data-testid="select-group">SelectGroup</div>));
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(<StudentGroupsFilter updateQueryParams={updateQueryParams} />);
});
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(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
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(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
expect(SelectGroup).toHaveBeenCalledTimes(2);
expect(screen.getAllByTestId('select-group')).toHaveLength(2);
});
describe('tracks SelectGroup', () => {
it('renders tracks SelectGroup with correct props', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
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(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
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(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
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(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
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(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
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(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
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(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
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(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
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(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const cohortsCall = SelectGroup.mock.calls[1][0];
expect(cohortsCall.options).toHaveLength(1);
});
});
});

View File

@@ -1,139 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradebookHeader component render default view shapshot 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="test-dashboard-url"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
test-course-id
</h2>
</div>
</div>
`;
exports[`GradebookHeader component render frozen grades snapshot: show frozen warning 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="test-dashboard-url"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
test-course-id
</h2>
</div>
<div
className="alert alert-warning"
role="alert"
>
The grades for this course are now frozen. Editing of grades is no longer allowed.
</div>
</div>
`;
exports[`GradebookHeader component render show bulk management snapshot: show toggle view message button with handleToggleViewClick method 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="test-dashboard-url"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
test-course-id
</h2>
<Button
onClick={[MockFunction hooks.handleToggleViewClick]}
variant="tertiary"
>
toggle-view-message
</Button>
</div>
</div>
`;
exports[`GradebookHeader component render user cannot view gradebook snapshot: show unauthorized warning 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="test-dashboard-url"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
test-course-id
</h2>
</div>
<div
className="alert alert-warning"
role="alert"
>
You are not authorized to view the gradebook for this course.
</div>
</div>
`;

View File

@@ -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(<GradebookHeader />);
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(<GradebookHeader />);
const header = screen.getByText('Gradebook').closest('.gradebook-header');
expect(header).toHaveClass('gradebook-header');
});
it('renders back to dashboard link', () => {
render(<GradebookHeader />);
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(<GradebookHeader />);
const title = screen.getByRole('heading', { level: 1 });
expect(title).toHaveTextContent('Gradebook');
});
it('renders course ID subtitle', () => {
render(<GradebookHeader />);
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(<GradebookHeader />);
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(<GradebookHeader />);
expect(instructorDashboardUrl).toHaveBeenCalled();
});
it('calls useGradebookHeaderData hook', () => {
render(<GradebookHeader />);
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(<GradebookHeader />);
expect(screen.getByRole('button')).toBeInTheDocument();
});
it('displays correct button text from toggleViewMessage', () => {
render(<GradebookHeader />);
const toggleButton = screen.getByRole('button');
expect(toggleButton).toHaveTextContent('View Bulk Management History');
});
it('calls handleToggleViewClick when button is clicked', async () => {
render(<GradebookHeader />);
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(<GradebookHeader />);
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(<GradebookHeader />);
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(<GradebookHeader />);
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(<GradebookHeader />);
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(<GradebookHeader />);
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(<GradebookHeader />);
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(<GradebookHeader />);
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(<GradebookHeader />);
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(<GradebookHeader />);
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(<GradebookHeader />);
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(<GradebookHeader />);
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();
});
});
});

View File

@@ -1,19 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BulkManagementControls render snapshot - show - network and import buttons 1`] = `
<div
className="d-flex"
>
<NetworkButton
label={
{
"defaultMessage": "Download Grades",
"description": "A labeled button that allows an admin user to download course grades all at once (in bulk).",
"id": "gradebook.GradesView.BulkManagementControls.bulkManagementLabel",
}
}
onClick={[MockFunction]}
/>
<ImportGradesButton />
</div>
`;

View File

@@ -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(() => <div data-testid="network-button">NetworkButton</div>));
jest.mock('../ImportGradesButton', () => jest.fn(() => (
<div data-testid="import-grades-button">ImportGradesButton</div>
)));
jest.mock('./hooks', () => jest.fn());
const hookProps = {
show: true,
handleClickExportGrades: jest.fn(),
};
useBulkManagementControlsData.mockReturnValue(hookProps);
initializeMocks();
describe('BulkManagementControls', () => {
describe('behavior', () => {
shallow(<BulkManagementControls />);
expect(useBulkManagementControlsData).toHaveBeenCalledWith();
const mockHandleClickExportGrades = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
describe('render', () => {
test('snapshot - show - network and import buttons', () => {
expect(shallow(<BulkManagementControls />).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(<BulkManagementControls />).isEmptyRender()).toEqual(true);
it('renders nothing when show is false', () => {
render(<BulkManagementControls />);
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(<BulkManagementControls />);
expect(NetworkButton).not.toHaveBeenCalled();
});
it('does not render ImportGradesButton when show is false', () => {
render(<BulkManagementControls />);
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(<BulkManagementControls />);
const containerDiv = screen.getByTestId('network-button').parentElement;
expect(containerDiv).toHaveClass('d-flex');
});
it('renders NetworkButton with correct props', () => {
render(<BulkManagementControls />);
expect(NetworkButton).toHaveBeenCalledWith(
{
label: messages.downloadGradesBtn,
onClick: mockHandleClickExportGrades,
},
{},
);
expect(screen.getByTestId('network-button')).toBeInTheDocument();
});
it('renders ImportGradesButton', () => {
render(<BulkManagementControls />);
expect(ImportGradesButton).toHaveBeenCalledWith({}, {});
expect(screen.getByTestId('import-grades-button')).toBeInTheDocument();
});
it('calls handleClickExportGrades when NetworkButton is clicked', () => {
render(<BulkManagementControls />);
const networkButtonCall = NetworkButton.mock.calls[0][0];
const { onClick } = networkButtonCall;
onClick();
expect(mockHandleClickExportGrades).toHaveBeenCalledTimes(1);
});
it('passes correct label to NetworkButton', () => {
render(<BulkManagementControls />);
const networkButtonCall = NetworkButton.mock.calls[0][0];
expect(networkButtonCall.label).toBe(messages.downloadGradesBtn);
});
it('renders both buttons in the correct order', () => {
render(<BulkManagementControls />);
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(<BulkManagementControls />);
expect(useBulkManagementControlsData).toHaveBeenCalledTimes(1);
});
it('uses the show value from hook to determine rendering', () => {
useBulkManagementControlsData.mockReturnValue({
show: false,
handleClickExportGrades: mockHandleClickExportGrades,
});
render(<BulkManagementControls />);
expect(screen.queryByTestId('network-button')).not.toBeInTheDocument();
useBulkManagementControlsData.mockReturnValue({
show: true,
handleClickExportGrades: mockHandleClickExportGrades,
});
render(<BulkManagementControls />);
expect(screen.getByTestId('network-button')).toBeInTheDocument();
});
it('passes handleClickExportGrades from hook to NetworkButton', () => {
const customHandler = jest.fn();
useBulkManagementControlsData.mockReturnValue({
show: true,
handleClickExportGrades: customHandler,
});
render(<BulkManagementControls />);
const networkButtonCall = NetworkButton.mock.calls[0][0];
expect(networkButtonCall.onClick).toBe(customHandler);
});
});
});

View File

@@ -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(<HistoryHeader {...props} />).snapshot).toMatchSnapshot();
});
it('renders header with label and value', () => {
render(<HistoryHeader {...defaultProps} />);
expect(screen.getByText('Test Label:')).toBeInTheDocument();
expect(screen.getByText('Test Value')).toBeInTheDocument();
});
it('renders header element with correct classes', () => {
render(<HistoryHeader {...defaultProps} />);
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(<HistoryHeader {...props} />);
expect(screen.getByText('String Value')).toBeInTheDocument();
});
it('renders with number value', () => {
const props = {
...defaultProps,
value: 85,
};
render(<HistoryHeader {...props} />);
expect(screen.getByText('85')).toBeInTheDocument();
});
it('renders with null value (default prop)', () => {
const props = {
id: 'test-id',
label: 'Test Label',
};
render(<HistoryHeader {...props} />);
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: <strong>Bold Label</strong>,
};
render(<HistoryHeader {...props} />);
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(<HistoryHeader {...props} />);
const headerElement = screen.getByText('Test Label:');
expect(headerElement).toHaveClass('grade-history-assignment-name');
});
it('renders container structure correctly', () => {
render(<HistoryHeader {...defaultProps} />);
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',
);
});
});

View File

@@ -1,15 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HistoryHeader Component snapshot 1`] = `
<div>
<div
className="grade-history-header grade-history-water"
>
Brita
:
</div>
<div>
hydration
</div>
</div>
`;