Skip to content

Commit 8f07d76

Browse files
committed
feat(Wizard): supporting component unit tests
1 parent 0b52df6 commit 8f07d76

20 files changed

Lines changed: 496 additions & 69 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@babel/preset-react": "^7.0.0",
3636
"@babel/preset-typescript": "^7.9.0",
3737
"@octokit/rest": "^16.43.2",
38+
"@testing-library/react-hooks": "^8.0.1",
3839
"@testing-library/jest-dom": "^5.16.2",
3940
"@testing-library/react": "^12.1.5",
4041
"@testing-library/user-event": "^13.5.0",

packages/react-core/src/next/components/Wizard/Wizard.tsx

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,13 @@ import styles from '@patternfly/react-styles/css/components/Wizard/wizard';
66
import {
77
DefaultWizardFooterProps,
88
DefaultWizardNavProps,
9-
isCustomWizardFooter,
109
isWizardParentStep,
1110
WizardNavStepFunction,
1211
CustomWizardNavFunction
1312
} from './types';
1413
import { buildSteps, normalizeNavStep } from './utils';
1514
import { useWizardContext, WizardContextProvider } from './WizardContext';
1615
import { WizardStepProps } from './WizardStep';
17-
import { WizardFooter } from './WizardFooter';
1816
import { WizardToggle } from './WizardToggle';
1917

2018
/**
@@ -39,6 +37,8 @@ export interface WizardProps extends React.HTMLProps<HTMLDivElement> {
3937
width?: number | string;
4038
/** Custom height of the wizard */
4139
height?: number | string;
40+
/** Flag to unmount inactive steps instead of hiding. Defaults to true */
41+
unmountInactiveSteps?: boolean;
4242
/** Callback function when a step in the nav is clicked */
4343
onNavByIndex?: WizardNavStepFunction;
4444
/** Callback function after next button is clicked */
@@ -130,35 +130,22 @@ export const Wizard = (props: WizardProps) => {
130130
<WizardContextProvider
131131
steps={steps}
132132
currentStepIndex={currentStepIndex}
133-
footer={isCustomWizardFooter(footer) && footer}
133+
footer={footer}
134134
onNext={goToNextStep}
135135
onBack={goToPrevStep}
136136
onClose={onClose}
137137
goToStepById={goToStepById}
138138
goToStepByName={goToStepByName}
139139
goToStepByIndex={goToStepByIndex}
140140
>
141-
<WizardInternal {...internalProps} footer={footer}>
142-
{children}
143-
</WizardInternal>
141+
<WizardInternal {...internalProps}>{children}</WizardInternal>
144142
</WizardContextProvider>
145143
);
146144
};
147145

148146
// eslint-disable-next-line patternfly-react/no-anonymous-functions
149-
const WizardInternal = ({ height, width, className, header, footer, nav, ...divProps }: WizardProps) => {
150-
const { activeStep, steps, footer: customFooter, onNext, onBack, onClose, goToStepByIndex } = useWizardContext();
151-
152-
const wizardFooter = customFooter || (
153-
<WizardFooter
154-
activeStep={activeStep}
155-
onNext={onNext}
156-
onBack={onBack}
157-
onClose={onClose}
158-
disableBackButton={activeStep?.id === steps[0]?.id}
159-
{...footer}
160-
/>
161-
);
147+
const WizardInternal = ({ height, width, className, header, nav, unmountInactiveSteps, ...divProps }: WizardProps) => {
148+
const { activeStep, steps, footer, goToStepByIndex } = useWizardContext();
162149

163150
return (
164151
<div
@@ -173,9 +160,10 @@ const WizardInternal = ({ height, width, className, header, footer, nav, ...divP
173160
<WizardToggle
174161
steps={steps}
175162
activeStep={activeStep}
176-
footer={wizardFooter}
163+
footer={footer}
177164
nav={nav}
178165
goToStepByIndex={goToStepByIndex}
166+
unmountInactiveSteps={unmountInactiveSteps}
179167
/>
180168
</div>
181169
);

packages/react-core/src/next/components/Wizard/WizardBody.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { css } from '@patternfly/react-styles';
88
*/
99

1010
export interface WizardBodyProps {
11-
children?: React.ReactNode | React.ReactNode[];
11+
children: React.ReactNode | React.ReactNode[];
1212
/** Set to true to remove the default body padding */
1313
hasNoBodyPadding?: boolean;
1414
/** An aria-label to use for the wrapper element */

packages/react-core/src/next/components/Wizard/WizardContext.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import React from 'react';
2-
import { WizardControlStep } from './types';
2+
3+
import { css } from '@patternfly/react-styles';
4+
import styles from '@patternfly/react-styles/css/components/Wizard/wizard';
5+
6+
import { DefaultWizardFooterProps, isCustomWizardFooter, WizardControlStep } from './types';
37
import { getActiveStep } from './utils';
8+
import { WizardFooter } from './WizardFooter';
49

510
export interface WizardContextProps {
611
/** List of steps */
@@ -22,15 +27,15 @@ export interface WizardContextProps {
2227
/** Navigate to step by index */
2328
goToStepByIndex: (index: number) => void;
2429
/** Update the footer with any react element */
25-
setFooter: (footer: React.ReactElement) => void;
30+
setFooter: (footer: DefaultWizardFooterProps | React.ReactElement) => void;
2631
}
2732

2833
export const WizardContext = React.createContext({} as WizardContextProps);
2934

3035
interface WizardContextRenderProps {
3136
steps: WizardControlStep[];
3237
activeStep: WizardControlStep;
33-
footer: React.ReactElement;
38+
footer: DefaultWizardFooterProps | React.ReactElement;
3439
onNext(): void;
3540
onBack(): void;
3641
onClose(): void;
@@ -39,7 +44,7 @@ interface WizardContextRenderProps {
3944
export interface WizardContextProviderProps {
4045
steps: WizardControlStep[];
4146
currentStepIndex: number;
42-
footer: React.ReactElement;
47+
footer: DefaultWizardFooterProps | React.ReactElement;
4348
children: React.ReactElement | ((props: WizardContextRenderProps) => React.ReactElement);
4449
onNext(): void;
4550
onBack(): void;
@@ -66,6 +71,23 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
6671
const [footer, setFooter] = React.useState(initialFooter);
6772
const activeStep = getActiveStep(steps, currentStepIndex);
6873

74+
const wizardFooter = React.useMemo(
75+
() =>
76+
isCustomWizardFooter(footer) ? (
77+
<div className={css(styles.wizardFooter)}>{footer}</div>
78+
) : (
79+
<WizardFooter
80+
activeStep={activeStep}
81+
onNext={onNext}
82+
onBack={onBack}
83+
onClose={onClose}
84+
disableBackButton={activeStep?.id === steps[0]?.id}
85+
{...footer}
86+
/>
87+
),
88+
[activeStep, footer, onBack, onClose, onNext, steps]
89+
);
90+
6991
// When the active step changes and the newly active step isn't visited, set the visited flag to true.
7092
React.useEffect(() => {
7193
if (activeStep && !activeStep?.visited) {
@@ -86,7 +108,7 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
86108
value={{
87109
steps,
88110
activeStep,
89-
footer,
111+
footer: wizardFooter,
90112
onNext,
91113
onBack,
92114
onClose,

packages/react-core/src/next/components/Wizard/WizardNavItem.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ export const WizardNavItem: React.FunctionComponent<WizardNavItemProps> = ({
7373
{...rest}
7474
{...(navItemComponent === 'a' ? { ...linkProps } : { ...btnProps })}
7575
{...(id && { id: id.toString() })}
76-
onClick={() => (isExpandable ? setIsExpanded(!isExpanded || isCurrent) : onNavItemClick(step))}
76+
onClick={e => {
77+
e.preventDefault();
78+
isExpandable ? setIsExpanded(!isExpanded || isCurrent) : onNavItemClick(step);
79+
}}
7780
className={css(
7881
styles.wizardNavLink,
7982
isCurrent && styles.modifiers.current,
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import React from 'react';
22

3-
import { WizardControlStep } from './types';
3+
import { WizardBasicStep } from './types';
44
import { WizardBody, WizardBodyProps } from './WizardBody';
55

66
/**
77
* Used as a passthrough of step properties for Wizard and all supporting child components.
88
* Also acts as a wrapper for content, with an optional inclusion of WizardBody.
99
*/
1010

11-
export interface WizardStepProps extends Omit<WizardControlStep, 'parentId' | 'subStepIds' | 'visited'> {
11+
export interface WizardStepProps extends Omit<WizardBasicStep, 'visited'> {
1212
/** Optional for when the step is used as a parent to sub-steps */
1313
children?: React.ReactNode;
1414
/** Props for WizardBody that wraps content by default. Can be set to null for exclusion of WizardBody. */
15-
body?: WizardBodyProps | null;
15+
body?: Omit<WizardBodyProps, 'children'> | null;
1616
/** Optional list of sub-steps */
1717
steps?: React.ReactElement<WizardStepProps>[];
1818
}
1919

2020
export const WizardStep = ({ body, children }: WizardStepProps) =>
21-
body === undefined ? <WizardBody {...body}>{children}</WizardBody> : <>{children}</>;
21+
body || body === undefined ? <WizardBody {...body}>{children}</WizardBody> : <>{children}</>;
2222

2323
WizardStep.displayName = 'WizardStep';

packages/react-core/src/next/components/Wizard/WizardToggle.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ export interface WizardToggleProps {
2828
activeStep: WizardControlStep;
2929
/** The WizardFooter */
3030
footer: React.ReactElement;
31-
/** Custom WizardNav or callback used to create a default WizardNav */
32-
nav: DefaultWizardNavProps | CustomWizardNavFunction;
3331
/** Navigate using the step index */
3432
goToStepByIndex: (index: number) => void;
35-
/** The button's aria-label */
33+
/** Custom WizardNav or callback used to create a default WizardNav */
34+
nav?: DefaultWizardNavProps | CustomWizardNavFunction;
35+
/** The expandable dropdown button's aria-label */
3636
'aria-label'?: string;
3737
/** Flag to unmount inactive steps instead of hiding. Defaults to true */
3838
unmountInactiveSteps?: boolean;

packages/react-core/src/next/components/Wizard/__tests__/Wizard.test.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,4 +295,40 @@ describe('Wizard', () => {
295295
userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
296296
expect(onClose).toHaveBeenCalled();
297297
});
298+
299+
it('unmounts inactive steps by default', () => {
300+
render(
301+
<Wizard>
302+
<WizardStep id="step-1" name="Test step 1">
303+
Step 1 content
304+
</WizardStep>
305+
<WizardStep id="step-2" name="Test step 2">
306+
Step 2 content
307+
</WizardStep>
308+
</Wizard>
309+
);
310+
311+
userEvent.click(screen.getByRole('button', { name: 'Test step 2' }));
312+
313+
expect(screen.queryByText('Step 1 content')).toBeNull();
314+
expect(screen.getByText('Step 2 content')).toBeVisible();
315+
});
316+
317+
it('keeps inactive steps mounted when unmountInactiveSteps is enabled', () => {
318+
render(
319+
<Wizard unmountInactiveSteps={false}>
320+
<WizardStep id="step-1" name="Test step 1">
321+
Step 1 content
322+
</WizardStep>
323+
<WizardStep id="step-2" name="Test step 2">
324+
Step 2 content
325+
</WizardStep>
326+
</Wizard>
327+
);
328+
329+
userEvent.click(screen.getByRole('button', { name: 'Test step 2' }));
330+
331+
expect(screen.getByText('Step 1 content')).toBeInTheDocument();
332+
expect(screen.getByText('Step 2 content')).toBeVisible();
333+
});
298334
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { WizardBody } from '../WizardBody';
4+
5+
describe('WizardBody', () => {
6+
it('renders children without additional props', () => {
7+
const { container } = render(<WizardBody>content</WizardBody>);
8+
9+
expect(container).toHaveTextContent('content');
10+
expect(container).not.toHaveAttribute('aria-label');
11+
expect(container).not.toHaveAttribute('aria-labelledby');
12+
});
13+
14+
it('has no padding className when hasNoBodyPadding is not specified', () => {
15+
render(<WizardBody>content</WizardBody>);
16+
expect(screen.getByText('content')).not.toHaveClass('pf-m-no-padding');
17+
});
18+
19+
it('has padding className when hasNoBodyPadding is specified', () => {
20+
render(<WizardBody hasNoBodyPadding>content</WizardBody>);
21+
expect(screen.getByText('content')).toHaveClass('pf-m-no-padding');
22+
});
23+
24+
it('has aria-label when one is specified', () => {
25+
render(<WizardBody aria-label="Body label">content</WizardBody>);
26+
expect(screen.getByLabelText('Body label')).toBeVisible();
27+
});
28+
29+
it('has aria-labelledby when one is specified', () => {
30+
const { container } = render(<WizardBody aria-labelledby="some-id">content</WizardBody>);
31+
expect(container.firstElementChild).toHaveAttribute('aria-labelledby', 'some-id');
32+
});
33+
34+
it('wrapper element is of type div when wrapperElement is not specified', () => {
35+
const { container } = render(<WizardBody aria-label="Wizard body">content</WizardBody>);
36+
expect(container.firstElementChild?.tagName).toEqual('DIV');
37+
});
38+
39+
it('renders with custom wrapperElement', () => {
40+
const { container } = render(<WizardBody wrapperElement="main">content</WizardBody>);
41+
expect(container.firstElementChild?.tagName).toEqual('MAIN');
42+
});
43+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React from 'react';
2+
3+
import { render, screen } from '@testing-library/react';
4+
import userEvent from '@testing-library/user-event';
5+
6+
import { WizardFooter } from '../WizardFooter';
7+
8+
describe('WizardFooter', () => {
9+
const defaultProps = {
10+
activeStep: { name: 'Step name', id: 'some-id' },
11+
onNext: jest.fn(),
12+
onBack: jest.fn(),
13+
onClose: jest.fn()
14+
};
15+
16+
it('has button names of "Next", "Back", and "Cancel" by default', () => {
17+
render(<WizardFooter {...defaultProps} />);
18+
19+
expect(screen.getByRole('button', { name: 'Next' })).toBeVisible();
20+
expect(screen.getByRole('button', { name: 'Back' })).toBeVisible();
21+
expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible();
22+
});
23+
24+
it('calls onNext when the next button is clicked', () => {
25+
const onNext = jest.fn();
26+
27+
render(<WizardFooter {...defaultProps} onNext={onNext} />);
28+
29+
userEvent.click(screen.getByRole('button', { name: 'Next' }));
30+
expect(onNext).toHaveBeenCalled();
31+
});
32+
33+
it('calls onBack when the back button is clicked', () => {
34+
const onBack = jest.fn();
35+
36+
render(<WizardFooter {...defaultProps} onBack={onBack} />);
37+
38+
userEvent.click(screen.getByRole('button', { name: 'Back' }));
39+
expect(onBack).toHaveBeenCalled();
40+
});
41+
42+
it('calls onClose when the close button is clicked', () => {
43+
const onClose = jest.fn();
44+
45+
render(<WizardFooter {...defaultProps} onClose={onClose} />);
46+
47+
userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
48+
expect(onClose).toHaveBeenCalled();
49+
});
50+
51+
it('can have custom button names', () => {
52+
render(
53+
<WizardFooter
54+
{...defaultProps}
55+
nextButtonText={<>Go!</>}
56+
backButtonText="Turn around!"
57+
cancelButtonText={<span>Get out!</span>}
58+
/>
59+
);
60+
61+
expect(screen.getByRole('button', { name: 'Go!' })).toBeVisible();
62+
expect(screen.getByRole('button', { name: 'Turn around!' })).toBeVisible();
63+
expect(screen.getByRole('button', { name: 'Get out!' })).toBeVisible();
64+
});
65+
66+
it('has disabled back button when disableBackButton is enabled', () => {
67+
render(<WizardFooter {...defaultProps} disableBackButton />);
68+
expect(screen.getByRole('button', { name: 'Back' })).toHaveAttribute('disabled');
69+
});
70+
71+
it('has no back button when activeStep has hideBackButton enabled', () => {
72+
render(<WizardFooter {...defaultProps} activeStep={{ ...defaultProps.activeStep, hideBackButton: true }} />);
73+
expect(screen.queryByRole('button', { name: 'Back' })).toBeNull();
74+
});
75+
76+
it('has no cancel button when activeStep has hideCancelButton enabled', () => {
77+
render(<WizardFooter {...defaultProps} activeStep={{ ...defaultProps.activeStep, hideCancelButton: true }} />);
78+
expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull();
79+
});
80+
81+
it(`uses activeStep's nextButtonText when specified instead of nextButtonText of WizardFooter`, () => {
82+
render(
83+
<WizardFooter
84+
{...defaultProps}
85+
nextButtonText="Footer next"
86+
activeStep={{ ...defaultProps.activeStep, nextButtonText: 'Active step next' }}
87+
/>
88+
);
89+
expect(screen.queryByRole('button', { name: 'Footer next' })).toBeNull();
90+
expect(screen.getByRole('button', { name: 'Active step next' })).toBeVisible();
91+
});
92+
});

0 commit comments

Comments
 (0)