Skip to content

Commit 09a258f

Browse files
authored
Add support for automated Storybook accessibility testing (#4820)
* quick convert to csf 3 * cleanup and disabling aria-hidden rule * adding storybook test runner * sample failures for test to catch * Revert "sample failures for test to catch" This reverts commit 36fa68a. * experimenting with interaction test * poking around to try and ignore aria rule for specific selector * test catching render errors and reuse play functions * using story axe config instead of manually setting it in main * fix lint and add keyboard interaction test * experiementing with jest-axe * testing interaction panel with simpler component * update yarn.lock * comments and cleanup * removing axe-core stuff this is a working commit before version bumping storybook interactions, for some reason it is breaking * disabling some false positives and run against all stories * fix storyName warning from storybook test runner * adding partial DnD interactions test * fixing yarn.lock after rebase * fixing package.json from rebase * update and cleanup tests + update to data test ids * fix/ignore various story only aXe issues (Card,Dialog, Landmark, DnD) some are fixed by adding ids/labels, other are ignored because they are story only tests cases and not part of the actual component accessibility * fix/ignore various story only aXe issues (ListBox, NumberField, Table, Toast, RAC) * fix/ignore various story only aXe issues (FocusScope, List, Checkbox, Meter, SideNav * fixing the rest of the failing false positive tests * fix lint
1 parent c6313b4 commit 09a258f

38 files changed

+3088
-108
lines changed

.storybook/test-runner.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
const {configureAxe, checkA11y, injectAxe} = require('axe-playwright');
2+
const {getStoryContext} = require('@storybook/test-runner');
3+
4+
5+
/*
6+
* See https://storybook.js.org/docs/react/writing-tests/test-runner#test-hook-api-experimental
7+
* to learn more about the test-runner hooks API.
8+
*/
9+
module.exports = {
10+
async preRender(page) {
11+
await injectAxe(page);
12+
},
13+
async postRender(page, context) {
14+
// Grab accessibility settings from the story itself
15+
const storyContext = await getStoryContext(page, context);
16+
if (storyContext.parameters?.a11y?.disable) {
17+
return;
18+
}
19+
20+
await configureAxe(page, {
21+
// TODO: Ideally would have a selector target for the storybook's sb main body element
22+
rules: [
23+
{
24+
id: 'color-contrast',
25+
selector: 'body *:not([data-a11y-ignore="color-contrast"])'
26+
},
27+
{
28+
id: 'aria-hidden-focus',
29+
selector: 'body *:not([data-a11y-ignore="aria-hidden-focus"])',
30+
},
31+
...(storyContext.parameters?.a11y?.config?.rules ?? [])
32+
]
33+
});
34+
35+
await checkA11y(page, '#root', {
36+
detailedReport: true,
37+
detailedReportOptions: {
38+
html: true,
39+
},
40+
axeOptions: storyContext.parameters?.a11y?.options,
41+
});
42+
},
43+
};

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"build:docs": "DOCS_ENV=staging parcel build 'packages/@react-{spectrum,aria,stately}/*/docs/*.mdx' 'packages/react-aria-components/docs/*.mdx' 'packages/@internationalized/*/docs/*.mdx' 'packages/dev/docs/pages/**/*.mdx'",
2323
"test": "cross-env STRICT_MODE=1 yarn jest",
2424
"test-loose": "yarn jest",
25+
"test-storybook": "test-storybook --url http://localhost:9003 --browsers chromium --no-cache",
2526
"build": "make build",
2627
"test:ssr": "cross-env STRICT_MODE=1 yarn jest --config jest.ssr.config.js",
2728
"ci-test": "cross-env STRICT_MODE=1 yarn jest --maxWorkers=2 && cross-env STRICT_MODE=1 yarn test:ssr --runInBand",
@@ -80,10 +81,14 @@
8081
"@storybook/addon-a11y": "^6.5.12",
8182
"@storybook/addon-actions": "^6.5.12",
8283
"@storybook/addon-controls": "^6.5.12",
84+
"@storybook/addon-links": "^6.5.12",
8385
"@storybook/addons": "^6.5.12",
8486
"@storybook/api": "^6.5.12",
8587
"@storybook/components": "^6.5.12",
88+
"@storybook/jest": "^0.0.10",
8689
"@storybook/react": "^6.5.12",
90+
"@storybook/test-runner": "^0.9.0",
91+
"@storybook/testing-library": "^0.0.13",
8792
"@storybook/testing-react": "^1.3.0",
8893
"@testing-library/dom": "^9.2.0",
8994
"@testing-library/jest-dom": "^5.16.5",
@@ -95,6 +100,7 @@
95100
"@typescript-eslint/parser": "^5.40.0",
96101
"autoprefixer": "^9.6.0",
97102
"axe-core": "^4.6.3",
103+
"axe-playwright": "^1.1.11",
98104
"babel-plugin-istanbul": "^6.0.0",
99105
"babel-plugin-macros": "^3.0.1",
100106
"babel-plugin-react-remove-properties": "^0.3.0",

packages/@react-aria/dnd/stories/DraggableListBox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function DraggableListBox(props) {
3737
useDraggableCollection({}, dragState, ref);
3838

3939
return (
40-
<ul {...listBoxProps} ref={ref} className={dndStyles['draggable-listbox']}>
40+
<ul {...listBoxProps} ref={ref} className={dndStyles['draggable-listbox']} aria-label="example draggable listbox">
4141
{[...state.collection].map((item) => (
4242
<Option
4343
key={item.key}

packages/@react-aria/dnd/stories/dnd.stories.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ export const Default = () => (
5858
<Flex direction="column" gap="size-200" alignItems="center">
5959
<Draggable />
6060
<Droppable />
61-
<input />
61+
<input aria-label="test input 1" />
6262
<Droppable type="text/html" />
63-
<input />
63+
<input aria-label="test input 2" />
6464
<Droppable />
6565
</Flex>
6666
);
@@ -74,7 +74,14 @@ export const NestedDropRegions = {
7474
</Droppable>
7575
</Flex>
7676
),
77-
name: 'nested drop regions'
77+
name: 'nested drop regions',
78+
parameters: {
79+
a11y: {
80+
config: {
81+
rules: [{id: 'nested-interactive', enabled: false}]
82+
}
83+
}
84+
}
7885
};
7986

8087
export const DraggableListbox = {

packages/@react-aria/focus/stories/FocusScope.stories.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,11 @@ export function Example({isPortaled, contain}: StoryProps) {
9494

9595
return (
9696
<div>
97-
<input />
98-
97+
<input aria-label="input before" />
9998
<button type="button" onClick={() => setOpen(true)}>
10099
Open dialog
101100
</button>
102-
<input />
101+
<input aria-label="input after" />
103102
{open && <NestedDialog onClose={() => setOpen(false)} isPortaled={isPortaled} contain={contain} />}
104103

105104
<div id={dialogsRoot} />

packages/@react-aria/interactions/stories/useFocusRing.stories.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,15 @@ export default {
3939

4040
export const SearchTableview = {
4141
render: () => <SearchExample />,
42-
name: 'search + tableview'
42+
name: 'search + tableview',
43+
parameters: {
44+
a11y: {
45+
config: {
46+
// Fails due to TableView's known issue, ignoring here since it isn't pertinent to the story
47+
rules: [{id: 'aria-required-children', selector: '*:not([role="grid"])'}]
48+
}
49+
}
50+
}
4351
};
4452

4553
function SearchExample() {
@@ -48,6 +56,7 @@ function SearchExample() {
4856
return (
4957
<div>
5058
<SearchField
59+
aria-label="table searchfield"
5160
onChange={(value) => {
5261
const newItems = manyRows.filter((item) =>
5362
item['C0'].toLowerCase().includes(value.toLowerCase())

packages/@react-aria/landmark/stories/Landmark.stories.tsx

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ function NestedExample() {
219219
);
220220
}
221221

222+
// TODO: known accessiblity failure https://github.com/adobe/react-spectrum/wiki/Known-accessibility-false-positives#tableview
222223
function TableExample() {
223224
return (
224225
<div>
@@ -428,23 +429,53 @@ export const NestedLandmarks = {
428429
};
429430
430431
export const TableLandmark = {
431-
render: TableTemplate
432+
render: TableTemplate,
433+
parameters: {
434+
a11y: {
435+
config: {
436+
// Fails due to TableView's known issue, ignoring here since it isn't pertinent to the story
437+
rules: [{id: 'aria-required-children', selector: '*:not([role="grid"])'}]
438+
}
439+
}
440+
}
432441
};
433442

434443
export const ApplicationWithLandmarks = {
435-
render: ApplicationTemplate
444+
render: ApplicationTemplate,
445+
parameters: {
446+
a11y: {
447+
config: {
448+
// Fails due to TableView's known issue, ignoring here since it isn't pertinent to the story
449+
rules: [{id: 'aria-required-children', selector: '*:not([role="grid"])'}]
450+
}
451+
}
452+
}
436453
};
437454

438455
export const DuplicateRolesWithLabels = {
439456
render: DuplicateRolesWithLabelsTemplate
440457
};
441458

442459
export const DuplicateRolesWithNoLabels = {
443-
render: DuplicateRolesWithNoLabelsTemplate
460+
render: DuplicateRolesWithNoLabelsTemplate,
461+
parameters: {
462+
a11y: {
463+
config: {
464+
rules: [{id: 'landmark-unique', enabled: false}]
465+
}
466+
}
467+
}
444468
};
445469

446470
export const DuplicateRolesWithSameLabels = {
447-
render: DuplicateRolesWithSameLabelsTemplate
471+
render: DuplicateRolesWithSameLabelsTemplate,
472+
parameters: {
473+
a11y: {
474+
config: {
475+
rules: [{id: 'landmark-unique', enabled: false}]
476+
}
477+
}
478+
}
448479
};
449480

450481
export const OneWithNoFocusableChildren = {
@@ -455,4 +486,14 @@ export const AllWithNoFocusableChildren = {
455486
render: AllWithNoFocusableChildrenExampleTemplate
456487
};
457488

458-
export {IframeExample};
489+
export const IframeExampleStory = {
490+
render: IframeExample,
491+
parameters: {
492+
a11y: {
493+
config: {
494+
rules: [{id: 'aria-allowed-role', enabled: false}]
495+
}
496+
}
497+
},
498+
name: 'iframe example'
499+
};

packages/@react-aria/menu/stories/useMenu.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const DoubleMenuFiresOnInteractOutside = {
4343
<Item key="cut">Cut</Item>
4444
<Item key="paste">Paste</Item>
4545
</MenuButton>
46-
<input />
46+
<input aria-label="input after" />
4747
</div>
4848
),
4949
name: 'double menu fires onInteractOutside'

packages/@react-aria/selection/stories/List.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function List<T extends object>(props: ListProps<T>) {
4545
});
4646

4747
return (
48-
<ul ref={ref} {...listProps} >
48+
<ul ref={ref} {...listProps} role="listbox" aria-label={props['aria-label'] ?? 'test listbox'}>
4949
{[...state.collection].map((item) => (
5050
<ListItem key={item.key} item={item} state={state} />
5151
))}

packages/@react-aria/selection/stories/useSelectableList.stories.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,17 @@ const options = [
125125
];
126126

127127
export default {
128-
title: 'useSelectableList'
128+
title: 'useSelectableList',
129+
parameters: {
130+
a11y: {
131+
config: {
132+
rules: [
133+
// Ignore landmark accessibility failure since the list is to test for selection/scrolling only
134+
{id: 'list', enabled: false}
135+
]
136+
}
137+
}
138+
}
129139
};
130140

131141
export const StaticUlStaticSubUl = () => (

packages/@react-aria/table/stories/useTable.stories.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,28 @@ export const ScrollTesting = {
106106

107107
export const ActionTesting = {
108108
render: Template,
109-
args: {selectionBehavior: 'replace', selectionStyle: 'highlight', onAction: action('onAction')}
109+
args: {selectionBehavior: 'replace', selectionStyle: 'highlight', onAction: action('onAction')},
110+
parameters: {
111+
a11y: {
112+
config: {
113+
// False positive, tabbing into the table is handled by us and will focus the row
114+
rules: [{id: 'scrollable-region-focusable', enabled: false}]
115+
}
116+
}
117+
}
110118
};
111119

112120
export const BackwardCompatActionTesting = {
113121
render: TemplateBackwardsCompat,
114-
args: {selectionBehavior: 'replace', selectionStyle: 'highlight', onAction: action('onAction')}
122+
args: {selectionBehavior: 'replace', selectionStyle: 'highlight', onAction: action('onAction')},
123+
parameters: {
124+
a11y: {
125+
config: {
126+
// False positive, tabbing into the table is handled by us and will focus the row
127+
rules: [{id: 'scrollable-region-focusable', enabled: false}]
128+
}
129+
}
130+
}
115131
};
116132

117133
export const TableWithResizingNoProps = {

packages/@react-spectrum/actionbar/stories/ActionBar.stories.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {Example} from './Example';
1717
import React from 'react';
1818
import {useViewportSize} from '@react-aria/utils';
1919

20-
2120
export default {
2221
title: 'ActionBar',
2322
component: ActionBar,
@@ -39,10 +38,19 @@ export default {
3938
export type ActionBarStory = ComponentStoryObj<any>;
4039

4140
export const Default: ActionBarStory = {
42-
render: (args) => <Example {...args} />
41+
render: (args) => <Example {...args} />,
42+
parameters: {
43+
a11y: {
44+
config: {
45+
// Fails due to TableView's known issue, ignoring here since it isn't pertinent to the story
46+
rules: [{id: 'aria-required-children', selector: '*:not([role="grid"])'}]
47+
}
48+
}
49+
}
4350
};
4451

4552
export const FullWidthStory: ActionBarStory = {
53+
...Default,
4654
render: (args) => <FullWidth {...args} />
4755
};
4856

0 commit comments

Comments
 (0)