feat: Shallow test deprecation part 2/9 (#483)

* 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

* feat: deprecated shallow in tests and added testing Utils

* chore: removed deprecated tests

* chore: commit to trigger test again

* chore: fix import warning for new testing file

* chore: address comments
This commit is contained in:
Javier Ontiveros
2025-08-13 12:43:13 -06:00
committed by GitHub
parent 00017e3be1
commit b608be06fe
14 changed files with 256 additions and 224 deletions

View File

@@ -9,5 +9,6 @@ module.exports = createConfig('jest', {
'src/segment.js',
'src/postcss.config.js',
'testUtils', // don't unit test jest mocking tools
'testUtilsExtra', // don't unit test jest mocking tools
],
});

View File

@@ -1,7 +1,12 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from 'testUtilsExtra';
import PercentGroup from './PercentGroup';
import { initializeMocks } from '../../testUtilsExtra';
jest.unmock('@openedx/paragon');
jest.unmock('react');
jest.unmock('@edx/frontend-platform/i18n');
describe('PercentGroup', () => {
let props = {
@@ -12,6 +17,7 @@ describe('PercentGroup', () => {
};
beforeEach(() => {
initializeMocks();
props = {
...props,
onChange: jest.fn().mockName('props.onChange'),
@@ -19,15 +25,17 @@ describe('PercentGroup', () => {
});
describe('Component', () => {
describe('snapshots', () => {
test('basic snapshot', () => {
const el = shallow(<PercentGroup {...props} />);
expect(el.snapshot).toMatchSnapshot();
});
test('disabled', () => {
const el = shallow(<PercentGroup {...props} disabled />);
expect(el.snapshot).toMatchSnapshot();
});
test('is displayed', () => {
render(<PercentGroup {...props} />);
expect(screen.getByRole('spinbutton', { name: 'Group Label' })).toBeInTheDocument();
expect(screen.getByText('Group Label')).toBeVisible();
expect(screen.getByText('%')).toBeVisible();
});
test('disabled', () => {
render(<PercentGroup {...props} disabled />);
expect(screen.getByRole('spinbutton', { name: 'Group Label' })).toBeDisabled();
expect(screen.getByText('Group Label')).toBeVisible();
expect(screen.getByText('%')).toBeVisible();
});
});
});

View File

@@ -1,57 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PercentGroup Component snapshots basic snapshot 1`] = `
<div
className="percent-group"
>
<Form.Group
controlId="group id"
>
<Form.Label>
Group Label
</Form.Label>
<Form.Control
disabled={false}
max={100}
min={0}
onChange={[MockFunction props.onChange]}
step={1}
type="number"
value="group VALUE"
/>
</Form.Group>
<span
className="input-percent-label"
>
%
</span>
</div>
`;
exports[`PercentGroup Component snapshots disabled 1`] = `
<div
className="percent-group"
>
<Form.Group
controlId="group id"
>
<Form.Label>
Group Label
</Form.Label>
<Form.Control
disabled={true}
max={100}
min={0}
onChange={[MockFunction props.onChange]}
step={1}
type="number"
value="group VALUE"
/>
</Form.Group>
<span
className="input-percent-label"
>
%
</span>
</div>
`;

View File

@@ -1,37 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PageButtons component render snapshot 1`] = `
<div
className="d-flex justify-content-center"
style={
{
"paddingBottom": "20px",
}
}
>
<Button
disabled="prev-disabled"
onClick={[MockFunction hooks.prev.onClick]}
style={
{
"margin": "20px",
}
}
variant="outline-primary"
>
prev-text
</Button>
<Button
disabled="next-disabled"
onClick={[MockFunction hooks.next.onClick]}
style={
{
"margin": "20px",
}
}
variant="outline-primary"
>
next-text
</Button>
</div>
`;

View File

@@ -1,53 +1,64 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Button } from '@openedx/paragon';
import { initializeMocks, render, screen } from 'testUtilsExtra';
import usePageButtonsData from './hooks';
import PageButtons from '.';
jest.unmock('@openedx/paragon');
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('react');
jest.mock('./hooks', () => jest.fn());
const hookProps = {
prev: {
disabled: 'prev-disabled',
disabled: false,
onClick: jest.fn().mockName('hooks.prev.onClick'),
text: 'prev-text',
},
next: {
disabled: 'next-disabled',
disabled: false,
onClick: jest.fn().mockName('hooks.next.onClick'),
text: 'next-text',
},
};
usePageButtonsData.mockReturnValue(hookProps);
let el;
describe('PageButtons component', () => {
beforeEach(() => {
beforeAll(() => {
jest.clearAllMocks();
el = shallow(<PageButtons />);
initializeMocks();
});
describe('behavior', () => {
it('initializes component hooks', () => {
expect(usePageButtonsData).toHaveBeenCalled();
describe('renders enabled buttons', () => {
beforeEach(() => {
usePageButtonsData.mockReturnValue(hookProps);
render(<PageButtons />);
});
test('prev button enabled', () => {
expect(screen.getByText(hookProps.prev.text)).toBeInTheDocument();
expect(screen.getByText(hookProps.next.text)).toBeEnabled();
});
test('next button enabled', () => {
expect(screen.getByText(hookProps.next.text)).toBeInTheDocument();
expect(screen.getByText(hookProps.prev.text)).toBeEnabled();
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
describe('renders disabled buttons', () => {
beforeAll(() => {
hookProps.prev.disabled = true;
hookProps.next.disabled = true;
});
test('prev button', () => {
const button = el.instance.findByType(Button)[0];
expect(button.props.disabled).toEqual(hookProps.prev.disabled);
expect(button.props.onClick).toEqual(hookProps.prev.onClick);
expect(button.children[0].el).toEqual(hookProps.prev.text);
beforeEach(() => {
usePageButtonsData.mockReturnValue(hookProps);
render(<PageButtons />);
});
test('next button', () => {
const button = el.instance.findByType(Button)[1];
expect(button.props.disabled).toEqual(hookProps.next.disabled);
expect(button.props.onClick).toEqual(hookProps.next.onClick);
expect(button.children[0].el).toEqual(hookProps.next.text);
test('prev button disabled', () => {
expect(screen.getByText(hookProps.next.text)).toBeInTheDocument();
expect(screen.getByText(hookProps.prev.text)).toBeDisabled();
});
test('next button disabled', () => {
expect(screen.getByText(hookProps.prev.text)).toBeInTheDocument();
expect(screen.getByText(hookProps.next.text)).toBeDisabled();
});
});
});

View File

@@ -1,20 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SearchControls component render snapshot 1`] = `
<div
className="search-container"
>
<SearchField
inputLabel="test-input-label"
onBlur={[MockFunction hooks.onBlur]}
onClear={[MockFunction hooks.onClear]}
onSubmit={[MockFunction hooks.onSubmit]}
value="test-search-value"
/>
<small
className="form-text text-muted search-help-text"
>
test-hint-text
</small>
</div>
`;

View File

@@ -21,7 +21,7 @@ export const SearchControls = () => {
<div className="search-container">
<SearchField
onSubmit={onSubmit}
inputLabel={inputLabel}
label={inputLabel}
onBlur={onBlur}
onClear={onClear}
value={searchValue}

View File

@@ -1,11 +1,12 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { SearchField } from '@openedx/paragon';
import { initializeMocks, render, screen } from 'testUtilsExtra';
import useSearchControlsData from './hooks';
import SearchControls from '.';
jest.unmock('@openedx/paragon');
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('react');
jest.mock('./hooks', () => jest.fn());
const hookProps = {
@@ -17,32 +18,19 @@ const hookProps = {
hintText: 'test-hint-text',
};
useSearchControlsData.mockReturnValue(hookProps);
let el;
describe('SearchControls component', () => {
beforeEach(() => {
initializeMocks();
render(<SearchControls />);
jest.clearAllMocks();
el = shallow(<SearchControls />);
});
describe('behavior', () => {
it('initializes component hooks', () => {
expect(useSearchControlsData).toHaveBeenCalled();
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('search field', () => {
const { props } = el.instance.findByType(SearchField)[0];
expect(props.onSubmit).toEqual(hookProps.onSubmit);
expect(props.onBlur).toEqual(hookProps.onBlur);
expect(props.onClear).toEqual(hookProps.onClear);
expect(props.inputLabel).toEqual(hookProps.inputLabel);
expect(props.value).toEqual(hookProps.searchValue);
expect(screen.getByLabelText(hookProps.inputLabel)).toBeInTheDocument();
expect(screen.getByRole('searchbox')).toHaveValue(hookProps.searchValue);
});
test('hint text', () => {
expect(el.instance.findByType('small')[0].children[0].el).toEqual(hookProps.hintText);
expect(screen.getByText(hookProps.hintText)).toBeInTheDocument();
});
});
});

View File

@@ -1,35 +1,31 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render } from '@testing-library/react';
import { selectors } from 'data/redux/hooks';
import SpinnerIcon from './SpinnerIcon';
jest.unmock('@openedx/paragon');
jest.unmock('react');
jest.unmock('@edx/frontend-platform/i18n');
jest.mock('data/redux/hooks', () => ({
selectors: {
root: { useShouldShowSpinner: jest.fn() },
},
}));
selectors.root.useShouldShowSpinner.mockReturnValue(true);
let el;
describe('SpinnerIcon', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<SpinnerIcon />);
});
describe('behavior', () => {
it('initializes redux hook', () => {
expect(selectors.root.useShouldShowSpinner).toHaveBeenCalled();
});
it('does not render if show: false', () => {
selectors.root.useShouldShowSpinner.mockReturnValueOnce(false);
const { container } = render(<SpinnerIcon />);
expect(container.querySelector('.fa.fa-spinner')).not.toBeInTheDocument();
});
describe('component', () => {
it('does not render if show: false', () => {
selectors.root.useShouldShowSpinner.mockReturnValueOnce(false);
el = shallow(<SpinnerIcon />);
expect(el.isEmptyRender()).toEqual(true);
});
test('snapshot - displays spinner overlay with spinner icon', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('displays spinner overlay with spinner icon', () => {
selectors.root.useShouldShowSpinner.mockReturnValueOnce(true);
const { container } = render(<SpinnerIcon />);
expect(container.querySelector('.fa.fa-spinner')).toBeInTheDocument();
});
});

View File

@@ -1,11 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SpinnerIcon component snapshot - displays spinner overlay with spinner icon 1`] = `
<div
className="spinner-overlay"
>
<Icon
className="fa fa-spinner fa-spin fa-5x color-black"
/>
</div>
`;

View File

@@ -11,7 +11,7 @@ import selectors from './selectors';
import reducers from './reducers';
import eventsMap from './services/segment/mapping';
export const createStore = () => {
export const createStore = (preloadedState = undefined) => {
const loggerMiddleware = createLogger();
const middleware = [thunkMiddleware, loggerMiddleware];
@@ -22,6 +22,7 @@ export const createStore = () => {
const store = redux.createStore(
reducers,
composeWithDevTools(redux.applyMiddleware(...middleware)),
preloadedState,
);
/**

View File

@@ -1,29 +1,24 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { getConfig } from '@edx/frontend-platform';
import { mockConfigs } from 'setupTest';
import { initializeMocks, render, waitFor } from 'testUtilsExtra';
import Head from './Head';
jest.mock('react-helmet', () => ({
Helmet: () => 'Helmet',
}));
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
const config = {
SITE_NAME: 'test-site-name',
FAVICON_URL: 'test-favicon-url',
};
getConfig.mockReturnValue(config);
jest.unmock('@openedx/paragon');
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('react');
describe('Head', () => {
it('should match render title tag and favicon with the site configuration values', () => {
const el = shallow(<Head />);
const title = el.instance.findByType('title')[0];
const link = el.instance.findByType('link')[0];
expect(title.children[0].el).toEqual(`Gradebook | ${config.SITE_NAME}`);
expect(link.props.rel).toEqual('shortcut icon');
expect(link.props.href).toEqual(config.FAVICON_URL);
it('should match render title tag and favicon with the site configuration values', async () => {
initializeMocks();
render(<Head />);
await waitFor(() => {
expect(document.title).toBe(`Gradebook | ${mockConfigs.SITE_NAME}`);
});
const favicon = document.querySelector('link[rel="shortcut icon"]');
expect(favicon).toBeInTheDocument();
expect(favicon.href).toBe(mockConfigs.FAVICON_URL);
});
});

View File

@@ -1,10 +1,22 @@
import '@testing-library/jest-dom';
export const mockConfigs = {
SITE_NAME: 'test-site-name',
FAVICON_URL: 'http://localhost:18000/favicon.ico',
LMS_BASE_URL: 'http://localhost:18000',
};
// 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';
process.env.SITE_NAME = 'localhost';
process.env.FAVICON_URL = 'http://localhost:18000/favicon.ico';
// many are here to prevent warnings on the tests
process.env.LMS_BASE_URL = mockConfigs.LMS_BASE_URL;
process.env.SITE_NAME = mockConfigs.SITE_NAME;
process.env.FAVICON_URL = mockConfigs.FAVICON_URL;
process.env.BASE_URL = mockConfigs.LMS_BASE_URL;
process.env.LOGIN_URL = `${mockConfigs.LMS_BASE_URL}/login`;
process.env.LOGOUT_URL = `${mockConfigs.LMS_BASE_URL}/logout`;
process.env.REFRESH_ACCESS_TOKEN_ENDPOINT = `${mockConfigs.LMS_BASE_URL}/refresh_access_token`;
process.env.ACCESS_TOKEN_COOKIE_NAME = 'edx';
process.env.CSRF_TOKEN_API_PATH = 'TOKEN_PATH';
jest.mock('@edx/frontend-platform/i18n', () => {
const i18n = jest.requireActual('@edx/frontend-platform/i18n');

145
src/testUtilsExtra.jsx Normal file
View File

@@ -0,0 +1,145 @@
/* eslint-disable import/no-extraneous-dependencies */
// This a file with extra things required for being able to mock services etc...
// Normally this is on the `testUtils`file, but given the current state of the tests
// It creates a non trivial circular dependency, which is avoided by not having the
// mocks that currently exists on testUtils, and that will be gone after the DEPR of react-unit-test-utils
// so to wrapup the migration this file needs to be integrated in testUtils as the last step.
import React from 'react';
import PropTypes from 'prop-types';
import {
MemoryRouter, Route, Routes, generatePath,
} from 'react-router';
import { AppProvider } from '@edx/frontend-platform/react';
import { render } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { initializeMockApp } from '@edx/frontend-platform';
import { createStore } from './data/store';
/** @deprecated Use React Query and/or regular React Context instead of redux */
let reduxStore;
/**
* This component works together with the custom `render()` method we have in
* this file to provide whatever react-router context you need for your
* component.
*
* In the simplest case, you don't need to worry about the router at all, so
* just render your component using `render(<TheComponent />)`.
*
* The next simplest way to use it is to specify `path` (the route matching rule
* that is normally used to determine when to show the component or its parent
* page) and `params` like this:
*
* ```
* render(<LibraryLayout />, { path: '/library/:libraryId/*', params: { libraryId: 'lib:Axim:testlib' } });
* ```
*
* In this case, components that use the `useParams` hook will get the right
* library ID, and we don't even have to mock anything.
*
* In other cases, such as when you have routes inside routes, you'll need to
* set the router's `initialEntries` (URL history) prop yourself, like this:
*
* ```
* render(<LibraryLayout />, {
* path: '/library/:libraryId/*',
* // The root component is mounted on the above path, as it is in the "real"
* // MFE. But to access the 'settings' sub-route/component for this test, we
* // need tospecify the URL like this:
* routerProps: { initialEntries: [`/library/${libraryId}/settings`] },
* });
* ```
*/
const RouterAndRoute = ({
children,
path = '/',
params = {},
routerProps = {},
}) => {
if (Object.entries(params).length > 0 || path !== '/') {
const newRouterProps = { ...routerProps };
if (!routerProps.initialEntries) {
// Substitute the params into the URL so '/library/:libraryId' becomes '/library/lib:org:123'
let pathWithParams = generatePath(path, params);
if (pathWithParams.endsWith('/*')) {
// Some routes (that contain child routes) need to end with /* in the <Route> but not in the router
pathWithParams = pathWithParams.substring(0, pathWithParams.length - 1);
}
newRouterProps.initialEntries = [pathWithParams];
}
return (
<MemoryRouter {...newRouterProps}>
<Routes>
<Route path={path} element={children} />
</Routes>
</MemoryRouter>
);
}
return (
<MemoryRouter {...routerProps}>{children}</MemoryRouter>
);
};
RouterAndRoute.propTypes = {
children: PropTypes.node,
path: PropTypes.string,
params: PropTypes.shape({}),
routerProps: PropTypes.shape({}),
};
export const makeWrapper = ({ extraWrapper, ...routeArgs } = {}) => {
// eslint-disable-next-line react/prop-types
const AllTheProviders = ({ children }) => (
<AppProvider store={reduxStore} wrapWithRouter={false}>
<IntlProvider locale="en" messages={{}}>
<RouterAndRoute {...routeArgs}>
{extraWrapper ? React.createElement(extraWrapper, undefined, children) : children}
</RouterAndRoute>
</IntlProvider>
</AppProvider>
);
return AllTheProviders;
};
/**
* Same as render() from `@testing-library/react` but this one provides all the
* wrappers our React components need to render properly.
*/
function customRender(ui, options = {}) {
return render(ui, { wrapper: makeWrapper(options) });
}
const defaultUser = {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
};
/**
* Initialize common mocks that many of our React components will require.
*
* This should be called within each test case, or in `beforeEach()`.
*
* Returns the new `axiosMock` in case you need to mock out axios requests.
*/
export function initializeMocks({ user = defaultUser, initialState = undefined } = {}) {
initializeMockApp({
authenticatedUser: user,
});
reduxStore = createStore(initialState);
// Clear the call counts etc. of all mocks. This doesn't remove the mock's effects; just clears their history.
jest.clearAllMocks();
return {
reduxStore,
};
}
export * from '@testing-library/react';
export {
customRender as render,
};