import { render, screen, userEvent } from '../test-utils';
import * as mockedDeviceDetection from './deviceDetection';
import Stepper from './Stepper';
jest.mock('./deviceDetection', () => ({
isTouchDevice: jest.fn(() => false),
}));
describe('Stepper', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const generateSteps = (stepsCount: number) =>
Array.from({ length: stepsCount }, () => ({
label: Math.random().toString(),
onClick: jest.fn(),
}));
const getSteps = () => screen.getAllByRole('listitem');
const initialProps = {
activeStep: 0,
steps: generateSteps(3),
};
const customRender = (overrides = {}) => {
return render();
};
describe('progress bar', () => {
it('renders nothing when no steps are passed in', () => {
customRender({ steps: [] });
expect(screen.queryByTestId('progress-bar')).not.toBeInTheDocument();
});
});
describe('steps', () => {
it('have rendered labels', () => {
const steps = generateSteps(5);
customRender({ steps });
getSteps().forEach((step, index) => {
expect(step).toHaveTextContent(steps[index].label);
});
});
describe('step interactive style', () => {
const expectStepIsVisuallyInteractive = (stepIndex: number) => {
// eslint-disable-next-line jest/valid-expect
return expect(getSteps()[stepIndex].classList.contains('tw-stepper__step--clickable'));
};
it('is not styled as interactive if it is the active step', async () => {
const steps = [
{ label: '0' },
{ label: '1', onClick: jest.fn() },
{ label: '2', onClick: jest.fn() },
];
customRender({ steps, activeStep: 0 });
expectStepIsVisuallyInteractive(0).toBe(false);
});
it('is not styled as interactive if not the active step and has no click handler', () => {
const steps = [
{ label: '0' },
{ label: '1', onClick: jest.fn() },
{ label: '2', onClick: jest.fn() },
];
customRender({ steps, activeStep: 1 });
expectStepIsVisuallyInteractive(0).toBe(false);
expectStepIsVisuallyInteractive(1).toBe(false);
});
it('is styled as interactive if not the active step and has click handler', () => {
const steps = [
{ label: '0', onClick: jest.fn() },
{ label: '1', onClick: jest.fn() },
{ label: '2', onClick: jest.fn() },
];
customRender({ steps, activeStep: 2 });
expectStepIsVisuallyInteractive(0).toBe(true);
expectStepIsVisuallyInteractive(1).toBe(true);
expectStepIsVisuallyInteractive(2).toBe(false);
});
});
describe('step interactivity', () => {
const getStepChild = (stepIndex: number) => getSteps()[stepIndex].children[0];
it('is not interactive if it is the active step', async () => {
const steps = [
{ label: '0', onClick: jest.fn() },
{ label: '1', onClick: jest.fn() },
{ label: '2', onClick: jest.fn() },
];
customRender({ steps, activeStep: 0 });
await userEvent.click(getStepChild(0));
expect(steps[0].onClick).not.toHaveBeenCalled();
});
it('is not interactive if not the active step but has no click handler', async () => {
const steps = [
{ label: '0' },
{ label: '1', onClick: jest.fn() },
{ label: '2', onClick: jest.fn() },
];
customRender({ steps, activeStep: 1 });
await userEvent.click(getStepChild(0));
expect(steps[1].onClick).not.toHaveBeenCalled();
await userEvent.click(getStepChild(1));
expect(steps[1].onClick).not.toHaveBeenCalled();
});
it('is interactive if not the active step and has click handler', async () => {
const steps = [
{ label: '0', onClick: jest.fn() },
{ label: '1', onClick: jest.fn() },
{ label: '2', onClick: jest.fn() },
];
customRender({ steps, activeStep: 2 });
await userEvent.click(getStepChild(1));
expect(steps[1].onClick).toHaveBeenCalledTimes(1);
});
});
it('are not clickable when active', async () => {
const clickedOnFirstStep = jest.fn();
const clickedOnSecondStep = jest.fn();
const initialProps = {
steps: [
{ label: 'one', onClick: clickedOnFirstStep },
{ label: 'two', onClick: clickedOnSecondStep },
],
activeStep: 0,
};
const { rerender } = customRender(initialProps);
const clickOnStep = async (stepIndex: number) => {
const step = screen.getByText(initialProps.steps[stepIndex].label).parentElement;
if (step) {
return userEvent.click(step);
}
};
await clickOnStep(0);
expect(clickedOnFirstStep).not.toHaveBeenCalled();
rerender();
await clickOnStep(0);
expect(clickedOnFirstStep).toHaveBeenCalledTimes(1);
await clickOnStep(1);
expect(clickedOnSecondStep).not.toHaveBeenCalled();
});
it('are active when they are the currently active step', () => {
const { rerender } = customRender({
steps: Array(4)
.fill('')
.map((_, i) => ({ label: i.toString() })),
activeStep: 1,
});
const stepActive = (index: number) =>
getSteps()[index].classList.contains('tw-stepper__step--active');
expect(stepActive(0)).toBe(false);
expect(stepActive(1)).toBe(true);
expect(stepActive(2)).toBe(false);
expect(stepActive(3)).toBe(false);
rerender();
expect(stepActive(1)).toBe(false);
expect(stepActive(2)).toBe(true);
});
it('are aria-current=step when active', () => {
const { rerender } = customRender({ steps: Array(4).fill({ label: '' }), activeStep: 1 });
const stepCurrent = (index: number) =>
getSteps()[index].getAttribute('aria-current') === 'step';
expect(stepCurrent(0)).toBe(false);
expect(stepCurrent(1)).toBe(true);
expect(stepCurrent(2)).toBe(false);
expect(stepCurrent(3)).toBe(false);
rerender();
expect(stepCurrent(1)).toBe(false);
expect(stepCurrent(2)).toBe(true);
});
});
describe('hover labels', () => {
it('will be rendered when provided', async () => {
const hoverLabel = 'hover label';
customRender({
steps: [{ hoverLabel, label: 'label' }, { label: 'label 2' }],
});
expect(await screen.findByText(hoverLabel)).toBeInTheDocument();
});
it('renders jsx', async () => {
customRender({
steps: [
{
hoverLabel: (
<>
hover label 1
hover label 2
>
),
label: 'one',
},
],
});
expect(await screen.findByText('hover label 1')).toBeInTheDocument();
expect(await screen.findByText('hover label 2')).toBeInTheDocument();
});
it('will not be rendered if the user is on a touch device', () => {
jest.spyOn(mockedDeviceDetection, 'isTouchDevice').mockImplementation(() => true);
customRender({
steps: [{ hoverLabel: 'hover label', label: 'label' }, { label: 'label 2' }],
});
expect(screen.queryByText('hover label')).not.toBeInTheDocument();
});
});
});