Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A11y: Create a11y test provider and revamp a11y addon #29643

Draft
wants to merge 37 commits into
base: next
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a913610
Implement afterEach
kasperpeulen Nov 11, 2024
3065626
Test afterEach
kasperpeulen Nov 12, 2024
83af2a6
Add afterEach normalizers
kasperpeulen Nov 12, 2024
8029b37
Fix test
kasperpeulen Nov 12, 2024
b918869
Bump @storybook/csf
kasperpeulen Nov 12, 2024
312b25d
Make dedicated order-of-hooks test
kasperpeulen Nov 12, 2024
e038e8b
Fix tests
kasperpeulen Nov 14, 2024
dd242dc
Merge remote-tracking branch 'origin/kasper/after-each' into valentin…
valentinpalkovic Nov 14, 2024
57a8cea
Core: Implement Story Completed
valentinpalkovic Nov 14, 2024
6d30bb9
Remove unhandledException from STORY_COMPLETED event
valentinpalkovic Nov 14, 2024
e49bc0f
Rename storyCompleted to storyFinished
valentinpalkovic Nov 14, 2024
b7c1d02
Update @storybook/csf
valentinpalkovic Nov 14, 2024
aa95d2a
Rename storyCompleted to storyFinished
valentinpalkovic Nov 14, 2024
08477ec
Refactor status of the story finished payload
valentinpalkovic Nov 14, 2024
c9c7249
Move afterEach after storyCompleted
valentinpalkovic Nov 15, 2024
d907091
Add StoryFinished payload test
valentinpalkovic Nov 15, 2024
96dad8c
Enhance Report interface to support generic result type
valentinpalkovic Nov 15, 2024
7668055
Remove redundant comments in Preview component for clarity
valentinpalkovic Nov 15, 2024
b80ee26
Refactor a11y addon to use afterEach and reporting API
valentinpalkovic Nov 15, 2024
c8647d4
Add tests
valentinpalkovic Nov 18, 2024
bc3da7a
Fix tests
valentinpalkovic Nov 18, 2024
a93ea7d
Fix E2E tests
valentinpalkovic Nov 19, 2024
d3923d2
Add stories for A11yPanel
valentinpalkovic Nov 19, 2024
2eebee1
Add discrepancy handling to A11yPanel
valentinpalkovic Nov 19, 2024
79ad98c
Fix tests
valentinpalkovic Nov 20, 2024
6dfea64
Merge pull request #29661 from storybookjs/valentin/add-a11y-discrepa…
valentinpalkovic Nov 20, 2024
667d403
Merge remote-tracking branch 'origin/next' into valentin/unified-a11y…
valentinpalkovic Nov 20, 2024
003e808
Fix potential undefined issue
valentinpalkovic Nov 20, 2024
141de06
Fix tests
valentinpalkovic Nov 20, 2024
66931ce
Fix tests
valentinpalkovic Nov 20, 2024
9622763
Support new parameters and globals for a11y
valentinpalkovic Nov 21, 2024
cfae158
Use vitest-axe for formatting purposes instead
valentinpalkovic Nov 22, 2024
3f1ae4e
Fix environment usage in preview file
valentinpalkovic Nov 22, 2024
9ed6077
Fix environment usage in preview file
valentinpalkovic Nov 22, 2024
4f485f3
Fix tests
valentinpalkovic Nov 22, 2024
c4039e1
Merge pull request #29682 from storybookjs/valentin/support-new-param…
valentinpalkovic Nov 22, 2024
e4604d8
Merge remote-tracking branch 'origin/next' into valentin/unified-a11y…
valentinpalkovic Nov 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions code/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,9 @@ export const parameters = {
opacity: 0.4,
},
},
a11y: {
warnings: ['minor', 'moderate', 'serious', 'critical'],
},
};

export const tags = ['test', 'vitest'];
5 changes: 3 additions & 2 deletions code/.storybook/storybook.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { userEvent as storybookEvent, expect as storybookExpect } from '@storybo
// eslint-disable-next-line import/namespace
import * as testAnnotations from '@storybook/experimental-addon-test/preview';

import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview';

import * as coreAnnotations from '../addons/toolbars/template/stories/preview';
import * as componentAnnotations from '../core/template/stories/preview';
// register global components used in many stories
Expand All @@ -15,10 +17,9 @@ import * as projectAnnotations from './preview';
vi.spyOn(console, 'warn').mockImplementation((...args) => console.log(...args));

const annotations = setProjectAnnotations([
// @ts-expect-error check type errors later
projectAnnotations,
// @ts-expect-error check type errors later
componentAnnotations,
a11yAddonAnnotations,
coreAnnotations,
testAnnotations,
{
Expand Down
35 changes: 31 additions & 4 deletions code/addons/a11y/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,9 @@ MyNonCheckedStory.parameters = {
};
```

## Parameters
## Parameters and globals

For more customizability use parameters to configure [aXe options](https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#api-name-axeconfigure).
For more customizability use parameters and globals to configure [aXe options](https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#api-name-axeconfigure).
You can override these options [at story level too](https://storybook.js.org/docs/react/configure/features-and-behavior#per-story-options).

```js
Expand All @@ -180,10 +180,16 @@ export default {
config: {},
// axe-core optionsParameter (https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#options-parameter)
options: {},
// optional flag to prevent the automatic check
manual: true,
// Defines which impact levels will be considered as warnings instead of errors if executed via Storybook's component testing
warnings: <'minor' | 'moderate' | 'serious' | 'critical'>[]
},
},
globals: {
a11y: {
// optional flag to prevent the automatic check
manual: true,
}
}
};

export const accessible = () => <button>Accessible button</button>;
Expand All @@ -193,6 +199,27 @@ export const inaccessible = () => (
);
```

## Violation impact levels

By default, the addon will consider all violations as errors. However, you can configure the addon to consider some violations as warnings instead of errors. This can be useful when @storybook/addon-a11y is used in combination with `@storybook/experimental-addon-test`. To do this, set the `warnings` parameter in the `a11y` object to an array of impact levels that should be considered as warnings.

```js
export default {
title: 'button',
parameters: {
a11y: {
/**
* @default [ ]
* @type: Array<'minor' | 'moderate' | 'serious' | 'critical'>
*/
warnings: ['minor', 'moderate'],
},
},
};
```

In this example, all violations with an impact level of `minor` or `moderate` will be considered as warnings. All other violations will be considered as errors. When running automated UI tests featured by Vitest, all violations with an impact level of `serious` or `critical` will now fail the test. This failure is reflected as an error in the sidebar or when running Vitest separately. `minor` and `moderate` violations will be reported as warnings but will not fail the test.

## Automate accessibility tests with test runner

The test runner does not apply any rules that you have set on your stories by default. You can configure the runner to correctly apply the rules by [following the guide on the Storybook docs](https://storybook.js.org/docs/writing-tests/accessibility-testing#automate-accessibility-tests-with-test-runner).
Expand Down
6 changes: 5 additions & 1 deletion code/addons/a11y/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,16 @@
},
"dependencies": {
"@storybook/addon-highlight": "workspace:*",
"axe-core": "^4.2.0"
"@storybook/test": "workspace:*",
"axe-core": "^4.2.0",
"vitest-axe": "^0.1.0"
},
"devDependencies": {
"@storybook/global": "^5.0.0",
"@storybook/icons": "^1.2.12",
"@testing-library/react": "^14.0.0",
"picocolors": "^1.1.0",
"pretty-format": "29.7.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-resize-detector": "^7.1.2",
Expand Down
1 change: 0 additions & 1 deletion code/addons/a11y/src/a11yRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ describe('a11yRunner', () => {
await import('./a11yRunner');

expect(mockedAddons.getChannel).toHaveBeenCalled();
expect(mockChannel.on).toHaveBeenCalledWith(EVENTS.REQUEST, expect.any(Function));
expect(mockChannel.on).toHaveBeenCalledWith(EVENTS.MANUAL, expect.any(Function));
});
});
72 changes: 22 additions & 50 deletions code/addons/a11y/src/a11yRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,64 +8,36 @@ import type { A11yParameters } from './params';
const { document } = global;

const channel = addons.getChannel();
// Holds axe core running state
let active = false;
// Holds latest story we requested a run
let activeStoryId: string | undefined;

const defaultParameters = { config: {}, options: {} };

/** Handle A11yContext events. Because the event are sent without manual check, we split calls */
const handleRequest = async (storyId: string, input: A11yParameters | null) => {
if (!input?.manual) {
await run(storyId, input ?? defaultParameters);
}
};

const run = async (storyId: string, input: A11yParameters = defaultParameters) => {
activeStoryId = storyId;
try {
if (!active) {
active = true;
channel.emit(EVENTS.RUNNING);
const { default: axe } = await import('axe-core');

const { element = '#storybook-root', config, options = {} } = input;
const htmlElement = document.querySelector(element as string);
export const run = async (input: A11yParameters = defaultParameters) => {
const { default: axe } = await import('axe-core');

if (!htmlElement) {
return;
}
const { element = '#storybook-root', config, options = {} } = input;
const htmlElement = document.querySelector(element as string) ?? document.body;

axe.reset();
if (config) {
axe.configure(config);
}
if (!htmlElement) {
return;
}
Comment on lines +20 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Should emit an error event when htmlElement is not found rather than silently returning undefined


const result = await axe.run(htmlElement, options);
axe.reset();
if (config) {
axe.configure(config);
}

// Axe result contains class instances, which telejson deserializes in a
// way that violates:
// Content Security Policy directive: "script-src 'self' 'unsafe-inline'".
const resultJson = JSON.parse(JSON.stringify(result));
return axe.run(htmlElement, options);
};

// It's possible that we requested a new run on a different story.
// Unfortunately, axe doesn't support a cancel method to abort current run.
// We check if the story we run against is still the current one,
// if not, trigger a new run using the current story
if (activeStoryId === storyId) {
channel.emit(EVENTS.RESULT, resultJson);
} else {
active = false;
run(activeStoryId);
}
}
channel.on(EVENTS.MANUAL, async (storyId: string, input: A11yParameters = defaultParameters) => {
try {
const result = await run(input);
// Axe result contains class instances, which telejson deserializes in a
// way that violates:
// Content Security Policy directive: "script-src 'self' 'unsafe-inline'".
const resultJson = JSON.parse(JSON.stringify(result));
channel.emit(EVENTS.RESULT, resultJson, storyId);
} catch (error) {
channel.emit(EVENTS.ERROR, error);
} finally {
active = false;
}
};

channel.on(EVENTS.REQUEST, handleRequest);
channel.on(EVENTS.MANUAL, run);
});
valentinpalkovic marked this conversation as resolved.
Show resolved Hide resolved
Loading