test: Deprecate react-unit-test-utils 7/9 (#491)

* test: Deprecate react-unit-test-utils 7/9

* test: coverage on app.test.jsx

* test: improve test cases for toast

* test: improve coverage on gradebook table

* test: missing snap/shallow depr

---------

Co-authored-by: diana-villalvazo-wgu <diana.villalvazo@wgu.edu>
This commit is contained in:
Victor Navarro
2025-09-08 08:08:56 -06:00
committed by GitHub
parent a2a3af4ea3
commit 40d7167744
9 changed files with 273 additions and 325 deletions

View File

@@ -1,63 +1,63 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Route } from 'react-router-dom';
import store from 'data/store';
import GradebookPage from 'containers/GradebookPage';
import { render, screen } from '@testing-library/react';
import App from './App';
jest.mock('react-router-dom', () => ({
BrowserRouter: () => 'BrowserRouter',
Route: () => 'Route',
Routes: () => 'Routes',
Routes: ({ children }) => children,
Route: ({ element }) => element,
}));
jest.mock('@edx/frontend-platform/react', () => ({
AppProvider: () => 'AppProvider',
AppProvider: ({ children }) => children,
}));
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'FooterSlot' }));
jest.mock('data/store', () => 'testStore');
jest.mock('containers/GradebookPage', () => 'GradebookPage');
jest.mock('@edx/frontend-component-header', () => 'Header');
jest.mock('./head/Head', () => 'Head');
let el;
let secondChild;
jest.mock('@edx/frontend-component-header', () => ({
__esModule: true,
default: () => <div>Header</div>,
}));
describe('App router component', () => {
test('snapshot', () => {
expect(shallow(<App />).snapshot).toMatchSnapshot();
jest.mock('@edx/frontend-component-footer', () => ({
FooterSlot: () => <div>Footer</div>,
}));
jest.mock('./head/Head', () => ({
__esModule: true,
default: () => <div>Head</div>,
}));
jest.mock('containers/GradebookPage', () => ({
__esModule: true,
default: () => <div>Gradebook</div>,
}));
describe('App', () => {
beforeEach(() => {
render(<App />);
});
describe('component', () => {
beforeEach(() => {
el = shallow(<App />);
secondChild = el.instance.children;
});
describe('AppProvider', () => {
test('AppProvider is the parent component, passed the redux store props', () => {
expect(el.instance.type).toBe('AppProvider');
expect(el.instance.props.store).toEqual(store);
});
});
describe('Head', () => {
test('first child of AppProvider', () => {
expect(el.instance.children[0].type).toBe('Head');
});
});
describe('Router', () => {
test('second child of AppProvider', () => {
expect(secondChild[1].type).toBe('div');
});
test('Header is above/outside-of the routing', () => {
expect(secondChild[1].children[0].type).toBe('Header');
expect(secondChild[1].children[1].type).toBe('main');
});
test('Routing - GradebookPage is only route', () => {
expect(secondChild[1].findByType(Route)).toHaveLength(1);
expect(secondChild[1].findByType(Route)[0].props.path).toEqual('/:courseId');
expect(secondChild[1].findByType(Route)[0].props.element.type).toEqual(GradebookPage);
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('renders Head component', () => {
const head = screen.getByText('Head');
expect(head).toBeInTheDocument();
});
it('renders Header component', () => {
const header = screen.getByText('Header');
expect(header).toBeInTheDocument();
});
it('renders Footer component', () => {
const footer = screen.getByText('Footer');
expect(footer).toBeInTheDocument();
});
it('renders main content wrapper', () => {
const main = screen.getByRole('main');
expect(main).toBeInTheDocument();
const gradebook = screen.getByText('Gradebook');
expect(gradebook).toBeInTheDocument();
});
});

View File

@@ -1,21 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App router component snapshot 1`] = `
<AppProvider
store="testStore"
>
<Head />
<div>
<Header />
<main>
<Routes>
<Route
element={<GradebookPage />}
path="/:courseId"
/>
</Routes>
</main>
<FooterSlot />
</div>
</AppProvider>
`;

View File

@@ -1,47 +1,36 @@
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { getLocalizedPercentSign } from 'i18n/utils';
import { selectors } from 'data/redux/hooks';
import transforms from 'data/redux/transforms';
import React from 'react';
import { Headings } from 'data/constants/grades';
import LabelReplacements from './LabelReplacements';
import Fields from './Fields';
import GradeButton from './GradeButton';
import { initializeMocks, render } from '../../../testUtilsExtra';
import * as hooks from './hooks';
import messages from './messages';
import useGradebookTableData from './hooks';
jest.unmock('@openedx/paragon');
jest.unmock('react');
jest.unmock('@edx/frontend-platform/i18n');
jest.mock('i18n/utils', () => ({
getLocalizedPercentSign: () => '%',
}));
jest.mock('./GradeButton', () => 'GradeButton');
jest.mock('./Fields', () => jest.requireActual('testUtils').mockNestedComponents({
Username: 'Fields.Username',
Text: 'Fields.Text',
}));
jest.mock('./LabelReplacements', () => jest.requireActual('testUtils').mockNestedComponents({
TotalGradeLabelReplacement: 'LabelReplacements.TotalGradeLabelReplacement',
UsernameLabelReplacement: 'LabelReplacements.UsernameLabelReplacement',
MastersOnlyLabelReplacement: 'LabelReplacements.MastersOnlyLabelReplacement',
}));
let mockUseAllGrades;
let mockUseGetHeadings;
jest.mock('data/redux/hooks', () => ({
selectors: {
grades: { useAllGrades: jest.fn() },
root: { useGetHeadings: jest.fn() },
grades: { useAllGrades: () => mockUseAllGrades() },
root: { useGetHeadings: () => mockUseGetHeadings() },
},
}));
jest.mock('data/redux/transforms', () => ({
grades: { roundGrade: jest.fn() },
grades: { roundGrade: jest.fn((val) => val) },
}));
const roundGrade = grade => grade * 20;
transforms.grades.roundGrade.mockImplementation(roundGrade);
jest.mock('i18n/utils', () => ({ getLocalizedPercentSign: () => '%' }));
jest.mock('./Fields', () => ({ Username: () => null, Text: () => null }));
jest.mock('./GradeButton', () => ({ __esModule: true, default: () => null }));
jest.mock('./LabelReplacements', () => ({
TotalGradeLabelReplacement: () => null,
UsernameLabelReplacement: () => null,
MastersOnlyLabelReplacement: () => null,
}));
const subsectionLabels = [
'subsectionLabel1',
@@ -85,7 +74,9 @@ const allGrades = [
],
},
];
const testHeading = 'test-heading-value';
const headings = [
Headings.totalGrade,
Headings.username,
@@ -93,100 +84,60 @@ const headings = [
Headings.fullName,
testHeading,
];
selectors.grades.useAllGrades.mockReturnValue(allGrades);
selectors.root.useGetHeadings.mockReturnValue(headings);
let out;
describe('useGradebookTableData', () => {
describe('useGradebookTableData hook', () => {
beforeAll(() => {
mockUseAllGrades = jest.fn();
mockUseGetHeadings = jest.fn();
});
beforeEach(() => {
jest.clearAllMocks();
out = useGradebookTableData();
mockUseAllGrades.mockReset();
mockUseGetHeadings.mockReset();
});
describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
});
it('initializes redux hooks', () => {
expect(selectors.grades.useAllGrades).toHaveBeenCalled();
expect(selectors.root.useGetHeadings).toHaveBeenCalled();
let hookResult;
const TestComponent = () => {
hookResult = hooks.useGradebookTableData();
return null;
};
beforeEach(() => {
initializeMocks();
hookResult = null;
mockUseAllGrades.mockReturnValue([]);
mockUseGetHeadings.mockReturnValue([]);
});
it('returns expected structure with empty data', () => {
render(<TestComponent />);
expect(hookResult).toEqual({
columns: [],
data: [],
grades: [],
nullMethod: expect.any(Function),
emptyContent: expect.any(String),
});
});
describe('output', () => {
describe('columns', () => {
test('total grade heading produces TotalGradeLabelReplacement label', () => {
const { Header, accessor } = out.columns[0];
expect(accessor).toEqual(headings[0]);
expect(shallow(Header)).toMatchObject(
shallow(<LabelReplacements.TotalGradeLabelReplacement />),
);
});
test('username heading produces UsernameLabelReplacement', () => {
const { Header, accessor } = out.columns[1];
expect(accessor).toEqual(headings[1]);
expect(shallow(Header)).toMatchObject(
shallow(<LabelReplacements.UsernameLabelReplacement />),
);
});
test('email heading replaces with email heading message', () => {
const { Header, accessor } = out.columns[2];
expect(accessor).toEqual(headings[2]);
expect(shallow(Header)).toMatchObject(
shallow(<LabelReplacements.MastersOnlyLabelReplacement {...messages.emailHeading} />),
);
});
test('fullName heading replaces with fullName heading message', () => {
const { Header, accessor } = out.columns[3];
expect(accessor).toEqual(headings[3]);
expect(shallow(Header)).toMatchObject(
shallow(<LabelReplacements.MastersOnlyLabelReplacement {...messages.fullNameHeading} />),
);
});
test('other headings are passed through', () => {
const { Header, accessor } = out.columns[4];
expect(accessor).toEqual(headings[4]);
expect(Header).toEqual(headings[4]);
});
});
describe('data', () => {
test('username field', () => {
allGrades.forEach((entry, index) => {
expect(out.data[index][Headings.username]).toMatchObject(
<Fields.Username username={entry.username} userKey={entry.external_user_key} />,
);
});
});
test('email field', () => {
allGrades.forEach((entry, index) => {
expect(out.data[index][Headings.email]).toMatchObject(
<Fields.Text value={entry.email} />,
);
});
});
test('totalGrade field', () => {
allGrades.forEach((entry, index) => {
expect(out.data[index][Headings.totalGrade]).toEqual(
`${roundGrade(entry.percent * 100)}${getLocalizedPercentSign()}`,
);
});
});
test('section breakdown', () => {
allGrades.forEach((entry, gradeIndex) => {
subsectionLabels.forEach((label, labelIndex) => {
expect(out.data[gradeIndex][label]).toMatchObject(
<GradeButton entry={entry} subsection={entry.section_breakdown[labelIndex]} />,
);
});
});
});
});
it('forwards grades from redux', () => {
expect(out.grades).toEqual(allGrades);
});
test('nullMethod returns null', () => {
expect(out.nullMethod()).toEqual(null);
});
test('emptyContent', () => {
expect(out.emptyContent).toEqual(formatMessage(messages.noResultsFound));
});
it('nullMethod returns null', () => {
render(<TestComponent />);
expect(hookResult.nullMethod()).toBeNull();
});
it('returns expected structure with grades and headings data', () => {
mockUseAllGrades.mockReturnValue(allGrades);
mockUseGetHeadings.mockReturnValue(headings);
render(<TestComponent />);
expect(hookResult.columns.length).toBe(headings.length);
expect(hookResult.columns[0].accessor).toEqual(headings[0]);
expect(hookResult.data.length).toBe(allGrades.length);
expect(hookResult.data[0]).toHaveProperty(Headings.username);
expect(hookResult.grades).toEqual(allGrades);
expect(hookResult.nullMethod()).toBeNull();
expect(hookResult.emptyContent).toBe(messages.noResultsFound.defaultMessage);
expect(mockUseAllGrades).toHaveBeenCalled();
expect(mockUseGetHeadings).toHaveBeenCalled();
});
});

View File

@@ -1,11 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ImportSuccessToast component render snapshot 1`] = `
<Toast
action="test-action"
onClose={[MockFunction hooks.onClose]}
show="test-show"
>
test-description
</Toast>
`;

View File

@@ -1,39 +1,89 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import useImportSuccessToastData from './hooks';
import { render, initializeMocks, screen } from 'testUtilsExtra';
import ImportSuccessToast from '.';
import useImportSuccessToastData from './hooks';
jest.unmock('@openedx/paragon');
jest.unmock('react');
jest.unmock('@edx/frontend-platform/i18n');
jest.mock('data/redux/hooks', () => ({
actions: {
app: {
useSetView: jest.fn(),
useSetShowImportSuccessToast: jest.fn(),
},
},
selectors: {
app: {
useShowImportSuccessToast: jest.fn(),
},
},
}));
jest.mock('./hooks', () => jest.fn());
const hookProps = {
action: 'test-action',
onClose: jest.fn().mockName('hooks.onClose'),
show: 'test-show',
description: 'test-description',
};
useImportSuccessToastData.mockReturnValue(hookProps);
initializeMocks();
let el;
describe('ImportSuccessToast component', () => {
beforeAll(() => {
el = shallow(<ImportSuccessToast />);
describe('ImportSuccessToast', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('behavior', () => {
it('initializes component hook', () => {
expect(useImportSuccessToastData).toHaveBeenCalled();
it('renders with show false', () => {
useImportSuccessToastData.mockReturnValue({
action: {
label: 'View Activity Log',
onClick: jest.fn(),
},
onClose: jest.fn(),
show: false,
description: 'Import Successful! Grades will be updated momentarily.',
});
render(<ImportSuccessToast />);
const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
const toastMessage = screen.queryByText('Import Successful! Grades will be updated momentarily.');
expect(toastMessage).toBeNull();
expect(useImportSuccessToastData).toHaveBeenCalled();
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
it('renders with show true', () => {
useImportSuccessToastData.mockReturnValue({
action: {
label: 'View Activity Log',
onClick: jest.fn(),
},
onClose: jest.fn(),
show: true,
description: 'Import Successful! Grades will be updated momentarily.',
});
test('Toast', () => {
expect(el.instance.type).toEqual('Toast');
expect(el.instance.props.action).toEqual(hookProps.action);
expect(el.instance.props.onClose).toEqual(hookProps.onClose);
expect(el.instance.props.show).toEqual(hookProps.show);
expect(el.instance.children[0].el).toEqual(hookProps.description);
render(<ImportSuccessToast />);
const toastMessage = screen.getByText('Import Successful! Grades will be updated momentarily.');
expect(toastMessage).toBeInTheDocument();
expect(useImportSuccessToastData).toHaveBeenCalled();
});
it('passes correct props to Toast component', () => {
const mockOnClose = jest.fn();
const mockOnClick = jest.fn();
useImportSuccessToastData.mockReturnValue({
action: {
label: 'View Activity Log',
onClick: mockOnClick,
},
onClose: mockOnClose,
show: true,
description: 'Import Successful! Grades will be updated momentarily.',
});
const { container } = render(<ImportSuccessToast />);
expect(container).toBeInTheDocument();
expect(useImportSuccessToastData).toHaveBeenCalled();
});
});

View File

@@ -1,30 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InterventionsReport component output snapshot 1`] = `
<div>
<h4
className="mt-0"
>
Interventions Report
</h4>
<div
className="d-flex justify-content-between align-items-center"
>
<div
className="intervention-report-description"
>
Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.
</div>
<NetworkButton
label={
{
"defaultMessage": "Download Interventions",
"description": "The labeled button to download the Intervention report from the Grades View",
"id": "gradebook.GradesView.InterventionsReport.downloadBtn",
}
}
onClick={[MockFunction]}
/>
</div>
</div>
`;

View File

@@ -1,42 +1,77 @@
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 NetworkButton from 'components/NetworkButton';
import messages from './messages';
import useInterventionsReportData from './hooks';
import InterventionsReport from '.';
jest.unmock('@openedx/paragon');
jest.unmock('react');
jest.unmock('@edx/frontend-platform/i18n');
jest.mock('components/NetworkButton', () => 'NetworkButton');
jest.mock('./hooks', () => jest.fn());
const hookProps = { show: true, handleClick: jest.fn() };
useInterventionsReportData.mockReturnValue(hookProps);
const useInterventionsReportData = require('./hooks');
let el;
describe('InterventionsReport component', () => {
initializeMocks();
describe('InterventionsReport', () => {
beforeEach(() => {
el = shallow(<InterventionsReport />);
jest.clearAllMocks();
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useInterventionsReportData).toHaveBeenCalledWith();
expect(useIntl).toHaveBeenCalledWith();
it('renders without errors when show is true', () => {
useInterventionsReportData.mockReturnValue({
show: true,
handleClick: jest.fn(),
});
render(<InterventionsReport />);
expect(screen.getByRole('heading', { level: 4 })).toBeInTheDocument();
expect(useInterventionsReportData).toHaveBeenCalled();
});
describe('output', () => {
it('does now render if show is false', () => {
useInterventionsReportData.mockReturnValueOnce({ ...hookProps, show: false });
el = shallow(<InterventionsReport />);
expect(el.isEmptyRender()).toEqual(true);
it('renders nothing when show is false', () => {
useInterventionsReportData.mockReturnValue({
show: false,
handleClick: jest.fn(),
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
const btnProps = el.instance.findByType(NetworkButton)[0].props;
expect(btnProps.label).toEqual(messages.downloadBtn);
expect(btnProps.onClick).toEqual(hookProps.handleClick);
render(<InterventionsReport />);
expect(screen.queryByRole('heading')).not.toBeInTheDocument();
expect(screen.queryByText('Interventions Report')).not.toBeInTheDocument();
expect(useInterventionsReportData).toHaveBeenCalled();
});
it('calls useInterventionsReportData hook', () => {
useInterventionsReportData.mockReturnValue({
show: true,
handleClick: jest.fn(),
});
render(<InterventionsReport />);
expect(useInterventionsReportData).toHaveBeenCalled();
});
it('renders with correct content when show is true', () => {
const mockReportData = {
show: true,
handleClick: jest.fn(),
};
useInterventionsReportData.mockReturnValue(mockReportData);
render(<InterventionsReport />);
expect(screen.getByText('Interventions Report')).toBeInTheDocument();
expect(
screen.getByText(/Need to find students who may be falling behind/),
).toBeInTheDocument();
const networkButton = document.querySelector('networkbutton');
expect(networkButton).toBeInTheDocument();
expect(useInterventionsReportData).toHaveBeenCalled();
});
});

View File

@@ -1,20 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StatusAlerts component render snapshot 1`] = `
<Fragment>
<Alert
onClose={[MockFunction hooks.successBanner.onClose]}
show="hooks.show-success-banner"
variant="success"
>
hooks.success-banner-text
</Alert>
<Alert
dismissible={false}
show="hooks.show-grade-filter"
variant="danger"
>
hooks.grade-filter-text
</Alert>
</Fragment>
`;

View File

@@ -1,31 +1,29 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Alert } from '@openedx/paragon';
import { render, screen } from '@testing-library/react';
import useStatusAlertsData from './hooks';
import StatusAlerts from '.';
jest.mock('./hooks', () => jest.fn());
jest.unmock('@openedx/paragon');
const hookProps = {
successBanner: {
onClose: jest.fn().mockName('hooks.successBanner.onClose'),
show: 'hooks.show-success-banner',
show: true,
text: 'hooks.success-banner-text',
},
gradeFilter: {
show: 'hooks.show-grade-filter',
show: true,
text: 'hooks.grade-filter-text',
},
};
useStatusAlertsData.mockReturnValue(hookProps);
let el;
describe('StatusAlerts component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<StatusAlerts />);
render(<StatusAlerts />);
});
describe('behavior', () => {
it('initializes component hooks', () => {
@@ -33,21 +31,17 @@ describe('StatusAlerts component', () => {
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
it('success banner', () => {
const alerts = screen.getAllByRole('alert');
const successAlert = alerts[0];
expect(successAlert).toHaveTextContent(hookProps.successBanner.text);
expect(successAlert).toHaveClass('alert-success');
});
test('success banner', () => {
const alert = el.instance.findByType(Alert)[0];
const { props } = alert;
expect(props.onClose).toEqual(hookProps.successBanner.onClose);
expect(props.show).toEqual(hookProps.successBanner.show);
expect(alert.children[0].el).toEqual(hookProps.successBanner.text);
});
test('grade filter banner', () => {
const alert = el.instance.findByType(Alert)[1];
const { props } = alert;
expect(props.show).toEqual(hookProps.gradeFilter.show);
expect(alert.children[0].el).toEqual(hookProps.gradeFilter.text);
it('grade filter banner', () => {
const alerts = screen.getAllByRole('alert');
const gradeFilter = alerts[1];
expect(gradeFilter).toHaveTextContent(hookProps.gradeFilter.text);
expect(gradeFilter).toHaveClass('alert-danger');
});
});
});