Skip to content

Commit

Permalink
addon-a11y: allow manual run
Browse files Browse the repository at this point in the history
  • Loading branch information
donaldpipowitch committed Nov 19, 2019
1 parent 123d0c6 commit 229229a
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 109 deletions.
3 changes: 2 additions & 1 deletion addons/a11y/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ addParameters({
a11y: {
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)
options: {}, // axe-core optionsParameter (https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#options-parameter)
manual: true // optional flag to prevent the automatic check
},
});

Expand Down
45 changes: 29 additions & 16 deletions addons/a11y/src/components/A11YPanel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import React from 'react';
import { mount } from 'enzyme';

import { ThemeProvider, themes, convert } from '@storybook/theming';
import { STORY_RENDERED } from '@storybook/core-events';
import { ScrollArea } from '@storybook/components';

import { A11YPanel } from './A11YPanel';
import { EVENTS } from '../constants';
Expand Down Expand Up @@ -63,7 +61,7 @@ function ThemedA11YPanel(props) {
}

describe('A11YPanel', () => {
it('should register STORY_RENDERED, RESULT and ERROR updater on mount', () => {
it('should register event listener on mount', () => {
// given
const api = createApi();
expect(api.on).not.toHaveBeenCalled();
Expand All @@ -73,12 +71,12 @@ describe('A11YPanel', () => {

// then
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);
expect(api.on.mock.calls[0][0]).toBe(EVENTS.RESULT);
expect(api.on.mock.calls[1][0]).toBe(EVENTS.ERROR);
expect(api.on.mock.calls[2][0]).toBe(EVENTS.MANUAL);
});

it('should request a run on tab activation', () => {
it('should show initial state on tab activation', () => {
// given
const api = createApi();

Expand All @@ -90,11 +88,10 @@ describe('A11YPanel', () => {
wrapper.update();

// then
expect(api.emit).toHaveBeenCalledWith(EVENTS.REQUEST);
expect(wrapper.find(ScrollArea).length).toBe(0);
expect(wrapper.find(A11YPanel)).toMatchSnapshot();
});

it('should deregister STORY_RENDERED, RESULT and ERROR updater on unmount', () => {
it('should deregister event listener on unmount', () => {
// given
const api = createApi();
const wrapper = mount(<ThemedA11YPanel api={api} />);
Expand All @@ -105,9 +102,25 @@ describe('A11YPanel', () => {

// then
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);
expect(api.off.mock.calls[0][0]).toBe(EVENTS.RESULT);
expect(api.off.mock.calls[1][0]).toBe(EVENTS.ERROR);
expect(api.off.mock.calls[2][0]).toBe(EVENTS.MANUAL);
});

it('should show manual state depending on config', () => {
// given
const api = createApi();

const wrapper = mount(<ThemedA11YPanel api={api} />);
expect(api.emit).not.toHaveBeenCalled();

// when
wrapper.setProps({ active: true });
api.emit(EVENTS.MANUAL, true);
wrapper.update();

// then
expect(wrapper.find(A11YPanel)).toMatchSnapshot();
});

it('should update run result', () => {
Expand Down Expand Up @@ -141,7 +154,7 @@ describe('A11YPanel', () => {
// given
const api = createApi();
const wrapper = mount(<ThemedA11YPanel api={api} active />);
const request = api.on.mock.calls.find(([event]) => event === STORY_RENDERED)[1];
const request = api.on.mock.calls.find(([event]) => event === EVENTS.MANUAL)[1];

expect(
wrapper
Expand Down Expand Up @@ -170,7 +183,7 @@ describe('A11YPanel', () => {
// given
const api = createApi();
mount(<ThemedA11YPanel api={api} active={false} />);
const request = api.on.mock.calls.find(([event]) => event === STORY_RENDERED)[1];
const request = api.on.mock.calls.find(([event]) => event === EVENTS.MANUAL)[1];
expect(api.emit).not.toHaveBeenCalled();

// when
Expand All @@ -197,7 +210,7 @@ describe('A11YPanel', () => {
// given
const api = createApi();
const wrapper = mount(<ThemedA11YPanel api={api} active />);
const request = api.on.mock.calls.find(([event]) => event === STORY_RENDERED)[1];
const request = api.on.mock.calls.find(([event]) => event === EVENTS.MANUAL)[1];

// when
request();
Expand Down
194 changes: 107 additions & 87 deletions addons/a11y/src/components/A11YPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-disable react/destructuring-assignment,default-case,consistent-return,no-case-declarations */
import React, { Component, Fragment } from 'react';

import { styled } from '@storybook/theming';

import { STORY_RENDERED } from '@storybook/core-events';
import { ActionBar, Icons, ScrollArea } from '@storybook/components';

import { AxeResults, Result } from 'axe-core';
Expand All @@ -20,60 +20,70 @@ export enum RuleType {
INCOMPLETION,
}

const Icon = styled(Icons)(
{
height: '12px',
width: '12px',
marginRight: '4px',
},
({ status, theme }: any) =>
status === 'running'
? {
animation: `${theme.animation.rotate360} 1s linear infinite;`,
}
: {}
);
const RotatingIcons = styled(Icons)(({ theme }) => ({
height: '12px',
width: '12px',
marginRight: '4px',
animation: `${theme.animation.rotate360} 1s linear infinite;`,
}));

const Passes = styled.span<{}>(({ theme }) => ({
const Passes = styled.span(({ theme }) => ({
color: theme.color.positive,
}));

const Violations = styled.span<{}>(({ theme }) => ({
const Violations = styled.span(({ theme }) => ({
color: theme.color.negative,
}));

const Incomplete = styled.span<{}>(({ theme }) => ({
const Incomplete = styled.span(({ theme }) => ({
color: theme.color.warning,
}));

const centeredStyle = {
const Centered = styled.span({
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>
))(centeredStyle);
Loader.displayName = 'Loader';

interface A11YPanelNormalState {
status: 'ready' | 'ran' | 'running';
});

interface InitialState {
status: 'initial';
}

interface ManualState {
status: 'manual';
}

interface RunningState {
status: 'running';
}

interface RanState {
status: 'ran';
passes: Result[];
violations: Result[];
incomplete: Result[];
}

interface ReadyState {
status: 'ready';
passes: Result[];
violations: Result[];
incomplete: Result[];
}

interface A11YPanelErrorState {
interface ErrorState {
status: 'error';
error: unknown;
}

type A11YPanelState = A11YPanelNormalState | A11YPanelErrorState;
type A11YPanelState =
| InitialState
| ManualState
| RunningState
| RanState
| ReadyState
| ErrorState;

interface A11YPanelProps {
active: boolean;
Expand All @@ -82,18 +92,15 @@ interface A11YPanelProps {

export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
state: A11YPanelState = {
status: 'ready',
passes: [],
violations: [],
incomplete: [],
status: 'initial',
};

componentDidMount() {
const { api } = this.props;

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

componentDidUpdate(prevProps: A11YPanelProps) {
Expand All @@ -103,18 +110,18 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
if (!prevProps.active && active) {
// removes all elements from the redux map in store from the previous panel
store.dispatch(clearElements());
this.request();
}
}

componentWillUnmount() {
const { api } = this.props;
api.off(STORY_RENDERED, this.request);
api.off(EVENTS.RESULT, this.onUpdate);

api.off(EVENTS.RESULT, this.onResult);
api.off(EVENTS.ERROR, this.onError);
api.off(EVENTS.MANUAL, this.onManual);
}

onUpdate = ({ passes, violations, incomplete }: AxeResults) => {
onResult = ({ passes, violations, incomplete }: AxeResults) => {
this.setState(
{
status: 'ran',
Expand Down Expand Up @@ -142,9 +149,18 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
});
};

onManual = (manual: boolean) => {
if (manual) {
this.setState({
status: 'manual',
});
} else {
this.request();
}
};

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

if (active) {
this.setState(
{
Expand All @@ -163,43 +179,39 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
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') {
actionTitle = 'Rerun tests';
} else if (status === 'running') {
actionTitle = (
<Fragment>
<Icon inline icon="sync" status={status} /> Running test
</Fragment>
);
} else if (status === 'ran') {
actionTitle = (
<Fragment>
<Icon inline icon="check" /> Tests completed
</Fragment>
);
}

return (
<Fragment>
<Provider store={store}>
{status === 'running' ? (
<Loader />
switch (this.state.status) {
case 'initial':
return <Centered>Initializing...</Centered>;
case 'manual':
return (
<Fragment>
<Centered>Manually run the accessibility scan.</Centered>
<ActionBar
key="actionbar"
actionItems={[{ title: 'Run test', onClick: this.request }]}
/>
</Fragment>
);
case 'running':
return (
<Centered>
<RotatingIcons inline icon="sync" /> Please wait while the accessibility scan is running
...
</Centered>
);
case 'ready':
case 'ran':
const { passes, violations, incomplete, status } = this.state;
const actionTitle =
status === 'ready' ? (
'Rerun tests'
) : (
<Fragment>
<Icons inline icon="check" /> Tests completed
</Fragment>
);
return (
<Provider store={store}>
<ScrollArea vertical horizontal>
<Tabs
key="tabs"
Expand Down Expand Up @@ -243,13 +255,21 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
]}
/>
</ScrollArea>
)}
<ActionBar
key="actionbar"
actionItems={[{ title: actionTitle, onClick: this.request }]}
/>
</Provider>
</Fragment>
);
<ActionBar
key="actionbar"
actionItems={[{ title: actionTitle, onClick: this.request }]}
/>
</Provider>
);
case 'error':
const { error } = this.state;
return (
<Centered>
The accessibility scan encountered an error.
<br />
{error}
</Centered>
);
}
}
}
Loading

0 comments on commit 229229a

Please sign in to comment.