From 229229aa877e74561e8cd970ca8b4369e2462edf Mon Sep 17 00:00:00 2001 From: Donald Pipowitch Date: Tue, 19 Nov 2019 10:10:10 +0100 Subject: [PATCH] addon-a11y: allow manual run --- addons/a11y/README.md | 3 +- addons/a11y/src/components/A11YPanel.test.js | 45 ++-- addons/a11y/src/components/A11YPanel.tsx | 194 ++++++++++-------- .../__snapshots__/A11YPanel.test.js.snap | 145 +++++++++++++ addons/a11y/src/constants.ts | 3 +- addons/a11y/src/index.ts | 8 +- lib/components/src/icon/icon.tsx | 4 +- 7 files changed, 293 insertions(+), 109 deletions(-) diff --git a/addons/a11y/README.md b/addons/a11y/README.md index d4d2638a9e6c..ec9d59c3a475 100755 --- a/addons/a11y/README.md +++ b/addons/a11y/README.md @@ -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 }, }); diff --git a/addons/a11y/src/components/A11YPanel.test.js b/addons/a11y/src/components/A11YPanel.test.js index 2f84b31afd39..9947bcc52f90 100644 --- a/addons/a11y/src/components/A11YPanel.test.js +++ b/addons/a11y/src/components/A11YPanel.test.js @@ -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'; @@ -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(); @@ -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(); @@ -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(); @@ -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(); + 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', () => { @@ -141,7 +154,7 @@ describe('A11YPanel', () => { // given const api = createApi(); const wrapper = mount(); - 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 @@ -170,7 +183,7 @@ describe('A11YPanel', () => { // given const api = createApi(); mount(); - 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 @@ -197,7 +210,7 @@ describe('A11YPanel', () => { // given const api = createApi(); const wrapper = mount(); - 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(); diff --git a/addons/a11y/src/components/A11YPanel.tsx b/addons/a11y/src/components/A11YPanel.tsx index 194acca7be6d..89d916a71f57 100644 --- a/addons/a11y/src/components/A11YPanel.tsx +++ b/addons/a11y/src/components/A11YPanel.tsx @@ -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'; @@ -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 }) => ( -
- Please wait while the accessibility scan is running - ... -
-))(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; @@ -82,18 +92,15 @@ interface A11YPanelProps { export class A11YPanel extends Component { 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) { @@ -103,18 +110,18 @@ export class A11YPanel extends Component { 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', @@ -142,9 +149,18 @@ export class A11YPanel extends Component { }); }; + onManual = (manual: boolean) => { + if (manual) { + this.setState({ + status: 'manual', + }); + } else { + this.request(); + } + }; + request = () => { const { api, active } = this.props; - if (active) { this.setState( { @@ -163,43 +179,39 @@ export class A11YPanel extends Component { 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') { - actionTitle = 'Rerun tests'; - } else if (status === 'running') { - actionTitle = ( - - Running test - - ); - } else if (status === 'ran') { - actionTitle = ( - - Tests completed - - ); - } - - return ( - - - {status === 'running' ? ( - + switch (this.state.status) { + case 'initial': + return Initializing...; + case 'manual': + return ( + + Manually run the accessibility scan. + + + ); + case 'running': + return ( + + Please wait while the accessibility scan is running + ... + + ); + case 'ready': + case 'ran': + const { passes, violations, incomplete, status } = this.state; + const actionTitle = + status === 'ready' ? ( + 'Rerun tests' ) : ( + + Tests completed + + ); + return ( + { ]} /> - )} - - - - ); + + + ); + case 'error': + const { error } = this.state; + return ( + + The accessibility scan encountered an error. +
+ {error} +
+ ); + } } } diff --git a/addons/a11y/src/components/__snapshots__/A11YPanel.test.js.snap b/addons/a11y/src/components/__snapshots__/A11YPanel.test.js.snap index 2ac873d47536..0ab1b7ebe993 100644 --- a/addons/a11y/src/components/__snapshots__/A11YPanel.test.js.snap +++ b/addons/a11y/src/components/__snapshots__/A11YPanel.test.js.snap @@ -1118,3 +1118,148 @@ exports[`A11YPanel should render report 1`] = ` `; + +exports[`A11YPanel should show initial state on tab activation 1`] = ` +.emotion-0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + height: 100%; +} + + + + + Initializing... + + + +`; + +exports[`A11YPanel should show manual state depending on config 1`] = ` +.emotion-0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + height: 100%; +} + + + + + Initializing... + + + +`; diff --git a/addons/a11y/src/constants.ts b/addons/a11y/src/constants.ts index efabac7a268b..79ef7446a159 100755 --- a/addons/a11y/src/constants.ts +++ b/addons/a11y/src/constants.ts @@ -7,5 +7,6 @@ export const CLEAR_ELEMENTS = 'CLEAR_ELEMENTS'; const RESULT = `${ADDON_ID}/result`; const REQUEST = `${ADDON_ID}/request`; const ERROR = `${ADDON_ID}/error`; +const MANUAL = `${ADDON_ID}/manual`; -export const EVENTS = { RESULT, REQUEST, ERROR }; +export const EVENTS = { RESULT, REQUEST, ERROR, MANUAL }; diff --git a/addons/a11y/src/index.ts b/addons/a11y/src/index.ts index a7f53ec29a73..ef6b7d546d61 100644 --- a/addons/a11y/src/index.ts +++ b/addons/a11y/src/index.ts @@ -11,8 +11,10 @@ interface Setup { element?: ElementContext; config: Spec; options: RunOptions; + manual: boolean; } -let setup: Setup = { element: null, config: {}, options: {} }; + +let setup: Setup = { element: null, config: {}, options: {}, manual: false }; const getElement = () => { const storyRoot = document.getElementById('story-root'); @@ -58,12 +60,14 @@ export const withA11y = makeDecorator({ if (storedDefaultSetup === null) { storedDefaultSetup = { ...setup }; } - Object.assign(setup, parameters as Setup); + Object.assign(setup, parameters as Partial); } else if (storedDefaultSetup !== null) { Object.assign(setup, storedDefaultSetup); storedDefaultSetup = null; } + addons.getChannel().on(EVENTS.REQUEST, () => run(setup.element, setup.config, setup.options)); + addons.getChannel().emit(EVENTS.MANUAL, setup.manual); return getStory(context); }, diff --git a/lib/components/src/icon/icon.tsx b/lib/components/src/icon/icon.tsx index fc8f1b1a0202..e421548de521 100644 --- a/lib/components/src/icon/icon.tsx +++ b/lib/components/src/icon/icon.tsx @@ -2,13 +2,13 @@ import React, { FunctionComponent } from 'react'; import { styled } from '@storybook/theming'; import icons, { IconKey } from './icons'; -import Svg from './svg'; +import Svg, { SvgProps } from './svg'; const Path = styled.path({ fill: 'currentColor', }); -export interface IconsProps { +export interface IconsProps extends SvgProps { icon: IconKey; }