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(); }); }); });