diff --git a/.storybook/__snapshots__/Welcome.story.storyshot b/.storybook/__snapshots__/Welcome.story.storyshot index 7658a5c56c..1106ddbad3 100644 --- a/.storybook/__snapshots__/Welcome.story.storyshot +++ b/.storybook/__snapshots__/Welcome.story.storyshot @@ -2868,6 +2868,18 @@ exports[`Storybook Snapshot tests and console checks Storyshots 0/Getting Starte IconDropdown +
+
+
+ EmptyState +
+
diff --git a/src/components/EmptyState/EmptyState.jsx b/src/components/EmptyState/EmptyState.jsx index 22ff45459e..5a8822798a 100644 --- a/src/components/EmptyState/EmptyState.jsx +++ b/src/components/EmptyState/EmptyState.jsx @@ -13,11 +13,9 @@ import { EmptystateNotauthorizedIcon as NotAuthImage, } from '../../icons/components'; -import './_emptystate.scss'; - const { iotPrefix } = settings; -const images = { +const icons = { error: ErrorImage, error404: Error404Image, empty: EmptyImage, @@ -26,37 +24,61 @@ const images = { success: SuccessImage, }; -const actionProp = PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ - label: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, - }), -]); - +// TODO: Discuss whether actions can be custom components, e.g. for showing details in error messages. const props = { /** Title of empty state */ title: PropTypes.string.isRequired, /** Description of empty state */ body: PropTypes.string.isRequired, /** Optional image of state */ - image: PropTypes.oneOfType([ + icon: PropTypes.oneOfType([ PropTypes.func, - PropTypes.oneOf([...Object.keys(images), '']), + PropTypes.oneOf([ + 'error', + 'error404', + 'empty', + 'not-authorized', + 'no-result', + 'success', + '', + ]), ]), /** Optional action for container */ - action: actionProp, + action: PropTypes.shape({ + label: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + }), + // action: PropTypes.oneOfType([ + // PropTypes.func, + // PropTypes.shape({ + // label: PropTypes.string.isRequired, + // onClick: PropTypes.func.isRequired, + // }), + // ]), /** Optional secondary action for container */ - secondaryAction: actionProp, + secondaryAction: PropTypes.shape({ + label: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + }), + // secondaryAction: PropTypes.oneOfType([ + // PropTypes.func, + // PropTypes.shape({ + // label: PropTypes.string.isRequired, + // onClick: PropTypes.func.isRequired, + // }), + // ]), /** Specify an optional className to be applied to the container */ className: PropTypes.string, + /** Specify a testid for testing this component */ + testID: PropTypes.string, }; const defaultProps = { action: null, secondaryAction: null, - image: '', + icon: '', className: '', + testID: 'EmptyState', }; /** @@ -65,43 +87,67 @@ const defaultProps = { */ const EmptyState = ({ title, - image, + icon, body, action, secondaryAction, className, + testID, }) => ( -
+
- {image && - React.createElement(typeof image === 'string' ? images[image] : image, { + {icon && + React.createElement(typeof icon === 'string' ? icons[icon] : icon, { className: `${iotPrefix}--empty-state--icon`, alt: '', - 'data-testid': 'emptystate-icon', + 'data-testid': `${testID}-icon`, })} -

{title}

-

{body}

+

+ {title} +

+

+ {body} +

{action && ( -
- {action.label ? ( +
+ + {/* {action.label ? ( ) : ( action - )} + )} */}
)} {secondaryAction && ( -
- {secondaryAction.label ? ( +
+ {secondaryAction.label && ( + // eslint-disable-next-line jsx-a11y/anchor-is-valid + + {secondaryAction.label} + + )} + {/* {secondaryAction.label ? ( // eslint-disable-next-line jsx-a11y/anchor-is-valid {secondaryAction.label} ) : ( secondaryAction - )} + )} */}
)}
diff --git a/src/components/EmptyState/EmptyState.story.jsx b/src/components/EmptyState/EmptyState.story.jsx index 7d3fddc841..e57043739d 100644 --- a/src/components/EmptyState/EmptyState.story.jsx +++ b/src/components/EmptyState/EmptyState.story.jsx @@ -6,17 +6,6 @@ import { DashboardIcon } from '../../icons/components'; import EmptyState from './EmptyState'; -const commonActions = { - action: { - label: text('action.label', 'Optional action'), - onClick: action('action onClick'), - }, - secondaryAction: { - label: text('link.label', 'Optional link'), - onClick: action('secondaryAction onClick'), - }, -}; - export default { title: 'Watson IoT/EmptyState', @@ -49,7 +38,7 @@ export default { export const FirstTimeUse = () => ( ( @@ -73,7 +62,7 @@ export const NoSearchResultsFound = () => ( export const Success = () => ( @@ -81,7 +70,7 @@ export const Success = () => ( export const Page404 = () => ( ( ( export const Error = () => ( ( export const NotAuthorized = () => ( ( export const NotConfigured = () => ( ( export const WithCustomIcon = () => ( ( export const Playground = () => ( diff --git a/src/components/EmptyState/EmptyState.test.jsx b/src/components/EmptyState/EmptyState.test.jsx index 29154279e1..a0a75b9487 100644 --- a/src/components/EmptyState/EmptyState.test.jsx +++ b/src/components/EmptyState/EmptyState.test.jsx @@ -16,17 +16,19 @@ import EmptyState from './EmptyState'; const title = 'Titletest'; const body = 'Titlebody'; -const iconID = 'emptystate-icon'; const commonProps = { title, body, }; + +const testID = 'EmptyState'; + const action = (name, onClick) => ({ label: name, onClick, }); -const images = { +const icons = { error: ErrorImage, error404: Error404Image, empty: EmptyImage, @@ -38,27 +40,26 @@ const images = { describe('EmptyState', () => { it('shows title and body', () => { render(); - expect(screen.getByText(title)).toBeTruthy(); - expect(screen.getByText(body)).toBeTruthy(); - expect(screen.queryByTestId(iconID)).toBeNull(); + expect(screen.getByTestId(`${testID}-title`).textContent).toEqual(title); + expect(screen.getByTestId(`${testID}-body`).textContent).toEqual(body); + expect(screen.queryByTestId(`${testID}-icon`)).toBeNull(); + }); + + it.each(Object.keys(icons))('shows different images', (icon) => { + const iconContainer = render(React.createElement(icons[icon])); + render(); + const renderedIcon = screen.getByTestId(`${testID}-icon`); + + // is passed image type equal to related icon + expect(renderedIcon.innerHTML).toEqual( + iconContainer.container.firstChild.innerHTML + ); }); - it('shows different images', () => { - // predefined images - Object.keys(images).forEach((image) => { - document.body.innerHTML = ''; - const icon = render(React.createElement(images[image])); - render(); - const renderedIcon = screen.getByTestId(iconID); - expect(renderedIcon.innerHTML).toEqual( - icon.container.firstChild.innerHTML - ); - }); - // passing custom image - document.body.innerHTML = ''; + it('shows custom image', () => { const icon = render(React.createElement(CustomIcon)); - render(); - const renderedIcon = screen.getByTestId(iconID); + render(); + const renderedIcon = screen.getByTestId(`${testID}-icon`); expect(renderedIcon.innerHTML).toEqual(icon.container.firstChild.innerHTML); }); @@ -73,21 +74,36 @@ describe('EmptyState', () => { ); // has button - expect(screen.getByRole('button')).toBeTruthy(); + expect(screen.getByTestId(`${testID}-action`)).toBeTruthy(); + // has label - expect(screen.getByText(actionLabel)).toBeTruthy(); + expect(screen.getByTestId(`${testID}-action`).textContent).toEqual( + actionLabel + ); + + // has no link + expect(screen.queryByTestId(`${testID}-secondaryAction`)).toBeNull(); // onclick called - userEvent.click(screen.getByRole('button')); + userEvent.click( + screen.getByTestId(`${testID}-action`).querySelector('button') + ); expect(onClick).toHaveBeenCalled(); + }); - // passing custom component - document.body.innerHTML = ''; - const customAction =
Hello
; - render(); + // it('shows custom action component', () => { + // const customAction =
Hello
; + // render(); - expect(screen.getByTestId('customcomponent')).toBeTruthy(); - }); + // expect(screen.getByTestId('customcomponent')).toBeTruthy(); + // }); + + // it('shows custom secondaryAction component', () => { + // const customAction =
Hello
; + // render(); + + // expect(screen.getByTestId('customcomponent')).toBeTruthy(); + // }); it('shows secondaryAction if desired', () => { const actionLabel = 'TestLink'; @@ -100,20 +116,50 @@ describe('EmptyState', () => { /> ); // has no button - expect(screen.queryByRole('button')).toBeNull(); + expect(screen.queryByTestId(`${testID}-action`)).toBeNull(); // has label - expect(screen.getByText(actionLabel)).toBeTruthy(); + expect(screen.getByTestId(`${testID}-secondaryAction`).textContent).toEqual( + actionLabel + ); // onclick called - userEvent.click(screen.getByText(actionLabel)); + userEvent.click( + screen.getByTestId(`${testID}-secondaryAction`).querySelector('a') + ); expect(onClick).toHaveBeenCalled(); + }); - // passing custom component - document.body.innerHTML = ''; - const customAction =
Hello
; - render(); + it('shows both actions if desired', () => { + const actionLabel = 'TestButton'; + const actionOnClick = jest.fn(); + const secondaryActionOnClick = jest.fn(); + + render( + + ); + + // has link and button with right content + expect(screen.getByTestId(`${testID}-action`).textContent).toEqual( + actionLabel + ); + expect(screen.getByTestId(`${testID}-secondaryAction`).textContent).toEqual( + actionLabel + ); + + // onclick called + userEvent.click( + screen.getByTestId(`${testID}-action`).querySelector('button') + ); + userEvent.click( + screen.getByTestId(`${testID}-secondaryAction`).querySelector('a') + ); - expect(screen.getByTestId('customcomponent')).toBeTruthy(); + expect(actionOnClick).toHaveBeenCalled(); + expect(secondaryActionOnClick).toHaveBeenCalled(); }); }); diff --git a/src/components/EmptyState/__snapshots__/EmptyState.story.storyshot b/src/components/EmptyState/__snapshots__/EmptyState.story.storyshot index 6ff5f088a2..0580788ff5 100644 --- a/src/components/EmptyState/__snapshots__/EmptyState.story.storyshot +++ b/src/components/EmptyState/__snapshots__/EmptyState.story.storyshot @@ -14,6 +14,7 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT/Empty >
@@ -160,16 +161,19 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT/Empty

Uh oh. Something’s not right.

Optional extra sentence or sentences to describe further details about the error and, if applicable, how the user can fix it.