diff --git a/addons/a11y/README.md b/addons/a11y/README.md
index 584e49f67028..d4d2638a9e6c 100755
--- a/addons/a11y/README.md
+++ b/addons/a11y/README.md
@@ -56,7 +56,6 @@ import { withA11y } from '@storybook/addon-a11y';
addDecorator(withA11y)
addParameters({
a11y: {
- // ... axe options
element: '#root', // optional selector which element to inspect
config: {}, // axe-core configurationOptions (https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#parameters-1)
options: {} // axe-core optionsParameter (https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#options-parameter)
diff --git a/addons/a11y/src/components/A11YPanel.test.js b/addons/a11y/src/components/A11YPanel.test.js
index 5c57664c573c..2f84b31afd39 100644
--- a/addons/a11y/src/components/A11YPanel.test.js
+++ b/addons/a11y/src/components/A11YPanel.test.js
@@ -63,7 +63,7 @@ function ThemedA11YPanel(props) {
}
describe('A11YPanel', () => {
- it('should register STORY_RENDERED and RESULT updater on mount', () => {
+ it('should register STORY_RENDERED, RESULT and ERROR updater on mount', () => {
// given
const api = createApi();
expect(api.on).not.toHaveBeenCalled();
@@ -72,9 +72,10 @@ describe('A11YPanel', () => {
mount();
// then
- expect(api.on.mock.calls.length).toBe(2);
+ expect(api.on.mock.calls.length).toBe(3);
expect(api.on.mock.calls[0][0]).toBe(STORY_RENDERED);
expect(api.on.mock.calls[1][0]).toBe(EVENTS.RESULT);
+ expect(api.on.mock.calls[2][0]).toBe(EVENTS.ERROR);
});
it('should request a run on tab activation', () => {
@@ -93,7 +94,7 @@ describe('A11YPanel', () => {
expect(wrapper.find(ScrollArea).length).toBe(0);
});
- it('should deregister STORY_RENDERED and RESULT updater on unmount', () => {
+ it('should deregister STORY_RENDERED, RESULT and ERROR updater on unmount', () => {
// given
const api = createApi();
const wrapper = mount();
@@ -103,9 +104,10 @@ describe('A11YPanel', () => {
wrapper.unmount();
// then
- expect(api.off.mock.calls.length).toBe(2);
+ expect(api.off.mock.calls.length).toBe(3);
expect(api.off.mock.calls[0][0]).toBe(STORY_RENDERED);
expect(api.off.mock.calls[1][0]).toBe(EVENTS.RESULT);
+ expect(api.off.mock.calls[2][0]).toBe(EVENTS.ERROR);
});
it('should update run result', () => {
diff --git a/addons/a11y/src/components/A11YPanel.tsx b/addons/a11y/src/components/A11YPanel.tsx
index 5f0c1fffb61f..194acca7be6d 100644
--- a/addons/a11y/src/components/A11YPanel.tsx
+++ b/addons/a11y/src/components/A11YPanel.tsx
@@ -46,26 +46,35 @@ const Incomplete = styled.span<{}>(({ theme }) => ({
color: theme.color.warning,
}));
+const centeredStyle = {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: '100%',
+};
+
const Loader = styled(({ className }) => (
Please wait while the accessibility scan is running
...
-))({
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- height: '100%',
-});
+))(centeredStyle);
Loader.displayName = 'Loader';
-interface A11YPanelState {
- status: string;
+interface A11YPanelNormalState {
+ status: 'ready' | 'ran' | 'running';
passes: Result[];
violations: Result[];
incomplete: Result[];
}
+interface A11YPanelErrorState {
+ status: 'error';
+ error: unknown;
+}
+
+type A11YPanelState = A11YPanelNormalState | A11YPanelErrorState;
+
interface A11YPanelProps {
active: boolean;
api: API;
@@ -84,6 +93,7 @@ export class A11YPanel extends Component {
api.on(STORY_RENDERED, this.request);
api.on(EVENTS.RESULT, this.onUpdate);
+ api.on(EVENTS.ERROR, this.onError);
}
componentDidUpdate(prevProps: A11YPanelProps) {
@@ -101,6 +111,7 @@ export class A11YPanel extends Component {
const { api } = this.props;
api.off(STORY_RENDERED, this.request);
api.off(EVENTS.RESULT, this.onUpdate);
+ api.off(EVENTS.ERROR, this.onError);
}
onUpdate = ({ passes, violations, incomplete }: AxeResults) => {
@@ -124,6 +135,13 @@ export class A11YPanel extends Component {
);
};
+ onError = (error: unknown) => {
+ this.setState({
+ status: 'error',
+ error,
+ });
+ };
+
request = () => {
const { api, active } = this.props;
@@ -142,8 +160,22 @@ export class A11YPanel extends Component {
};
render() {
- const { passes, violations, incomplete, status } = this.state;
const { active } = this.props;
+ if (!active) return null;
+
+ // eslint-disable-next-line react/destructuring-assignment
+ if (this.state.status === 'error') {
+ const { error } = this.state;
+ return (
+
+ The accessibility scan encountered an error.
+
+ {error}
+
+ );
+ }
+
+ const { passes, violations, incomplete, status } = this.state;
let actionTitle;
if (status === 'ready') {
@@ -162,7 +194,7 @@ export class A11YPanel extends Component {
);
}
- return active ? (
+ return (
{status === 'running' ? (
@@ -218,6 +250,6 @@ export class A11YPanel extends Component {
/>
- ) : null;
+ );
}
}
diff --git a/addons/a11y/src/components/__snapshots__/A11YPanel.test.js.snap b/addons/a11y/src/components/__snapshots__/A11YPanel.test.js.snap
index ecdb07f6dbde..9250f79b1c82 100644
--- a/addons/a11y/src/components/__snapshots__/A11YPanel.test.js.snap
+++ b/addons/a11y/src/components/__snapshots__/A11YPanel.test.js.snap
@@ -177,6 +177,10 @@ exports[`A11YPanel should render report 1`] = `
"storybook/a11y/result",
[Function],
],
+ Array [
+ "storybook/a11y/error",
+ [Function],
+ ],
],
"results": Array [
Object {
@@ -187,6 +191,10 @@ exports[`A11YPanel should render report 1`] = `
"type": "return",
"value": undefined,
},
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
],
},
}
diff --git a/addons/a11y/src/constants.ts b/addons/a11y/src/constants.ts
index d5b153986461..efabac7a268b 100755
--- a/addons/a11y/src/constants.ts
+++ b/addons/a11y/src/constants.ts
@@ -6,5 +6,6 @@ export const ADD_ELEMENT = 'ADD_ELEMENT';
export const CLEAR_ELEMENTS = 'CLEAR_ELEMENTS';
const RESULT = `${ADDON_ID}/result`;
const REQUEST = `${ADDON_ID}/request`;
+const ERROR = `${ADDON_ID}/error`;
-export const EVENTS = { RESULT, REQUEST };
+export const EVENTS = { RESULT, REQUEST, ERROR };
diff --git a/addons/a11y/src/index.ts b/addons/a11y/src/index.ts
index d1b8998a58dc..9f91b8492b79 100644
--- a/addons/a11y/src/index.ts
+++ b/addons/a11y/src/index.ts
@@ -40,7 +40,8 @@ const run = (element: ElementContext, config: Spec, options: RunOptions) => {
restoreScroll: true,
} as RunOptions) // cast to RunOptions is necessary because axe types are not up to date
)
- .then(report);
+ .then(report)
+ .catch(error => addons.getChannel().emit(EVENTS.ERROR, String(error)));
});
};
@@ -48,12 +49,20 @@ if (module && module.hot && module.hot.decline) {
module.hot.decline();
}
+let storedDefaultSetup: Setup | null = null;
+
export const withA11y = makeDecorator({
name: 'withA11Y',
parameterName: PARAM_KEY,
wrapper: (getStory, context, { parameters }) => {
if (parameters) {
- setup = parameters as Setup;
+ if (storedDefaultSetup === null) {
+ storedDefaultSetup = { ...setup };
+ }
+ Object.assign(setup, parameters as Setup);
+ } else if (storedDefaultSetup !== null) {
+ Object.assign(setup, storedDefaultSetup);
+ storedDefaultSetup = null;
}
addons.getChannel().on(EVENTS.REQUEST, () => run(setup.element, setup.config, setup.options));