Skip to content

Commit

Permalink
Addon-A11y: Show errors, reset config properly (#8779)
Browse files Browse the repository at this point in the history
Addon-A11y: Show errors, reset config properly
  • Loading branch information
shilman committed Nov 14, 2019
2 parents aec46e4 + bacccb9 commit 2d18cba
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 19 deletions.
1 change: 0 additions & 1 deletion addons/a11y/README.md
Expand Up @@ -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)
Expand Down
10 changes: 6 additions & 4 deletions addons/a11y/src/components/A11YPanel.test.js
Expand Up @@ -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();
Expand All @@ -72,9 +72,10 @@ describe('A11YPanel', () => {
mount(<ThemedA11YPanel api={api} />);

// 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', () => {
Expand All @@ -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(<ThemedA11YPanel api={api} />);
Expand All @@ -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', () => {
Expand Down
54 changes: 43 additions & 11 deletions addons/a11y/src/components/A11YPanel.tsx
Expand Up @@ -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 }) => (
<div className={className}>
<Icon inline icon="sync" status="running" /> Please wait while the accessibility scan is running
...
</div>
))({
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;
Expand All @@ -84,6 +93,7 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {

api.on(STORY_RENDERED, this.request);
api.on(EVENTS.RESULT, this.onUpdate);
api.on(EVENTS.ERROR, this.onError);
}

componentDidUpdate(prevProps: A11YPanelProps) {
Expand All @@ -101,6 +111,7 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
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) => {
Expand All @@ -124,6 +135,13 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
);
};

onError = (error: unknown) => {
this.setState({
status: 'error',
error,
});
};

request = () => {
const { api, active } = this.props;

Expand All @@ -142,8 +160,22 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
};

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 (
<div style={centeredStyle}>
The accessibility scan encountered an error.
<br />
{error}
</div>
);
}

const { passes, violations, incomplete, status } = this.state;

let actionTitle;
if (status === 'ready') {
Expand All @@ -162,7 +194,7 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
);
}

return active ? (
return (
<Fragment>
<Provider store={store}>
{status === 'running' ? (
Expand Down Expand Up @@ -218,6 +250,6 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
/>
</Provider>
</Fragment>
) : null;
);
}
}
Expand Up @@ -177,6 +177,10 @@ exports[`A11YPanel should render report 1`] = `
"storybook/a11y/result",
[Function],
],
Array [
"storybook/a11y/error",
[Function],
],
],
"results": Array [
Object {
Expand All @@ -187,6 +191,10 @@ exports[`A11YPanel should render report 1`] = `
"type": "return",
"value": undefined,
},
Object {
"type": "return",
"value": undefined,
},
],
},
}
Expand Down
3 changes: 2 additions & 1 deletion addons/a11y/src/constants.ts
Expand Up @@ -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 };
13 changes: 11 additions & 2 deletions addons/a11y/src/index.ts
Expand Up @@ -39,20 +39,29 @@ 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)));
});
};

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));

Expand Down

1 comment on commit 2d18cba

@vercel
Copy link

@vercel vercel bot commented on 2d18cba Nov 14, 2019

Choose a reason for hiding this comment

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

Please sign in to comment.