-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'dev' into fix-checkbox-skeleton
- Loading branch information
Showing
35 changed files
with
715 additions
and
77 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
# Testing | ||
|
||
## How to test | ||
|
||
Follow the [Guiding Principles of React Testing Library](https://github.com/testing-library/react-testing-library#guiding-principles) when writing tests. | ||
|
||
## What to test | ||
|
||
### Accessibility | ||
|
||
Automated accessibility tests are required for significant component variations and should use existing `toHaveNoAxeViolations()` and `toHaveNoDAPViolations()` Jest matchers. | ||
|
||
Use this structure for automated accessibility tests: | ||
|
||
```jsx | ||
test('should have no Axe or DAP violations', async () => { | ||
const main = document.createElement('main'); | ||
render(<Component />, { | ||
// DAP requires a landmark '<main>' in the DOM: | ||
container: document.body.appendChild(main), | ||
}); | ||
|
||
await expect(document.body).toHaveNoAxeViolations(); | ||
await expect(document.body).toHaveNoDAPViolations('ComponentName'); | ||
}); | ||
|
||
test('should have no Axe or DAP violations with component variation', async () => { | ||
const main = document.createElement('main'); | ||
render(<Component variationProp={value} />, { | ||
// DAP requires a landmark '<main>' in the DOM: | ||
container: document.body.appendChild(main), | ||
}); | ||
|
||
await expect(document.body).toHaveNoAxeViolations(); | ||
await expect(document.body).toHaveNoDAPViolations( | ||
'ComponentName with variation' | ||
); | ||
}); | ||
``` | ||
|
||
#### Test structure | ||
|
||
- Each significant variation of a component should have its own test block. | ||
- Use an **asynchronous** test block, and **await** each accessibility rule matcher. | ||
|
||
#### DAP requirements | ||
|
||
- DAP requires that components be rendered inside a valid landmark element, which is why the `container` is a `main` node. This extra step may not be required if the component is already wrapped in a `main` or another significant HTML landmark element. | ||
- DAP requires a unique id per test within a given component. (Hence, "ComponentName" and "ComponentName with variation") | ||
|
||
## User events | ||
|
||
### Click and key press events | ||
|
||
Events (`onClick`, `onKeyPress`, etc) that are important to component functionality should be tested using mocks. | ||
|
||
```jsx | ||
test('should invoke close mock when close button is clicked', () => { | ||
const onCloseMock = jest.fn(); | ||
const { getByLabelText } = render( | ||
<Component labelText="test close" onClick={onCloseMock} /> | ||
); | ||
|
||
userEvent.click(getByLabelText(/test close/i)); | ||
expect(onCloseMock).toHaveBeenCalledTimes(1); | ||
}); | ||
``` | ||
|
||
### Tabbing | ||
|
||
Write a series of tab cycle tests in order to verify that a user can tab through the interactive pieces of the component in a predictable way. Tab cycle tests are very useful for finding hidden "focus traps". | ||
|
||
Each tab cycle test should traverse completely through the ideal tab order, in a loop that begins and ends with the first interactive element that receives focus. | ||
|
||
Similar to accessibility tests, a tab cycle test should be written for each significant component variation, especially if a variation adds or removes interactive elements from the component. | ||
|
||
Example of a tab cycle test for `PanelV2`: | ||
|
||
```jsx | ||
test('should cycle panel elements in tab order', () => { | ||
const { getByLabelText, getByText } = render( | ||
<PanelV2 | ||
closeButton={{ | ||
label: 'test close', | ||
}} | ||
renderFooter={() => (<Button>test footer button</Button>)} | ||
> | ||
<PanelContent> | ||
test content text | ||
<Button>test content button</Button> | ||
</PanelContent> | ||
</PanelV2> | ||
); | ||
|
||
userEvent.tab(); | ||
|
||
// The close button: | ||
expect(getByLabelText(/test close/i)).toHaveFocus(); | ||
|
||
userEvent.tab(); | ||
|
||
// The button inside the `PanelContent` wrapper: | ||
expect(getByText(/test content button/i)).toHaveFocus(); | ||
|
||
userEvent.tab(); | ||
|
||
// The footer button: | ||
expect(getByText(/test footer button/i)).toHaveFocus(); | ||
|
||
userEvent.tab(); | ||
|
||
// Loop complete. | ||
// The close button: | ||
expect(getByLabelText(/test close/i)).toHaveFocus(); | ||
}); | ||
``` | ||
|
||
If the component is a single interactive element, like a single `<a>` or `<button>` then a tab cycle test may not be necessary. The `TrendingCard` is an example of a component that does not require a tab cycle test. | ||
|
||
## Props | ||
|
||
### Custom class names | ||
|
||
Ensure that custom class names are checked: | ||
|
||
```jsx | ||
test('should add custom class', () => { | ||
const { getByText } = render( | ||
<ExternalLink href="https://www.ibm.com/security" className="custom-class"> | ||
test link | ||
</ExternalLink> | ||
); | ||
expect(getByText(/test link/i).closest('a')).toHaveClass('custom-class'); | ||
}); | ||
``` | ||
|
||
### Spread attribute | ||
|
||
If a component includes a spread attribute (i.e., `...other` in the props definition), include a test to check that extra props can be passed through to the component. | ||
|
||
You can use a testing attribute like `data-testid` to test for this: | ||
|
||
```jsx | ||
test('should pass through extra props via spread attribute', () => { | ||
const { queryByTestId } = render( | ||
<ExternalLink href="https://www.ibm.com/security" data-testid="test-id"> | ||
test link | ||
</ExternalLink> | ||
); | ||
expect(queryByTestId('test-id')).toBeInTheDocument(); | ||
}); | ||
``` | ||
|
||
### Object or array of significant values | ||
|
||
If an object or array has significant values, then those values should be tested individually. "Significant values" in this case means any values that render completely different nodes, or that individually could cause an error. | ||
|
||
For example, with the `ICA`'s `locale` prop, it makes sense to verify that each possible `locale` value does not throw an error: | ||
|
||
```jsx | ||
// Note: Locales is an array of values. | ||
Locales.forEach(locale => | ||
test(`should accept '${locale}' locale`, () => { | ||
const { container } = render( | ||
<ICA label="test ICA" value={10} locale={locale} /> | ||
); | ||
expect(() => container).not.toThrow(); | ||
}) | ||
); | ||
``` | ||
|
||
And in another example for the `StatusIcon`'s `status` prop, it makes sense to run an accessibility test on each type of `status` because each `status` renders completely different Nodes: | ||
|
||
```jsx | ||
// Note: STATUS is an array of values. | ||
STATUS.forEach(status => | ||
test(`should have no Axe or DAP violations when \`status\` is '${status}'`, async () => { | ||
const main = document.createElement('main'); | ||
render(<StatusIcon status={status} message="test message" />, { | ||
// DAP requires a landmark '<main>' in the DOM: | ||
container: document.body.appendChild(main), | ||
}); | ||
await expect(document.body).toHaveNoAxeViolations(); | ||
await expect(document.body).toHaveNoDAPViolations( | ||
`StatusIcon with ${status} status` | ||
); | ||
}) | ||
); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
src/components/SummaryCard/SummaryCardAction/__tests__/SummaryCardAction.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
/** | ||
* @file Summary card tests. | ||
* @copyright IBM Security 2020 | ||
*/ | ||
|
||
import { render } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import React from 'react'; | ||
|
||
import { SummaryCardAction } from '../../../..'; | ||
|
||
import { carbonPrefix } from '../../../../globals/namespace'; | ||
|
||
describe('SummaryCardAction', () => { | ||
test('should not be interactive when loading', () => { | ||
const { getByText } = render( | ||
<div> | ||
{/* This button is NOT interactive: */} | ||
<SummaryCardAction loading>test loading button</SummaryCardAction> | ||
{/* This button is interactive: */} | ||
<button>test example button</button> | ||
</div> | ||
); | ||
|
||
// Expect button to have `disabled` attribute: | ||
expect(getByText(/test loading button/i).closest('button')).toHaveAttribute( | ||
'disabled' | ||
); | ||
|
||
userEvent.tab(); | ||
|
||
// Expect the loading button, listed first, not to be focussed: | ||
expect(getByText(/test loading button/i)).not.toHaveFocus(); | ||
|
||
// Expect the example button, listed second, to be focussed: | ||
expect(getByText(/test example button/i)).toHaveFocus(); | ||
|
||
userEvent.tab(); | ||
|
||
// Expect the example button to still be focussed: | ||
expect(getByText(/test example button/i)).toHaveFocus(); | ||
}); | ||
|
||
test('should accept `expandedContent` and only show when activated', () => { | ||
const { getByText, queryByText, getByTestId } = render( | ||
<SummaryCardAction | ||
closeButtonIconDescription="test close button" | ||
expandedContent="test expanded action content" | ||
data-testid="test-button-id" | ||
> | ||
test button | ||
</SummaryCardAction> | ||
); | ||
|
||
// Expect expanded content to NOT be in the document: | ||
expect( | ||
queryByText(/test expanded action content/i) | ||
).not.toBeInTheDocument(); | ||
|
||
// Expect `aria-expanded` attribute to be `false`: | ||
expect(getByTestId('test-button-id')).toHaveAttribute( | ||
'aria-expanded', | ||
'false' | ||
); | ||
|
||
// Click on the action button to show expanded content: | ||
userEvent.click(getByText('test button')); | ||
|
||
// Expect expanded content to be visible: | ||
expect(queryByText(/test expanded action content/i)).toBeVisible(); | ||
|
||
// Expect `aria-expanded` attribute to be `true`: | ||
expect(getByTestId('test-button-id')).toHaveAttribute( | ||
'aria-expanded', | ||
'true' | ||
); | ||
}); | ||
|
||
test('should accept a custom class name', () => { | ||
const { container } = render( | ||
<SummaryCardAction className="custom-class" /> | ||
); | ||
expect(container.firstElementChild).toHaveClass('custom-class'); | ||
}); | ||
|
||
test("should add a description to the expanded content's close button via `closeButtonIconDescription`", () => { | ||
const { getByText, getAllByLabelText } = render( | ||
<SummaryCardAction | ||
expandedContent="test content" | ||
closeButtonIconDescription="test icon label" | ||
> | ||
test button | ||
</SummaryCardAction> | ||
); | ||
// Must open the expanded content to see the close button: | ||
userEvent.click(getByText('test button')); | ||
// Use `getAll*` & check for 2 occurances because the icon button | ||
// applies the label to its `title` and `aria-label` attributes: | ||
expect(getAllByLabelText(/test icon label/i).length).toBe(2); | ||
}); | ||
|
||
test('should accept children', () => { | ||
const { queryByText } = render( | ||
<SummaryCardAction>test content</SummaryCardAction> | ||
); | ||
expect(queryByText(/test content/i)).toBeVisible(); | ||
}); | ||
|
||
test('should pass through extra props via spread attribute', () => { | ||
const { queryByTestId } = render( | ||
<SummaryCardAction data-testid="test-id" /> | ||
); | ||
expect(queryByTestId('test-id')).toBeVisible(); | ||
}); | ||
|
||
test('should apply assistive text and icon-only class name when `hasIconOnly` is `true`', () => { | ||
const { queryByText } = render( | ||
<SummaryCardAction iconDescription="test icon" hasIconOnly /> | ||
); | ||
// Expect icon description to be applied as assistive text: | ||
expect(queryByText(/test icon/i)).toBeVisible(); | ||
// Expect button to have Carbon's icon-only class: | ||
expect(queryByText(/test icon/i).closest('button')).toHaveClass( | ||
`${carbonPrefix}btn--icon-only` | ||
); | ||
}); | ||
|
||
test('should invoke click mock when button is clicked', () => { | ||
const onClickMock = jest.fn(); | ||
const { getByText } = render( | ||
<SummaryCardAction onClick={onClickMock}>test content</SummaryCardAction> | ||
); | ||
userEvent.click(getByText(/test content/i)); | ||
expect(onClickMock).toHaveBeenCalledTimes(1); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.