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:
@@ -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>
|
||||
`;
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
`;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user