test: Deprecate react-unit-test-utils 8/9 (#492)

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

* test: address comments

---------

Co-authored-by: diana-villalvazo-wgu <diana.villalvazo@wgu.edu>
This commit is contained in:
Victor Navarro
2025-09-02 08:01:09 -06:00
committed by GitHub
parent 34a657d212
commit 2456251790
8 changed files with 902 additions and 473 deletions

View File

@@ -1,28 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ScoreViewInput component render snapshot 1`] = `
<Form.Group
controlId="ScoreView"
>
<Form.Label>
Score View
:
</Form.Label>
<Form.Control
as="select"
onChange={[MockFunction hooks.toggleGradeFormat]}
value="test-grade-format"
>
<option
value="percent"
>
Percent
</option>
<option
value="absolute"
>
Absolute
</option>
</Form.Control>
</Form.Group>
`;

View File

@@ -1,67 +1,241 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { GradeFormats } from 'data/constants/grades';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import userEvent from '@testing-library/user-event';
import { formatMessage } from 'testUtils';
import { actions, selectors } from 'data/redux/hooks';
import ScoreViewInput from '.';
import messages from './messages';
import { ScoreViewInput } from '.';
jest.unmock('@openedx/paragon');
jest.unmock('react');
jest.unmock('@edx/frontend-platform/i18n');
jest.mock('data/redux/hooks', () => ({
actions: {
grades: { useToggleGradeFormat: jest.fn() },
grades: {
useToggleGradeFormat: jest.fn(),
},
},
selectors: {
grades: { useGradeData: jest.fn() },
grades: {
useGradeData: jest.fn(),
},
},
}));
const toggleGradeFormat = jest.fn().mockName('hooks.toggleGradeFormat');
actions.grades.useToggleGradeFormat.mockReturnValue(toggleGradeFormat);
const gradeFormat = 'test-grade-format';
selectors.grades.useGradeData.mockReturnValue({ gradeFormat });
const { actions, selectors } = require('data/redux/hooks');
initializeMocks();
describe('ScoreViewInput', () => {
const mockToggleFormat = jest.fn();
let el;
describe('ScoreViewInput component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<ScoreViewInput />);
});
describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
selectors.grades.useGradeData.mockReturnValue({
gradeFormat: 'percent',
});
it('initializes redux hooks', () => {
expect(actions.grades.useToggleGradeFormat).toHaveBeenCalled();
expect(selectors.grades.useGradeData).toHaveBeenCalled();
actions.grades.useToggleGradeFormat.mockReturnValue(mockToggleFormat);
});
it('renders without errors', () => {
render(<ScoreViewInput />);
expect(document.body).toBeInTheDocument();
});
it('renders form group with correct label', () => {
render(<ScoreViewInput />);
expect(screen.getByLabelText(/score view/i)).toBeInTheDocument();
});
it('renders select element with correct options', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toBeInTheDocument();
expect(
screen.getByRole('option', { name: /percent/i }),
).toBeInTheDocument();
expect(
screen.getByRole('option', { name: /absolute/i }),
).toBeInTheDocument();
});
it('displays correct selected value for percent format', () => {
selectors.grades.useGradeData.mockReturnValue({
gradeFormat: 'percent',
});
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveValue('percent');
});
it('displays correct selected value for absolute format', () => {
selectors.grades.useGradeData.mockReturnValue({
gradeFormat: 'absolute',
});
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveValue('absolute');
});
it('calls toggle function when selection changes', async () => {
render(<ScoreViewInput />);
const user = userEvent.setup();
const select = screen.getByRole('combobox', { name: /score view/i });
await user.selectOptions(select, 'absolute');
expect(mockToggleFormat).toHaveBeenCalledTimes(1);
});
describe('accessibility', () => {
it('has proper form structure', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
const label = screen.getByText(/score view/i);
expect(select).toBeInTheDocument();
expect(label).toBeInTheDocument();
});
it('has accessible label association', () => {
render(<ScoreViewInput />);
const label = screen.getByText(/score view/i);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(label).toBeInTheDocument();
expect(select).toHaveAccessibleName(/score view/i);
});
it('has correct control ID for accessibility', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveAttribute('id', 'ScoreView');
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
describe('form control behavior', () => {
it('renders as a select element', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select.tagName).toBe('SELECT');
});
test('label', () => {
const label = el.instance.children[0];
expect(label.children[0].el).toEqual(`${formatMessage(messages.scoreView)}`);
it('has correct option values', () => {
render(<ScoreViewInput />);
const percentOption = screen.getByRole('option', { name: /percent/i });
const absoluteOption = screen.getByRole('option', { name: /absolute/i });
expect(percentOption).toHaveValue('percent');
expect(absoluteOption).toHaveValue('absolute');
});
describe('form control', () => {
let control;
beforeEach(() => {
control = el.instance.children;
});
test('value and onChange from redux hooks', () => {
expect(control[1].props.value).toEqual(gradeFormat);
expect(control[1].props.onChange).toEqual(toggleGradeFormat);
});
test('absolute and percent options', () => {
const { children } = control[1];
expect(children[0].props.value).toEqual(GradeFormats.percent);
expect(children[0].children[0].el).toEqual(formatMessage(messages.percent));
expect(children[1].props.value).toEqual(GradeFormats.absolute);
expect(children[1].children[0].el).toEqual(formatMessage(messages.absolute));
it('has exactly two options', () => {
render(<ScoreViewInput />);
const options = screen.getAllByRole('option');
expect(options).toHaveLength(2);
});
});
describe('redux integration', () => {
it('uses grade data selector hook', () => {
render(<ScoreViewInput />);
expect(selectors.grades.useGradeData).toHaveBeenCalledTimes(1);
});
it('uses toggle grade format action hook', () => {
render(<ScoreViewInput />);
expect(actions.grades.useToggleGradeFormat).toHaveBeenCalledTimes(1);
});
it('responds to different grade format values', () => {
const { rerender } = render(<ScoreViewInput />);
let select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveValue('percent');
selectors.grades.useGradeData.mockReturnValue({
gradeFormat: 'absolute',
});
rerender(<ScoreViewInput />);
select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveValue('absolute');
});
});
describe('user interactions', () => {
it('handles option selection', async () => {
render(<ScoreViewInput />);
const user = userEvent.setup();
const select = screen.getByRole('combobox', { name: /score view/i });
await user.selectOptions(select, 'absolute');
expect(mockToggleFormat).toHaveBeenCalledWith(expect.any(Object));
});
it('maintains state consistency', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
const percentOption = screen.getByRole('option', { name: /percent/i });
expect(select).toHaveValue('percent');
expect(percentOption).toBeInTheDocument();
});
});
describe('internationalization', () => {
it('displays localized label text', () => {
render(<ScoreViewInput />);
expect(screen.getByText('Score View:')).toBeInTheDocument();
});
it('displays localized option text', () => {
render(<ScoreViewInput />);
expect(screen.getByText('Percent')).toBeInTheDocument();
expect(screen.getByText('Absolute')).toBeInTheDocument();
});
});
describe('component structure', () => {
it('uses proper Bootstrap form classes', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveClass('form-control');
});
it('renders within form group structure', () => {
render(<ScoreViewInput />);
const label = screen.getByText(/score view/i);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(label).toBeInTheDocument();
expect(select).toBeInTheDocument();
expect(select).toHaveAccessibleName(expect.stringMatching(/score view/i));
});
});
});

View File

@@ -1,41 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NetworkButton component snapshots snapshot 1`] = `
<StatefulButton
className="ml-2 test-class"
disabledStates={
[
"pending",
]
}
icons={
{
"default": <Icon
className="fa mr-2 fa-download"
/>,
"pending": <Icon
className="fa mr-2 fa-spinner fa-spin"
/>,
}
}
labels={
{
"default": <FormattedMessage
defaultMessage="test button label"
description="test button label description"
id="label-id"
showSpinner={false}
/>,
"pending": <FormattedMessage
defaultMessage="test button label"
description="test button label description"
id="label-id"
showSpinner={false}
/>,
}
}
onClick={[MockFunction]}
state="default"
variant="outline-primary"
/>
`;

View File

@@ -1,89 +1,309 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Icon, StatefulButton } from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { render, screen, initializeMocks } from 'testUtilsExtra';
// eslint-disable-next-line import/no-extraneous-dependencies
import userEvent from '@testing-library/user-event';
import selectors from 'data/selectors';
import { NetworkButton, mapStateToProps, buttonStates } from '.';
jest.mock('@edx/frontend-platform/i18n', () => ({
FormattedMessage: () => 'FormattedMessage',
}));
jest.mock('@openedx/paragon', () => ({
Icon: () => 'Icon',
StatefulButton: () => 'StatefulButton',
}));
jest.unmock('@openedx/paragon');
jest.unmock('react');
jest.unmock('@edx/frontend-platform/i18n');
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
root: { shouldShowSpinner: (state) => ({ shouldShowSpinner: state }) },
root: {
shouldShowSpinner: jest.fn(),
},
}));
describe('NetworkButton component', () => {
describe('snapshots', () => {
let el;
let btnProps;
const selectors = require('data/selectors');
initializeMocks();
describe('NetworkButton', () => {
const defaultProps = {
label: {
id: 'test.button.label',
defaultMessage: 'Test Button',
description: 'A test button',
},
onClick: jest.fn(),
className: '',
showSpinner: false,
import: false,
};
beforeEach(() => {
jest.clearAllMocks();
});
it('renders without errors', () => {
render(<NetworkButton {...defaultProps} />);
expect(
screen.getByRole('button', { name: /test button/i }),
).toBeInTheDocument();
});
it('renders button with download icon by default', () => {
render(<NetworkButton {...defaultProps} />);
const button = screen.getByRole('button');
const icon = button.querySelector('.fa-download');
expect(icon).toBeInTheDocument();
});
it('renders button with upload icon when import is true', () => {
const props = {
className: 'test-class',
label: {
id: 'label-id',
defaultMessage: 'test button label',
description: 'test button label description',
showSpinner: false,
},
...defaultProps,
import: true,
};
beforeEach(() => {
props.onClick = jest.fn();
el = shallow(<NetworkButton {...props} />);
btnProps = el.instance.findByType(StatefulButton)[0].props;
render(<NetworkButton {...props} />);
const button = screen.getByRole('button');
const icon = button.querySelector('.fa-upload');
expect(icon).toBeInTheDocument();
});
it('applies custom className when provided', () => {
const props = {
...defaultProps,
className: 'custom-class',
};
render(<NetworkButton {...props} />);
const button = screen.getByRole('button');
expect(button).toHaveClass('custom-class', 'ml-2');
});
it('applies default margin class', () => {
render(<NetworkButton {...defaultProps} />);
const button = screen.getByRole('button');
expect(button).toHaveClass('ml-2');
});
it('calls onClick when button is clicked', async () => {
const onClick = jest.fn();
const props = {
...defaultProps,
onClick,
};
render(<NetworkButton {...props} />);
const user = userEvent.setup();
const button = screen.getByRole('button');
await user.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
});
describe('spinner state', () => {
it('shows spinner icon when showSpinner is true', () => {
const props = {
...defaultProps,
showSpinner: true,
};
render(<NetworkButton {...props} />);
const button = screen.getByRole('button');
const spinner = button.querySelector('.fa-spinner.fa-spin');
expect(spinner).toBeInTheDocument();
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
it('disables button when showSpinner is true', () => {
const props = {
...defaultProps,
showSpinner: true,
};
render(<NetworkButton {...props} />);
const button = screen.getByRole('button');
expect(button).toHaveAttribute('aria-disabled', 'true');
});
it('sets labels to translated label prop', () => {
expect(btnProps.labels).toEqual({
default: (<FormattedMessage {...props.label} />),
pending: (<FormattedMessage {...props.label} />),
});
it('does not call onClick when button is disabled and clicked', async () => {
const onClick = jest.fn();
const props = {
...defaultProps,
onClick,
showSpinner: true,
};
render(<NetworkButton {...props} />);
const user = userEvent.setup();
const button = screen.getByRole('button');
await user.click(button);
expect(onClick).not.toHaveBeenCalled();
});
describe('export icons', () => {
it('sets icons with spinner pending icon and download default', () => {
expect(btnProps.icons).toEqual({
pending: (<Icon className="fa mr-2 fa-spinner fa-spin" />),
default: (<Icon className="fa mr-2 fa-download" />),
});
});
it('enables button when showSpinner is false', () => {
render(<NetworkButton {...defaultProps} />);
const button = screen.getByRole('button');
expect(button).toBeEnabled();
});
describe('import icons', () => {
it('sets icons with spinner pending icon and upload default', () => {
el = shallow(<NetworkButton {...props} import />);
expect(el.instance.findByType(StatefulButton)[0].props.icons).toEqual({
pending: (<Icon className="fa mr-2 fa-spinner fa-spin" />),
default: (<Icon className="fa mr-2 fa-upload" />),
});
});
});
describe('button states', () => {
it('uses default state when showSpinner is false', () => {
const component = new NetworkButton(defaultProps);
expect(component.buttonState).toBe(buttonStates.default);
});
describe('buttonState', () => {
it('is set to pending state if props.showSpinner', () => {
expect(btnProps.state).toEqual(buttonStates.default);
});
it('is set to pending state if props.showSpinner', () => {
el = shallow(<NetworkButton {...props} showSpinner />);
expect(el.instance.findByType(StatefulButton)[0].props.state).toEqual(buttonStates.pending);
expect(btnProps.state).toEqual(buttonStates.default);
it('uses pending state when showSpinner is true', () => {
const props = {
...defaultProps,
showSpinner: true,
};
const component = new NetworkButton(props);
expect(component.buttonState).toBe(buttonStates.pending);
});
});
describe('computed properties', () => {
it('generates correct labels object', () => {
const component = new NetworkButton(defaultProps);
const { labels } = component;
expect(labels.default).toBeDefined();
expect(labels.pending).toBeDefined();
});
it('generates correct icons for download button', () => {
const component = new NetworkButton(defaultProps);
const { icons } = component;
expect(icons.default).toBeDefined();
expect(icons.pending).toBeDefined();
});
it('generates correct icons for import button', () => {
const props = {
...defaultProps,
import: true,
};
const component = new NetworkButton(props);
const { icons } = component;
expect(icons.default).toBeDefined();
expect(icons.pending).toBeDefined();
});
});
describe('accessibility', () => {
it('has accessible button role', () => {
render(<NetworkButton {...defaultProps} />);
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
});
it('has accessible name from label', () => {
render(<NetworkButton {...defaultProps} />);
expect(
screen.getByRole('button', { name: /test button/i }),
).toBeInTheDocument();
});
it('indicates disabled state to screen readers', () => {
const props = {
...defaultProps,
showSpinner: true,
};
render(<NetworkButton {...props} />);
const button = screen.getByRole('button');
expect(button).toHaveAttribute('aria-disabled', 'true');
});
});
describe('visual states', () => {
it('has outline-primary variant styling', () => {
render(<NetworkButton {...defaultProps} />);
const button = screen.getByRole('button');
expect(button).toHaveClass('btn-outline-primary');
});
it('shows different visual states based on spinner', () => {
const { rerender } = render(<NetworkButton {...defaultProps} />);
let button = screen.getByRole('button');
expect(button).toBeEnabled();
expect(button.querySelector('.fa-download')).toBeInTheDocument();
rerender(<NetworkButton {...defaultProps} showSpinner />);
button = screen.getByRole('button');
expect(button).toHaveAttribute('aria-disabled', 'true');
expect(button.querySelector('.fa-spinner')).toBeInTheDocument();
});
});
describe('component interaction', () => {
it('maintains label text in both states', () => {
const { rerender } = render(<NetworkButton {...defaultProps} />);
expect(
screen.getByRole('button', { name: /test button/i }),
).toBeInTheDocument();
rerender(<NetworkButton {...defaultProps} showSpinner />);
expect(
screen.getByRole('button', { name: /test button/i }),
).toBeInTheDocument();
});
it('changes icon but maintains functionality', async () => {
const onClick = jest.fn();
const user = userEvent.setup();
const { rerender } = render(
<NetworkButton {...defaultProps} onClick={onClick} />,
);
let button = screen.getByRole('button');
expect(button.querySelector('.fa-download')).toBeInTheDocument();
await user.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
onClick.mockClear();
rerender(<NetworkButton {...defaultProps} onClick={onClick} import />);
button = screen.getByRole('button');
expect(button.querySelector('.fa-upload')).toBeInTheDocument();
await user.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
});
});
describe('mapStateToProps', () => {
it('maps showSpinner from state', () => {
const mockState = { app: { network: { showSpinner: true } } };
selectors.root.shouldShowSpinner.mockReturnValue(true);
const result = mapStateToProps(mockState);
expect(selectors.root.shouldShowSpinner).toHaveBeenCalledWith(mockState);
expect(result).toEqual({
showSpinner: true,
});
});
});
describe('mapStateToProps', () => {
const testState = { a: 'wrinkle', in: 'time' };
let mapped;
beforeEach(() => {
mapped = mapStateToProps(testState);
describe('default props', () => {
it('has correct default className', () => {
expect(NetworkButton.defaultProps.className).toBe('');
});
test('showSpinner from root shouldShowSpinner selector', () => {
expect(mapped.showSpinner).toEqual(selectors.root.shouldShowSpinner(testState));
it('has correct default showSpinner', () => {
expect(NetworkButton.defaultProps.showSpinner).toBe(false);
});
it('has correct default import', () => {
expect(NetworkButton.defaultProps.import).toBe(false);
});
});
});

View File

@@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WithSidebar Component snapshots basic snapshot 1`] = `
<div
className="d-flex sidebar-container page-gradebook"
>
<aside
className="sidebar d-none"
onTransitionEnd={[MockFunction handleSlideDone]}
>
<div>
Some Sidebar Content
</div>
</aside>
<div
className="sidebar-contents position-relative"
>
<b>
aby in a bi
</b>
</div>
</div>
`;

View File

@@ -1,123 +1,233 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import {
WithSidebar,
mapStateToProps,
mapDispatchToProps,
} from '.';
import { WithSidebar, mapStateToProps, mapDispatchToProps } from '.';
jest.unmock('@openedx/paragon');
jest.unmock('react');
jest.unmock('@edx/frontend-platform/i18n');
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
app: {
filterMenu: {
isClosed: jest.fn(state => ({ isClosed: state })),
isOpening: jest.fn(state => ({ isOpening: state })),
open: jest.fn(state => ({ open: state })),
},
},
},
}));
jest.mock('data/thunkActions', () => ({
__esModule: true,
default: {
app: {
filterMenu: {
handleTransitionEnd: jest.fn().mockName('handleTransitionEnd'),
},
app: {
filterMenu: {
open: jest.fn(),
isClosed: jest.fn(),
isOpening: jest.fn(),
},
},
}));
jest.mock('data/thunkActions', () => ({
app: {
filterMenu: {
handleTransitionEnd: jest.fn(),
},
},
}));
const selectors = require('data/selectors');
const thunkActions = require('data/thunkActions');
initializeMocks();
describe('WithSidebar', () => {
let props = {
sidebar: (<div>Some Sidebar Content</div>),
children: (<b>aby in a bi</b>),
const defaultProps = {
children: <div>Main Content</div>,
sidebar: <div>Sidebar Content</div>,
open: false,
isClosed: true,
isOpening: false,
open: false,
handleSlideDone: jest.fn(),
};
beforeEach(() => {
props = {
...props,
handleSlideDone: jest.fn().mockName('handleSlideDone'),
};
jest.clearAllMocks();
});
describe('Component', () => {
describe('behavior', () => {
let el;
beforeEach(() => {
el = shallow(<WithSidebar {...props} />);
});
describe('sidebarClassNames', () => {
const getVal = () => [
...el.instance.props.className.split(' '),
...el.instance.children[0].props.className.split(' '),
...el.instance.children[1].props.className.split(' '),
];
it('returns a "sidebar" classname', () => {
expect(getVal()).toContain('sidebar');
});
it('includes an open className iff props.open', () => {
expect(getVal()).not.toContain('open');
el = shallow(<WithSidebar {...props} open />);
expect(getVal()).toContain('open');
});
it('includes a d-none className iff props.isClosed', () => {
expect(getVal()).toContain('d-none');
el = shallow(<WithSidebar {...props} isClosed={false} />);
expect(getVal()).not.toContain('d-none');
});
});
describe('contentClassNames', () => {
const getVal = () => el.instance.children[1].props.className.split(' ');
it('includes sidebar-contents and position-relative classNames', () => {
expect(getVal()).toContain('sidebar-contents');
expect(getVal()).toContain('position-relative');
});
it('includes an opening class iff props.isOpening', () => {
expect(getVal()).not.toContain('opening');
el = shallow(<WithSidebar {...props} isOpening />);
expect(getVal()).toContain('opening');
});
});
it('renders without errors', () => {
render(<WithSidebar {...defaultProps} />);
expect(document.body).toBeInTheDocument();
});
it('renders main content', () => {
render(<WithSidebar {...defaultProps} />);
expect(screen.getByText('Main Content')).toBeInTheDocument();
});
it('renders sidebar content', () => {
render(<WithSidebar {...defaultProps} />);
expect(screen.getByText('Sidebar Content')).toBeInTheDocument();
});
it('applies correct container classes', () => {
render(<WithSidebar {...defaultProps} />);
const container = screen
.getByText('Main Content')
.closest('.sidebar-container');
expect(container).toHaveClass(
'd-flex',
'sidebar-container',
'page-gradebook',
);
});
describe('sidebar states', () => {
it('applies closed sidebar classes when isClosed is true', () => {
render(<WithSidebar {...defaultProps} isClosed />);
const sidebar = screen.getByText('Sidebar Content').closest('aside');
expect(sidebar).toHaveClass('sidebar', 'd-none');
expect(sidebar).not.toHaveClass('open');
});
describe('snapshots', () => {
test('basic snapshot', () => {
const el = shallow(<WithSidebar {...props} />);
expect(el.snapshot).toMatchSnapshot();
});
it('applies open sidebar classes when open is true', () => {
const props = {
...defaultProps,
open: true,
isClosed: false,
};
render(<WithSidebar {...props} />);
const sidebar = screen.getByText('Sidebar Content').closest('aside');
expect(sidebar).toHaveClass('sidebar', 'open');
expect(sidebar).not.toHaveClass('d-none');
});
it('applies opening content classes when isOpening is true', () => {
const props = {
...defaultProps,
isOpening: true,
};
render(<WithSidebar {...props} />);
const content = screen
.getByText('Main Content')
.closest('.sidebar-contents');
expect(content).toHaveClass(
'sidebar-contents',
'position-relative',
'opening',
);
});
it('does not apply opening class when isOpening is false', () => {
render(<WithSidebar {...defaultProps} />);
const content = screen
.getByText('Main Content')
.closest('.sidebar-contents');
expect(content).toHaveClass('sidebar-contents', 'position-relative');
expect(content).not.toHaveClass('opening');
});
});
describe('event handlers', () => {
it('calls handleSlideDone on sidebar transition end', () => {
const handleSlideDone = jest.fn();
const props = {
...defaultProps,
handleSlideDone,
};
render(<WithSidebar {...props} />);
const sidebar = screen.getByText('Sidebar Content').closest('aside');
sidebar.dispatchEvent(new Event('transitionend', { bubbles: true }));
expect(handleSlideDone).toHaveBeenCalledTimes(1);
});
});
describe('mapStateToProps', () => {
const testState = { A: 'laska' };
let mapped;
beforeEach(() => {
mapped = mapStateToProps(testState);
});
test('open from app.filterMenu.open', () => {
expect(mapped.open).toEqual(selectors.app.filterMenu.open(testState));
});
test('isClosed from app.filterMenu.isClosed', () => {
expect(mapped.isClosed).toEqual(selectors.app.filterMenu.isClosed(testState));
});
test('open from app.filterMenu.isOpening', () => {
expect(mapped.isOpening).toEqual(selectors.app.filterMenu.isOpening(testState));
it('maps filter menu state properties', () => {
const mockState = { app: { filterMenu: {} } };
selectors.app.filterMenu.open.mockReturnValue(true);
selectors.app.filterMenu.isClosed.mockReturnValue(false);
selectors.app.filterMenu.isOpening.mockReturnValue(true);
const result = mapStateToProps(mockState);
expect(selectors.app.filterMenu.open).toHaveBeenCalledWith(mockState);
expect(selectors.app.filterMenu.isClosed).toHaveBeenCalledWith(mockState);
expect(selectors.app.filterMenu.isOpening).toHaveBeenCalledWith(
mockState,
);
expect(result).toEqual({
open: true,
isClosed: false,
isOpening: true,
});
});
});
describe('mapDispatchToProps', () => {
describe('handleSlideDone', () => {
test('from thunkActions.app.filterMenu.handleTransitionEnd', () => {
expect(mapDispatchToProps.handleSlideDone).toEqual(
thunkActions.app.filterMenu.handleTransitionEnd,
);
});
it('maps handleSlideDone action', () => {
expect(mapDispatchToProps.handleSlideDone).toBe(
thunkActions.app.filterMenu.handleTransitionEnd,
);
});
});
describe('component structure and accessibility', () => {
it('uses semantic aside element for sidebar', () => {
render(<WithSidebar {...defaultProps} />);
const sidebar = screen.getByRole('complementary');
expect(sidebar.tagName).toBe('ASIDE');
expect(sidebar).toContainElement(screen.getByText('Sidebar Content'));
});
it('renders content in a properly structured container', () => {
render(<WithSidebar {...defaultProps} />);
const content = screen.getByText('Main Content');
const contentContainer = content.closest('.sidebar-contents');
expect(contentContainer).toHaveClass(
'sidebar-contents',
'position-relative',
);
expect(contentContainer).toContainElement(content);
});
});
describe('class name computation', () => {
it('computes sidebar class names correctly for different states', () => {
const { rerender } = render(<WithSidebar {...defaultProps} />);
let sidebar = screen.getByRole('complementary');
expect(sidebar).toHaveClass('sidebar', 'd-none');
expect(sidebar).not.toHaveClass('open');
rerender(<WithSidebar {...defaultProps} open isClosed={false} />);
sidebar = screen.getByRole('complementary');
expect(sidebar).toHaveClass('sidebar', 'open');
expect(sidebar).not.toHaveClass('d-none');
});
it('computes content class names correctly for different states', () => {
const { rerender } = render(<WithSidebar {...defaultProps} />);
let content = screen
.getByText('Main Content')
.closest('.sidebar-contents');
expect(content).toHaveClass('sidebar-contents', 'position-relative');
expect(content).not.toHaveClass('opening');
rerender(<WithSidebar {...defaultProps} isOpening />);
content = screen.getByText('Main Content').closest('.sidebar-contents');
expect(content).toHaveClass(
'sidebar-contents',
'position-relative',
'opening',
);
});
});
});

View File

@@ -1,37 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradebookPage component snapshot - shows BulkManagementHistoryView if activeView === views.bulkManagementHistory 1`] = `
<WithSidebar
sidebar={
<GradebookFilters
updateQueryParams={[Function]}
/>
}
>
<div
className="px-3 gradebook-content"
>
<GradebookHeader />
<BulkManagementHistoryView />
</div>
</WithSidebar>
`;
exports[`GradebookPage component snapshot - shows GradesView if aciveView === views.grades 1`] = `
<WithSidebar
sidebar={
<GradebookFilters
updateQueryParams={[Function]}
/>
}
>
<div
className="px-3 gradebook-content"
>
<GradebookHeader />
<GradesView
updateQueryParams={[Function]}
/>
</div>
</WithSidebar>
`;

View File

@@ -1,164 +1,218 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { render } from '@testing-library/react'; // eslint-disable-line import/no-extraneous-dependencies
import { shallow } from '@edx/react-unit-test-utils';
import queryString from 'query-string';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
import GradebookFilters from 'components/GradebookFilters';
import GradebookHeader from 'components/GradebookHeader';
import GradesView from 'components/GradesView';
import BulkManagementHistoryView from 'components/BulkManagementHistoryView';
import { views } from 'data/constants/app';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import { GradebookPage, mapStateToProps, mapDispatchToProps } from '.';
jest.mock('query-string', () => ({
parse: jest.fn(val => ({ parsed: val })),
stringify: (val) => `stringify: ${JSON.stringify(val, Object.keys(val).sort())}`,
}));
jest.unmock('@openedx/paragon');
jest.unmock('react');
jest.unmock('@edx/frontend-platform/i18n');
jest.mock(
'components/WithSidebar',
// eslint-disable-next-line react/prop-types
() => function WithSidebar({ children }) {
return (
<div data-testid="with-sidebar">
<p>WithSidebar</p>
{children}
</div>
);
},
);
jest.mock(
'components/GradebookHeader',
() => function GradebookHeader() {
return <div data-testid="gradebook-header">GradebookHeader</div>;
},
);
jest.mock(
'components/GradesView',
() => function GradesView() {
return <div data-testid="grades-view">GradesView</div>;
},
);
jest.mock(
'components/GradebookFilters',
() => function GradebookFilters() {
return <div data-testid="gradebook-filters">GradebookFilters</div>;
},
);
jest.mock(
'components/BulkManagementHistoryView',
() => function BulkManagementHistoryView() {
return (
<div data-testid="bulk-management-history">
BulkManagementHistoryView
</div>
);
},
);
jest.mock('@openedx/paragon', () => ({
Tab: () => 'Tab',
Tabs: () => 'Tabs',
}));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
app: {
activeView: (state) => ({ activeView: state }),
},
},
}));
jest.mock('data/thunkActions', () => ({
__esModule: true,
default: {
app: { initialize: jest.fn() },
app: {
activeView: jest.fn(),
},
}));
jest.mock('components/WithSidebar', () => 'WithSidebar');
jest.mock('components/GradebookHeader', () => 'GradebookHeader');
jest.mock('components/GradesView', () => 'GradesView');
jest.mock('components/GradebookFilters', () => 'GradebookFilters');
jest.mock('components/BulkManagementHistoryView', () => 'BulkManagementHistoryView');
jest.mock('data/thunkActions', () => ({
app: {
initialize: jest.fn(),
},
}));
jest.mock('query-string', () => ({
parse: jest.fn(),
stringify: jest.fn(),
}));
const queryString = require('query-string');
const selectors = require('data/selectors');
const thunkActions = require('data/thunkActions');
initializeMocks();
describe('GradebookPage', () => {
describe('component', () => {
const courseId = 'a course';
let el;
const props = {
location: {
pathname: '/',
search: 'searchString',
},
courseId,
activeView: views.grades,
};
beforeEach(() => {
props.initializeApp = jest.fn();
props.navigate = jest.fn();
});
test('snapshot - shows BulkManagementHistoryView if activeView === views.bulkManagementHistory', () => {
el = shallow(<GradebookPage {...props} activeView={views.bulkManagementHistory} />);
expect(el.snapshot).toMatchSnapshot();
});
test('snapshot - shows GradesView if aciveView === views.grades', () => {
el = shallow(<GradebookPage {...props} />);
expect(el.snapshot).toMatchSnapshot();
});
describe('render', () => {
beforeEach(() => {
el = shallow(<GradebookPage {...props} />);
const defaultProps = {
navigate: jest.fn(),
location: { pathname: '/gradebook', search: '?course_id=test-course' },
courseId: 'test-course-id',
activeView: 'grades',
initializeApp: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
queryString.parse.mockReturnValue({});
queryString.stringify.mockReturnValue('course_id=test-course');
});
it('renders without errors', () => {
render(<GradebookPage {...defaultProps} />);
expect(screen.getByText('WithSidebar')).toBeInTheDocument();
});
it('calls initializeApp on mount with courseId and parsed query', () => {
const mockQuery = { assignment: 'test-assignment' };
queryString.parse.mockReturnValue(mockQuery);
render(<GradebookPage {...defaultProps} />);
expect(defaultProps.initializeApp).toHaveBeenCalledWith(
defaultProps.courseId,
mockQuery,
);
expect(queryString.parse).toHaveBeenCalledWith(
defaultProps.location.search,
);
});
it('renders GradebookHeader in content area', () => {
render(<GradebookPage {...defaultProps} />);
expect(screen.getByText('GradebookHeader')).toBeInTheDocument();
});
it('renders GradesView when activeView is grades', () => {
render(<GradebookPage {...defaultProps} activeView="grades" />);
expect(screen.getByText('GradesView')).toBeInTheDocument();
});
it('renders BulkManagementHistoryView when activeView is bulkManagementHistory', () => {
render(
<GradebookPage {...defaultProps} activeView="bulkManagementHistory" />,
);
expect(screen.getByText('BulkManagementHistoryView')).toBeInTheDocument();
});
describe('updateQueryParams', () => {
it('updates query parameters and navigates', () => {
const component = new GradebookPage(defaultProps);
const queryParams = {
assignment: 'new-assignment',
student: 'student-1',
};
queryString.parse.mockReturnValue({ course_id: 'test-course' });
queryString.stringify.mockReturnValue(
'course_id=test-course&assignment=new-assignment&student=student-1',
);
component.updateQueryParams(queryParams);
expect(queryString.parse).toHaveBeenCalledWith(
defaultProps.location.search,
);
expect(queryString.stringify).toHaveBeenCalledWith({
course_id: 'test-course',
assignment: 'new-assignment',
student: 'student-1',
});
describe('top-level WithSidebar', () => {
test('sidebar from GradebookFilters, with updateQueryParams', () => {
const { sidebar } = el.instance.props;
expect(sidebar).toMatchObject(
<GradebookFilters updateQueryParams={el.shallowWrapper.props.sidebar.props.updateQueryParams} />,
);
});
});
describe('gradebook-content', () => {
let content;
let children;
beforeEach(() => {
content = el.instance.children;
children = content[0].children;
});
it('is wrapped in a div w/ px-3 gradebook-content classNames', () => {
expect(content[0].type).toEqual('div');
expect(content[0].props.className).toEqual('px-3 gradebook-content');
});
it('displays Gradebook header and then tabs', () => {
expect(shallow(children[0])).toEqual(shallow(<GradebookHeader />));
});
it('displays GradesView if activeView === views.grades', () => {
expect(shallow(children[1])).toEqual(shallow((
<GradesView updateQueryParams={el.shallowWrapper.props.sidebar.props.updateQueryParams} />
)));
});
it('displays Bulk Management History View if activeView === views.bulkManagementHistory', () => {
el = shallow(<GradebookPage {...props} activeView={views.bulkManagementHistory} />);
const mainView = el.instance.children[0].children[1];
expect(shallow(mainView)).toEqual(shallow(
<BulkManagementHistoryView />,
));
});
expect(defaultProps.navigate).toHaveBeenCalledWith({
pathname: defaultProps.location.pathname,
search:
'?course_id=test-course&assignment=new-assignment&student=student-1',
});
});
describe('behavior', () => {
beforeEach(() => {
el = shallow(<GradebookPage {...props} />);
it('removes query parameters when value is falsy', () => {
const component = new GradebookPage(defaultProps);
const queryParams = { assignment: null, student: '' };
queryString.parse.mockReturnValue({
course_id: 'test-course',
assignment: 'old-assignment',
student: 'old-student',
});
describe('componentDidMount', () => {
test('initializes app with courseId and urlQuery', () => {
render(<GradebookPage {...props} />);
expect(props.initializeApp).toHaveBeenCalledWith(
courseId,
queryString.parse(props.location.search),
);
});
});
describe('updateQueryParams', () => {
it('replaces values for truthy values', () => {
queryString.parse.mockImplementation(key => ({ [key]: key }));
const newKey = 'testKey';
const val1 = 'VALUE';
const val2 = 'VALTWO!!';
const args = { [newKey]: val1, [props.location.search]: val2 };
el.shallowWrapper.props.sidebar.props.updateQueryParams(args);
expect(props.navigate).toHaveBeenCalledWith({ pathname: '/', search: `?${queryString.stringify(args)}` });
});
it('clears values for non-truthy values', () => {
queryString.parse.mockImplementation(key => ({ [key]: key }));
const newKey = 'testKey';
const val1 = 'VALUE';
const val2 = false;
const args = { [newKey]: val1, [props.location.search]: val2 };
el.shallowWrapper.props.sidebar.props.updateQueryParams(args);
expect(props.navigate).toHaveBeenCalledWith(
{ pathname: '/', search: `?${queryString.stringify({ [newKey]: val1 })}` },
);
});
queryString.stringify.mockReturnValue('course_id=test-course');
component.updateQueryParams(queryParams);
expect(queryString.stringify).toHaveBeenCalledWith({
course_id: 'test-course',
});
});
});
describe('mapStateToProps', () => {
let mapped;
const testState = { trash: 'in', the: 'wind' };
beforeEach(() => {
mapped = mapStateToProps(testState);
});
test('activeView from app.activeView', () => {
expect(mapped.activeView).toEqual(selectors.app.activeView(testState));
it('maps activeView from state', () => {
const mockState = { app: { activeView: 'bulkManagementHistory' } };
selectors.app.activeView.mockReturnValue('bulkManagementHistory');
const result = mapStateToProps(mockState);
expect(selectors.app.activeView).toHaveBeenCalledWith(mockState);
expect(result).toEqual({
activeView: 'bulkManagementHistory',
});
});
});
describe('mapDispatchToProps', () => {
test('initializeApp from thunkActions.app.initialize', () => {
expect(mapDispatchToProps.initializeApp).toEqual(thunkActions.app.initialize);
it('maps initializeApp action', () => {
expect(mapDispatchToProps.initializeApp).toBe(
thunkActions.app.initialize,
);
});
});
describe('default props', () => {
it('has correct default location', () => {
expect(GradebookPage.defaultProps.location).toEqual({
pathname: '/',
search: '',
});
});
});
describe('component lifecycle', () => {
it('binds updateQueryParams in constructor', () => {
const component = new GradebookPage(defaultProps);
expect(typeof component.updateQueryParams).toBe('function');
});
});
});