import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Wizard, WizardFooterProps, WizardStep, WizardNavProps, WizardStepChangeScope } from '../';
import styles from '@patternfly/react-styles/css/components/Wizard/wizard';
test('renders step when child is of type WizardStep', () => {
render(
Step content
);
expect(screen.getByRole('button', { name: 'Test step' })).toBeVisible();
expect(screen.getByText('Step content')).toBeVisible();
});
test('renders a header when specified', () => {
render(
);
expect(screen.getByText('Some header')).toBeVisible();
});
test('renders default footer without specifying the footer prop', () => {
render(
);
expect(screen.getByRole('button', { name: 'Next' })).toBeVisible();
expect(screen.getByRole('button', { name: 'Back' })).toBeVisible();
expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible();
});
test('renders default footer with custom props', () => {
const footer: Partial = {
nextButtonText: <>Proceed with caution>,
backButtonText: 'Turn back!',
cancelButtonText: 'Leave now!'
};
render(
);
expect(screen.getByRole('button', { name: 'Proceed with caution' })).toBeVisible();
expect(screen.getByRole('button', { name: 'Turn back!' })).toBeVisible();
expect(screen.getByRole('button', { name: 'Leave now!' })).toBeVisible();
});
test('can add props to default footer buttons', () => {
render(
);
const nextButton = screen.getByRole('button', { name: 'Next' });
expect(nextButton).toHaveProperty('id', 'next-button');
expect(nextButton).toHaveClass('custom-class');
expect(screen.getByRole('button', { name: 'Back' })).toHaveProperty('id', 'back-button');
expect(screen.getByRole('button', { name: 'Cancel' })).toHaveProperty('id', 'cancel-button');
});
test('renders custom footer', () => {
render(
Some footer>}>
);
expect(screen.getByText('Some footer')).toBeVisible();
});
test('renders default nav without specifying the nav prop', () => {
render(
);
expect(screen.getByRole('navigation')).toBeVisible();
expect(screen.getByRole('button', { name: 'Test step' })).toBeVisible();
});
test('renders default nav with custom props', () => {
const nav: Partial = {
'aria-label': 'Some nav label',
'aria-labelledby': 'wizard-id'
};
render(
]} />
);
const navElement = screen.getByLabelText('Some nav label');
expect(navElement).toBeVisible();
expect(navElement).toHaveAttribute('aria-labelledby', 'wizard-id');
expect(screen.getByRole('button', { name: 'Test step 2' })).toHaveAttribute('disabled');
});
test('renders nav aria label', () => {
render(
);
const navElement = screen.getByLabelText('custom nav aria-label');
expect(navElement).toHaveAttribute('aria-label');
});
test('renders custom nav', () => {
const customNav = () => ;
render(
);
expect(screen.getByRole('navigation')).toBeVisible();
});
test('starts with the first step as the current one by default', () => {
render(
Step 1 content
);
expect(screen.getByText('Step 1 content')).toBeVisible();
expect(screen.getByRole('button', { name: 'Test step 1' })).toHaveClass('pf-m-current');
});
test(`can start at a step that isn't the first by specifying 'startIndex'`, () => {
render(
Step 2 content
);
expect(screen.getByText('Step 2 content')).toBeVisible();
expect(screen.getByRole('button', { name: 'Test step 2' })).toHaveClass('pf-m-current');
});
test(`can use custom classNames and spread other props into the wizard's div`, () => {
render(
);
expect(screen.getByTestId('wizard-id')).toHaveClass('some-class');
});
test(`can customize the wizard's height and width`, () => {
render(
);
const wizard = screen.getByTestId('wizard-id');
expect(wizard).toHaveStyle('width: 500px');
});
test('calls onStepChange on nav item click', async () => {
const user = userEvent.setup();
const onStepChange = jest.fn();
render(
);
await user.click(screen.getByRole('button', { name: 'Test step 2' }));
expect(onStepChange).toHaveBeenCalledWith(
null,
expect.objectContaining({ id: 'step-2' }),
expect.objectContaining({ id: 'step-1' }),
WizardStepChangeScope.Nav
);
});
test('calls onStepChange and not onSave on next button click when not on the last step', async () => {
const user = userEvent.setup();
const onStepChange = jest.fn();
const onSave = jest.fn();
render(
);
await user.click(screen.getByRole('button', { name: 'Next' }));
expect(onStepChange).toHaveBeenCalledWith(
null,
expect.objectContaining({ id: 'step-2' }),
expect.objectContaining({ id: 'step-1' }),
WizardStepChangeScope.Next
);
expect(onSave).not.toHaveBeenCalled();
});
test('calls onStepChange on back button click', async () => {
const user = userEvent.setup();
const onStepChange = jest.fn();
render(
);
await user.click(screen.getByRole('button', { name: 'Test step 2' }));
await user.click(screen.getByRole('button', { name: 'Back' }));
expect(onStepChange).toHaveBeenCalledWith(
null,
expect.objectContaining({ id: 'step-1' }),
expect.objectContaining({ id: 'step-2' }),
WizardStepChangeScope.Back
);
});
test('calls onSave and not onClose on next button click when on the last step', async () => {
const user = userEvent.setup();
const onSave = jest.fn();
const onClose = jest.fn();
render(
);
await user.click(screen.getByRole('button', { name: 'Test step 2' }));
await user.click(screen.getByRole('button', { name: 'Next' }));
expect(onSave).toHaveBeenCalled();
expect(onClose).not.toHaveBeenCalled();
});
test('calls onClose when onSave is not specified on next button click when on the last step', async () => {
const user = userEvent.setup();
const onClose = jest.fn();
render(
);
await user.click(screen.getByRole('button', { name: 'Test step 2' }));
await user.click(screen.getByRole('button', { name: 'Next' }));
expect(onClose).toHaveBeenCalled();
});
test('calls onClose on cancel link click', async () => {
const user = userEvent.setup();
const onClose = jest.fn();
render(
);
await user.click(screen.getByRole('button', { name: 'Cancel' }));
expect(onClose).toHaveBeenCalled();
});
test('does not render inactive step content', async () => {
const user = userEvent.setup();
render(
Step 1 content
Step 2 content
);
await user.click(screen.getByRole('button', { name: 'Test step 2' }));
expect(screen.queryByText('Step 1 content')).toBeNull();
expect(screen.getByText('Step 2 content')).toBeVisible();
});
test('parent steps have collapsed sub-steps when isExpandable is true unless the step is active', async () => {
const user = userEvent.setup();
render(
]}
/>
);
expect(screen.getByLabelText('Expand step icon')).toBeVisible();
await user.click(screen.getByRole('button', { name: 'Next' }));
expect(screen.getByLabelText('Collapse step icon')).toBeVisible();
});
test('parent step can be expandable by setting isExpandable to true', () => {
render(
]}
/>
);
expect(screen.queryByLabelText('step icon', { exact: false })).toBeVisible();
});
test('incrementally shows/hides steps based on the activeStep when isProgressive is enabled', async () => {
const user = userEvent.setup();
render(
Step 1 content
Step 2 content
Step 3 content
);
const nextButton = screen.getByRole('button', {
name: 'Next'
});
const backButton = screen.getByRole('button', {
name: 'Back'
});
// Initially only the first nav item will be visible
expect(
screen.getByRole('button', {
name: 'Test step 1'
})
).toBeVisible();
expect(
screen.queryByRole('button', {
name: 'Test step 2'
})
).toBeNull();
expect(
screen.queryByRole('button', {
name: 'Test step 3'
})
).toBeNull();
// Progressing to the next step will show steps 1 & 2
await user.click(nextButton);
expect(
screen.getByRole('button', {
name: 'Test step 1'
})
).toBeVisible();
expect(
screen.getByRole('button', {
name: 'Test step 2'
})
).toBeVisible();
expect(
screen.queryByRole('button', {
name: 'Test step 3'
})
).toBeNull();
// Progressing to the next step will show all steps
await user.click(nextButton);
expect(
screen.getByRole('button', {
name: 'Test step 1'
})
).toBeVisible();
expect(
screen.getByRole('button', {
name: 'Test step 2'
})
).toBeVisible();
expect(
screen.getByRole('button', {
name: 'Test step 3'
})
).toBeVisible();
// Going back a step will hide step 3
await user.click(backButton);
expect(
screen.getByRole('button', {
name: 'Test step 1'
})
).toBeVisible();
expect(
screen.getByRole('button', {
name: 'Test step 2'
})
).toBeVisible();
expect(
screen.queryByRole('button', {
name: 'Test step 3'
})
).toBeNull();
// Going back a step will hide step 2 & 3
await user.click(backButton);
expect(
screen.getByRole('button', {
name: 'Test step 1'
})
).toBeVisible();
expect(
screen.queryByRole('button', {
name: 'Test step 2'
})
).toBeNull();
expect(
screen.queryByRole('button', {
name: 'Test step 3'
})
).toBeNull();
});
test('parent step can be expandable by setting isExpandable to true', () => {
render(
]}
/>
);
expect(screen.getByLabelText('step icon', { exact: false })).toBeVisible();
});
test('child steps are disabled when parent is disabled', () => {
render(
]}
/>
);
expect(
screen.getByRole('button', {
name: 'Test step 1'
})
).toBeDisabled();
expect(
screen.getByRole('button', {
name: 'Sub step 1'
})
).toBeDisabled();
});
test('child steps are hidden when parent is hidden', () => {
render(
]} />
);
expect(
screen.queryByRole('button', {
name: 'Test step 1'
})
).toBeNull();
expect(
screen.queryByRole('button', {
name: 'Sub step 1'
})
).toBeNull();
});
test('onStepChange skips over disabled or hidden steps and substeps', async () => {
const user = userEvent.setup();
const onStepChange = jest.fn();
render(
]} />
]}
/>
,
]}
/>
]}
/>
,
]}
/>
);
const nextButton = screen.getByRole('button', { name: 'Next' });
const backButton = screen.getByRole('button', { name: 'Back' });
await user.click(nextButton);
expect(onStepChange).toHaveBeenCalledWith(
null,
expect.objectContaining({ id: 'step2-sub1' }),
expect.objectContaining({ id: 'step-1' }),
WizardStepChangeScope.Next
);
await user.click(nextButton);
expect(onStepChange).toHaveBeenCalledWith(
null,
expect.objectContaining({ id: 'step5-sub2' }),
expect.objectContaining({ id: 'step2-sub1' }),
WizardStepChangeScope.Next
);
await user.click(nextButton);
expect(onStepChange).toHaveBeenCalledWith(
null,
expect.objectContaining({ id: 'step8-sub2' }),
expect.objectContaining({ id: 'step5-sub2' }),
WizardStepChangeScope.Next
);
await user.click(backButton);
expect(onStepChange).toHaveBeenCalledWith(
null,
expect.objectContaining({ id: 'step5-sub2' }),
expect.objectContaining({ id: 'step8-sub2' }),
WizardStepChangeScope.Back
);
await user.click(backButton);
expect(onStepChange).toHaveBeenCalledWith(
null,
expect.objectContaining({ id: 'step2-sub1' }),
expect.objectContaining({ id: 'step5-sub2' }),
WizardStepChangeScope.Back
);
await user.click(backButton);
expect(onStepChange).toHaveBeenCalledWith(
null,
expect.objectContaining({ id: 'step-1' }),
expect.objectContaining({ id: 'step2-sub1' }),
WizardStepChangeScope.Back
);
});
test('clicking parent step navigates to first visible sub-step when first sub-step is hidden', async () => {
const user = userEvent.setup();
const onStepChange = jest.fn();
render(
,
,
]}
/>
);
// Navigate to step 3 first
await user.click(screen.getByRole('button', { name: 'Test step 3' }));
expect(onStepChange).toHaveBeenCalledWith(
null,
expect.objectContaining({ id: 'step-3' }),
expect.objectContaining({ id: 'step-1' }),
WizardStepChangeScope.Nav
);
// Click on parent step 2 - should navigate to the first VISIBLE sub-step (step2-sub2), not crash
await user.click(screen.getByRole('button', { name: 'Test step 2' }));
expect(onStepChange).toHaveBeenCalledWith(
null,
expect.objectContaining({ id: 'step2-sub2' }),
expect.objectContaining({ id: 'step-3' }),
WizardStepChangeScope.Nav
);
});
test(`Renders with ${styles.modifiers.plain} class when isPlain is true`, () => {
render(
);
expect(screen.getByTestId('wizard-plain')).toHaveClass(styles.modifiers.plain);
});
test(`Renders with ${styles.modifiers.noPlainOnGlass} class when isNoPlainOnGlass is true`, () => {
render(
);
expect(screen.getByTestId('wizard-no-plain')).toHaveClass(styles.modifiers.noPlainOnGlass);
});