feat: deprecate shallow in tests part 1/9 (#481)

* feat: deprecated shallow in tests

* chore: test utils from dev to dependencies

* chore: removed unused imports

* chore: restore packages to devDep

* chore: renamed header to pass lint
This commit is contained in:
Javier Ontiveros
2025-08-07 16:12:39 -06:00
committed by GitHub
parent b88969c9bb
commit 25f686a875
15 changed files with 89 additions and 370 deletions

26
package-lock.json generated
View File

@@ -50,7 +50,8 @@
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@openedx/frontend-build": "^14.3.3",
"@testing-library/react": "^16.2.0",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/react": "^16.3.0",
"es-check": "^2.3.0",
"fetch-mock": "^12.2.0",
"identity-obj-proxy": "^3.0.0",
@@ -5172,17 +5173,16 @@
}
},
"node_modules/@testing-library/jest-dom": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
"integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==",
"license": "MIT",
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.4.tgz",
"integrity": "sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ==",
"dependencies": {
"@adobe/css-tools": "^4.4.0",
"aria-query": "^5.0.0",
"chalk": "^3.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.6.3",
"lodash": "^4.17.21",
"picocolors": "^1.1.1",
"redent": "^3.0.0"
},
"engines": {
@@ -5191,19 +5191,6 @@
"yarn": ">=1"
}
},
"node_modules/@testing-library/jest-dom/node_modules/chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
@@ -5214,7 +5201,6 @@
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz",
"integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
},

View File

@@ -70,7 +70,8 @@
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@openedx/frontend-build": "^14.3.3",
"@testing-library/react": "^16.2.0",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/react": "^16.3.0",
"es-check": "^2.3.0",
"fetch-mock": "^12.2.0",
"identity-obj-proxy": "^3.0.0",

View File

@@ -1,20 +1,11 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Alert } from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { render, screen } from '@testing-library/react';
import selectors from 'data/selectors';
import messages from './messages';
import { BulkManagementAlerts, mapStateToProps } from './BulkManagementAlerts';
jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: m => m,
FormattedMessage: () => 'FormattedMessage',
}));
jest.mock('@openedx/paragon', () => ({
Alert: () => 'Alert',
}));
jest.unmock('@openedx/paragon');
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
@@ -29,47 +20,19 @@ const errorMessage = 'Oh noooooo';
describe('BulkManagementAlerts', () => {
describe('component', () => {
let el;
describe('no errer, no upload success', () => {
beforeEach(() => {
el = shallow(<BulkManagementAlerts />);
describe('states of the warnings', () => {
test('no alert shown', () => {
const el = render(<BulkManagementAlerts bulkImportError="" uploadSuccess={false} />);
expect(el.container.querySelectorAll('.alert').length).toEqual(0);
});
test('snapshot - bulkImportError closed, success closed', () => {
expect(el.snapshot).toMatchSnapshot();
test('Just success alert shown', () => {
const el = render(<BulkManagementAlerts bulkImportError="" uploadSuccess />);
expect(el.container.querySelectorAll('.alert-success').length).toEqual(1);
});
test('closed danger alert', () => {
expect(el.instance.children[0].type).toBe('Alert');
expect(el.instance.findByType(Alert)[0].props.show).toEqual(false);
expect(el.instance.findByType(Alert)[0].props.variant).toEqual('danger');
});
test('closed success alert', () => {
expect(el.instance.children[1].type).toBe('Alert');
expect(el.instance.findByType(Alert)[1].props.show).toEqual(false);
expect(el.instance.findByType(Alert)[1].props.variant).toEqual('success');
});
});
describe('no errer, no upload success', () => {
beforeEach(() => {
el = shallow(<BulkManagementAlerts uploadSuccess bulkImportError={errorMessage} />);
});
const assertions = [
'danger alert open with bulkImportError',
'success alert open with messages.successDialog',
];
test(`snapshot - ${assertions.join(', ')}`, () => {
expect(el.snapshot).toMatchSnapshot();
});
test('open danger alert with bulkImportError content', () => {
expect(el.instance.children[0].type).toBe('Alert');
expect(el.instance.findByType(Alert)[0].children[0].el).toEqual(errorMessage);
expect(el.instance.findByType(Alert)[0].props.show).toEqual(true);
});
test('open success alert with messages.successDialog content', () => {
expect(el.instance.children[1].type).toBe('Alert');
expect(el.shallowWrapper.props.children[1].props.children).toEqual(
<FormattedMessage {...messages.successDialog} />,
);
expect(el.instance.children[1].props.show).toEqual(true);
test('Just error alert shown', () => {
const el = render(<BulkManagementAlerts bulkImportError={errorMessage} uploadSuccess={false} />);
expect(el.container.querySelectorAll('.alert-danger').length).toEqual(1);
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
});
});

View File

@@ -19,10 +19,8 @@ const ResultsSummary = ({
text,
}) => (
<Hyperlink
href={lms.urls.bulkGradesUrlByRow(rowId)}
destination="www.edx.org"
destination={lms.urls.bulkGradesUrlByRow(rowId)}
target="_blank"
rel="noopener noreferrer"
showLaunchIcon={false}
>
<Icon src={Download} className="d-inline-block" />

View File

@@ -1,21 +1,13 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Download } from '@openedx/paragon/icons';
import lms from 'data/services/lms';
import { render, screen } from '@testing-library/react';
import ResultsSummary from './ResultsSummary';
jest.mock('@openedx/paragon', () => ({
Hyperlink: () => 'Hyperlink',
Icon: () => 'Icon',
}));
jest.mock('@openedx/paragon/icons', () => ({
Download: 'DownloadIcon',
}));
jest.unmock('@openedx/paragon');
jest.mock('data/services/lms', () => ({
urls: {
bulkGradesUrlByRow: jest.fn((rowId) => ({ url: { rowId } })),
bulkGradesUrlByRow: jest.fn((rowId) => (`www.edx.org/${rowId}`)),
},
}));
@@ -25,28 +17,21 @@ describe('ResultsSummary component', () => {
text: 'texty',
};
let el;
const assertions = [
'safe hyperlink with bulkGradesUrl with course and row id',
'download icon',
'results text',
];
let link;
beforeEach(() => {
el = shallow(<ResultsSummary {...props} />);
});
test(`snapshot - ${assertions.join(', ')}`, () => {
expect(el.snapshot).toMatchSnapshot();
el = render(<ResultsSummary {...props} />);
link = screen.getByRole('link', { name: props.text });
});
test('Hyperlink has target="_blank" and rel="noopener noreferrer"', () => {
expect(el.instance.props.target).toEqual('_blank');
expect(el.instance.props.rel).toEqual('noopener noreferrer');
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
});
test('Hyperlink has href to bulkGradesUrl', () => {
expect(el.instance.props.href).toEqual(lms.urls.bulkGradesUrlByRow(props.rowId));
expect(link).toHaveAttribute('href', lms.urls.bulkGradesUrlByRow(props.rowId));
});
test('displays Download Icon and text', () => {
const icon = el.instance.children[0];
expect(icon.type).toEqual('Icon');
expect(icon.props.src).toEqual(Download);
expect(el.instance.children[1].el).toEqual(props.text);
expect(link).toHaveTextContent(props.text);
const span = el.container.querySelector('span[aria-hidden="true"]');
expect(span).toBeInTheDocument();
});
});

View File

@@ -1,45 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BulkManagementAlerts component no errer, no upload success snapshot - bulkImportError closed, success closed 1`] = `
<Fragment>
<Alert
dismissible={false}
show={false}
variant="danger"
/>
<Alert
dismissible={false}
show={false}
variant="success"
>
<FormattedMessage
defaultMessage="CSV processing. File uploads may take several minutes to complete."
description="Success Dialog message in BulkManagement Tab File Upload Form"
id="gradebook.BulkManagementHistoryView.successDialog"
/>
</Alert>
</Fragment>
`;
exports[`BulkManagementAlerts component no errer, no upload success snapshot - danger alert open with bulkImportError, success alert open with messages.successDialog 1`] = `
<Fragment>
<Alert
dismissible={false}
show={true}
variant="danger"
>
Oh noooooo
</Alert>
<Alert
dismissible={false}
show={true}
variant="success"
>
<FormattedMessage
defaultMessage="CSV processing. File uploads may take several minutes to complete."
description="Success Dialog message in BulkManagement Tab File Upload Form"
id="gradebook.BulkManagementHistoryView.successDialog"
/>
</Alert>
</Fragment>
`;

View File

@@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ResultsSummary component snapshot - safe hyperlink with bulkGradesUrl with course and row id, download icon, results text 1`] = `
<Hyperlink
destination="www.edx.org"
href={
{
"url": {
"rowId": 42,
},
}
}
rel="noopener noreferrer"
showLaunchIcon={false}
target="_blank"
>
<Icon
className="d-inline-block"
src="DownloadIcon"
/>
texty
</Hyperlink>
`;

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { getConfig } from '@edx/frontend-platform';
import Header from '.';
jest.unmock('@openedx/paragon');
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
describe('Header', () => {
test('has edx link with logo url', () => {
const url = 'www.ourLogo.url';
const baseUrl = 'www.lms.url';
getConfig.mockReturnValue({ LOGO_URL: url, LMS_BASE_URL: baseUrl });
render(<Header />);
expect(screen.getByRole('link')).toHaveAttribute('href', `${baseUrl}/dashboard`);
expect(screen.getByAltText('edX logo')).toHaveAttribute('src', url);
});
});

View File

@@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header snapshot - has edx link with logo url 1`] = `
<div
className="mb-3"
>
<header
className="d-flex justify-content-center align-items-center p-3 border-bottom-blue"
>
<Hyperlink
destination="undefined/dashboard"
>
<img
alt="edX logo"
height="30"
src="www.ourLogo.url"
width="60"
/>
</Hyperlink>
<div />
</header>
</div>
`;

View File

@@ -1,21 +0,0 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { getConfig } from '@edx/frontend-platform';
import Header from '.';
jest.mock('@openedx/paragon', () => ({
Hyperlink: () => 'Hyperlink',
}));
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
describe('Header', () => {
test('snapshot - has edx link with logo url', () => {
const url = 'www.ourLogo.url';
getConfig.mockReturnValue({ LOGO_URL: url });
expect(shallow(<Header />).snapshot).toMatchSnapshot();
});
});

View File

@@ -1,44 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AssignmentFilterType component render snapshot 1`] = `
<div
className="student-filters"
>
<SelectGroup
disabled={true}
id="assignment-types"
label="Assignment Types"
onChange={[MockFunction]}
options={
[
<option
value=""
>
All
</option>,
<option
value="test-type"
>
test-type
</option>,
<option
value="type1"
>
type1
</option>,
<option
value="type2"
>
type2
</option>,
<option
value="type3"
>
type3
</option>,
]
}
value="test-type"
/>
</div>
`;

View File

@@ -1,12 +1,14 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render, screen } from '@testing-library/react';
import SelectGroup from '../SelectGroup';
import useAssignmentFilterTypeData from './hooks';
import AssignmentFilterType from '.';
jest.mock('../SelectGroup', () => 'SelectGroup');
jest.unmock('@openedx/paragon');
jest.unmock('react');
jest.unmock('@edx/frontend-platform/i18n');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
const handleChange = jest.fn();
@@ -21,27 +23,15 @@ useAssignmentFilterTypeData.mockReturnValue({
const updateQueryParams = jest.fn();
let el;
describe('AssignmentFilterType component', () => {
beforeAll(() => {
el = shallow(<AssignmentFilterType updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useAssignmentFilterTypeData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
render(<IntlProvider locale="en"><AssignmentFilterType updateQueryParams={updateQueryParams} /></IntlProvider>);
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('filter options', () => {
const { options } = el.instance.findByType(SelectGroup)[0].props;
expect(options.length).toEqual(5);
const optionProps = options[1].props;
expect(optionProps.value).toEqual(assignmentTypes[0]);
expect(optionProps.children).toEqual(testType);
const options = screen.getAllByRole('option');
expect(options.length).toEqual(5); // 4 types + "All Types"
expect(options[1]).toHaveTextContent(testType);
});
});
});

View File

@@ -1,63 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseFilter component render if disabled snapshot 1`] = `
<Fragment>
<div
className="grade-filter-inputs"
>
<PercentGroup
id="minimum-grade"
label="Min Grade"
onChange={[MockFunction]}
value={23}
/>
<PercentGroup
id="maximum-grade"
label="Max Grade"
onChange={[MockFunction]}
value={300}
/>
</div>
<div
className="grade-filter-action"
>
<Button
disabled={true}
variant="outline-secondary"
>
Apply
</Button>
</div>
</Fragment>
`;
exports[`CourseFilter component render with selected assignment snapshot 1`] = `
<Fragment>
<div
className="grade-filter-inputs"
>
<PercentGroup
id="minimum-grade"
label="Min Grade"
onChange={[MockFunction]}
value={23}
/>
<PercentGroup
id="maximum-grade"
label="Max Grade"
onChange={[MockFunction]}
value={300}
/>
</div>
<div
className="grade-filter-action"
>
<Button
disabled={false}
variant="outline-secondary"
>
Apply
</Button>
</div>
</Fragment>
`;

View File

@@ -1,13 +1,14 @@
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 } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import PercentGroup from '../PercentGroup';
import useCourseGradeFilterData from './hooks';
import CourseFilter from '.';
jest.mock('../PercentGroup', () => 'PercentGroup');
jest.unmock('@openedx/paragon');
jest.unmock('react');
jest.unmock('@edx/frontend-platform/i18n');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
const hookData = {
@@ -27,48 +28,37 @@ useCourseGradeFilterData.mockReturnValue(hookData);
const updateQueryParams = jest.fn();
let el;
describe('CourseFilter component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<CourseFilter updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useCourseGradeFilterData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
});
describe('render', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('with selected assignment', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
beforeEach(() => {
render(<IntlProvider locale="en"><CourseFilter updateQueryParams={updateQueryParams} /></IntlProvider>);
});
it('renders a PercentGroup for both Max and Min filters', () => {
let { props } = el.instance.findByType(PercentGroup)[0];
expect(props.value).toEqual(hookData.min.value);
expect(props.onChange).toEqual(hookData.min.onChange);
props = el.instance.findByType(PercentGroup)[1].props;
expect(props.value).toEqual(hookData.max.value);
expect(props.onChange).toEqual(hookData.max.onChange);
expect(screen.getByRole('spinbutton', { name: 'Min Grade' })).toHaveValue(hookData.min.value);
expect(screen.getByRole('spinbutton', { name: 'Max Grade' })).toHaveValue(hookData.max.value);
});
it('renders a submit button', () => {
const { props } = el.instance.findByType(Button)[0];
expect(props.disabled).toEqual(false);
expect(props.onClick).toEqual(hookData.handleApplyClick);
expect(screen.getByRole('button', { name: 'Apply' })).toBeInTheDocument();
// Expect it to be enabled
expect(screen.getByRole('button', { name: 'Apply' })).not.toBeDisabled();
});
});
describe('if disabled', () => {
beforeEach(() => {
jest.clearAllMocks();
useCourseGradeFilterData.mockReturnValueOnce({ ...hookData, isDisabled: true });
el = shallow(<CourseFilter updateQueryParams={updateQueryParams} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
render(<IntlProvider locale="en"><CourseFilter updateQueryParams={updateQueryParams} /></IntlProvider>);
});
it('disables submit', () => {
const { props } = el.instance.findByType(Button)[0];
expect(props.disabled).toEqual(true);
expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled();
});
});
});

View File

@@ -1,3 +1,5 @@
import '@testing-library/jest-dom';
// These configuration values are usually set in webpack's EnvironmentPlugin however
// Jest does not use webpack so we need to set these so for testing
process.env.LMS_BASE_URL = 'http://localhost:18000';