Skip to content

Commit

Permalink
Add discrepancy handling to A11yPanel
Browse files Browse the repository at this point in the history
  • Loading branch information
valentinpalkovic committed Nov 19, 2024
1 parent d3923d2 commit 2eebee1
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 90 deletions.
45 changes: 25 additions & 20 deletions code/addons/a11y/src/components/A11YPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CheckIcon, SyncIcon } from '@storybook/icons';
import { useA11yContext } from './A11yContext';
import { Report } from './Report';
import { Tabs } from './Tabs';
import { TestDiscrepancyMessage } from './TestDiscrepancyMessage';

export enum RuleType {
VIOLATION,
Expand Down Expand Up @@ -43,7 +44,7 @@ const Centered = styled.span({
});

export const A11YPanel: React.FC = () => {
const { results, status, handleManual, error } = useA11yContext();
const { results, status, handleManual, error, discrepancy } = useA11yContext();

const manualActionItems = useMemo(
() => [{ title: 'Run test', onClick: handleManual }],
Expand Down Expand Up @@ -106,31 +107,35 @@ export const A11YPanel: React.FC = () => {

return (
<>
{status === 'initial' && <Centered>Initializing...</Centered>}
{status === 'manual' && (
<>
<Centered>Manually run the accessibility scan.</Centered>
<ActionBar key="actionbar" actionItems={manualActionItems} />
</>
)}
{status === 'running' && (
<Centered>
<RotatingIcon size={12} /> Please wait while the accessibility scan is running ...
</Centered>
)}
{(status === 'ready' || status === 'ran') && (
{discrepancy && <TestDiscrepancyMessage discrepancy={discrepancy} />}
{status === 'ready' || status === 'ran' ? (
<>
<ScrollArea vertical horizontal>
<Tabs key="tabs" tabs={tabs} />
</ScrollArea>
<ActionBar key="actionbar" actionItems={readyActionItems} />
</>
)}
{status === 'error' && (
<Centered>
The accessibility scan encountered an error.
<br />
{typeof error === 'string' ? error : JSON.stringify(error)}
) : (
<Centered style={{ marginTop: discrepancy ? '1em' : 0 }}>
{status === 'initial' && 'Initializing...'}
{status === 'manual' && (
<>
<>Manually run the accessibility scan.</>
<ActionBar key="actionbar" actionItems={manualActionItems} />
</>
)}
{status === 'running' && (
<>
<RotatingIcon size={12} /> Please wait while the accessibility scan is running ...
</>
)}
{status === 'error' && (
<>
The accessibility scan encountered an error.
<br />
{typeof error === 'string' ? error : JSON.stringify(error)}
</>
)}
</Centered>
)}
</>
Expand Down
34 changes: 32 additions & 2 deletions code/addons/a11y/src/components/A11yContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FC, PropsWithChildren } from 'react';
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';

import {
STORY_FINISHED,
Expand All @@ -10,6 +10,7 @@ import {
useAddonState,
useChannel,
useParameter,
useStorybookApi,
useStorybookState,
} from 'storybook/internal/manager-api';
import type { Report } from 'storybook/internal/preview-api';
Expand All @@ -19,9 +20,10 @@ import { HIGHLIGHT } from '@storybook/addon-highlight';

import type { AxeResults, Result } from 'axe-core';

import { ADDON_ID, EVENTS } from '../constants';
import { ADDON_ID, EVENTS, TEST_PROVIDER_ID } from '../constants';
import type { A11yParameters } from '../params';
import type { A11YReport } from '../types';
import type { TestDiscrepancy } from './TestDiscrepancyMessage';

export interface Results {
passes: Result[];
Expand All @@ -40,6 +42,7 @@ export interface A11yContextStore {
setStatus: (status: Status) => void;
error: unknown;
handleManual: () => void;
discrepancy: TestDiscrepancy;
}

const colorsByType = [
Expand All @@ -63,6 +66,7 @@ export const A11yContext = createContext<A11yContextStore>({
status: 'initial',
error: undefined,
handleManual: () => {},
discrepancy: null,
});

const defaultResult = {
Expand All @@ -80,12 +84,15 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {

const getInitialStatus = useCallback((manual = false) => (manual ? 'manual' : 'initial'), []);

const api = useStorybookApi();
const [results, setResults] = useAddonState<Results>(ADDON_ID, defaultResult);
const [tab, setTab] = useState(0);
const [error, setError] = React.useState<unknown>(undefined);
const [status, setStatus] = useState<Status>(getInitialStatus(parameters.manual!));
const [highlighted, setHighlighted] = useState<string[]>([]);

const { storyId } = useStorybookState();
const storyStatus = api.getCurrentStoryStatus();

Check failure on line 95 in code/addons/a11y/src/components/A11yContext.tsx

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/components/A11yContext.test.tsx > A11yContext > should render children

TypeError: api.getCurrentStoryStatus is not a function ❯ A11yContextProvider src/components/A11yContext.tsx:95:27 ❯ renderWithHooks ../../node_modules/react-dom/cjs/react-dom.development.js:16305:18 ❯ mountIndeterminateComponent ../../node_modules/react-dom/cjs/react-dom.development.js:20074:13 ❯ beginWork ../../node_modules/react-dom/cjs/react-dom.development.js:21587:16 ❯ HTMLUnknownElement.callCallback ../../node_modules/react-dom/cjs/react-dom.development.js:4164:14 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/event/EventTarget.ts:222:42 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/nodes/element/Element.ts:1170:29 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/event/EventTarget.ts:131:9 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/nodes/element/Element.ts:1170:29 ❯ Object.invokeGuardedCallbackDev ../../node_modules/react-dom/cjs/react-dom.development.js:4213:16

Check failure on line 95 in code/addons/a11y/src/components/A11yContext.tsx

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/components/A11yContext.test.tsx > A11yContext > should handle STORY_FINISHED event correctly

TypeError: api.getCurrentStoryStatus is not a function ❯ A11yContextProvider src/components/A11yContext.tsx:95:27 ❯ renderWithHooks ../../node_modules/react-dom/cjs/react-dom.development.js:16305:18 ❯ mountIndeterminateComponent ../../node_modules/react-dom/cjs/react-dom.development.js:20074:13 ❯ beginWork ../../node_modules/react-dom/cjs/react-dom.development.js:21587:16 ❯ HTMLUnknownElement.callCallback ../../node_modules/react-dom/cjs/react-dom.development.js:4164:14 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/event/EventTarget.ts:222:42 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/nodes/element/Element.ts:1170:29 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/event/EventTarget.ts:131:9 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/nodes/element/Element.ts:1170:29 ❯ Object.invokeGuardedCallbackDev ../../node_modules/react-dom/cjs/react-dom.development.js:4213:16

Check failure on line 95 in code/addons/a11y/src/components/A11yContext.tsx

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/components/A11yContext.test.tsx > A11yContext > should handle STORY_RENDER_PHASE_CHANGED event correctly

TypeError: api.getCurrentStoryStatus is not a function ❯ A11yContextProvider src/components/A11yContext.tsx:95:27 ❯ renderWithHooks ../../node_modules/react-dom/cjs/react-dom.development.js:16305:18 ❯ mountIndeterminateComponent ../../node_modules/react-dom/cjs/react-dom.development.js:20074:13 ❯ beginWork ../../node_modules/react-dom/cjs/react-dom.development.js:21587:16 ❯ HTMLUnknownElement.callCallback ../../node_modules/react-dom/cjs/react-dom.development.js:4164:14 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/event/EventTarget.ts:222:42 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/nodes/element/Element.ts:1170:29 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/event/EventTarget.ts:131:9 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/nodes/element/Element.ts:1170:29 ❯ Object.invokeGuardedCallbackDev ../../node_modules/react-dom/cjs/react-dom.development.js:4213:16

Check failure on line 95 in code/addons/a11y/src/components/A11yContext.tsx

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/components/A11yContext.test.tsx > A11yContext > should handle STORY_RENDER_PHASE_CHANGED event correctly when in manual mode

TypeError: api.getCurrentStoryStatus is not a function ❯ A11yContextProvider src/components/A11yContext.tsx:95:27 ❯ renderWithHooks ../../node_modules/react-dom/cjs/react-dom.development.js:16305:18 ❯ mountIndeterminateComponent ../../node_modules/react-dom/cjs/react-dom.development.js:20074:13 ❯ beginWork ../../node_modules/react-dom/cjs/react-dom.development.js:21587:16 ❯ HTMLUnknownElement.callCallback ../../node_modules/react-dom/cjs/react-dom.development.js:4164:14 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/event/EventTarget.ts:222:42 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/nodes/element/Element.ts:1170:29 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/event/EventTarget.ts:131:9 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/nodes/element/Element.ts:1170:29 ❯ Object.invokeGuardedCallbackDev ../../node_modules/react-dom/cjs/react-dom.development.js:4213:16

Check failure on line 95 in code/addons/a11y/src/components/A11yContext.tsx

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/components/A11yContext.test.tsx > A11yContext > should handle STORY_FINISHED event with error correctly

TypeError: api.getCurrentStoryStatus is not a function ❯ A11yContextProvider src/components/A11yContext.tsx:95:27 ❯ renderWithHooks ../../node_modules/react-dom/cjs/react-dom.development.js:16305:18 ❯ mountIndeterminateComponent ../../node_modules/react-dom/cjs/react-dom.development.js:20074:13 ❯ beginWork ../../node_modules/react-dom/cjs/react-dom.development.js:21587:16 ❯ HTMLUnknownElement.callCallback ../../node_modules/react-dom/cjs/react-dom.development.js:4164:14 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/event/EventTarget.ts:222:42 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/nodes/element/Element.ts:1170:29 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/event/EventTarget.ts:131:9 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/nodes/element/Element.ts:1170:29 ❯ Object.invokeGuardedCallbackDev ../../node_modules/react-dom/cjs/react-dom.development.js:4213:16

Check failure on line 95 in code/addons/a11y/src/components/A11yContext.tsx

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/components/A11yContext.test.tsx > A11yContext > should handle manual run correctly

TypeError: api.getCurrentStoryStatus is not a function ❯ A11yContextProvider src/components/A11yContext.tsx:95:27 ❯ renderWithHooks ../../node_modules/react-dom/cjs/react-dom.development.js:16305:18 ❯ mountIndeterminateComponent ../../node_modules/react-dom/cjs/react-dom.development.js:20074:13 ❯ beginWork ../../node_modules/react-dom/cjs/react-dom.development.js:21587:16 ❯ HTMLUnknownElement.callCallback ../../node_modules/react-dom/cjs/react-dom.development.js:4164:14 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/event/EventTarget.ts:222:42 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/nodes/element/Element.ts:1170:29 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/event/EventTarget.ts:131:9 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/nodes/element/Element.ts:1170:29 ❯ Object.invokeGuardedCallbackDev ../../node_modules/react-dom/cjs/react-dom.development.js:4213:16

Check failure on line 95 in code/addons/a11y/src/components/A11yContext.tsx

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/components/A11yContext.test.tsx > A11yContext > should toggle highlight correctly

TypeError: api.getCurrentStoryStatus is not a function ❯ A11yContextProvider src/components/A11yContext.tsx:95:27 ❯ renderWithHooks ../../node_modules/react-dom/cjs/react-dom.development.js:16305:18 ❯ mountIndeterminateComponent ../../node_modules/react-dom/cjs/react-dom.development.js:20074:13 ❯ beginWork ../../node_modules/react-dom/cjs/react-dom.development.js:21587:16 ❯ HTMLUnknownElement.callCallback ../../node_modules/react-dom/cjs/react-dom.development.js:4164:14 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/event/EventTarget.ts:222:42 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/nodes/element/Element.ts:1170:29 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/event/EventTarget.ts:131:9 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/nodes/element/Element.ts:1170:29 ❯ Object.invokeGuardedCallbackDev ../../node_modules/react-dom/cjs/react-dom.development.js:4213:16

Check failure on line 95 in code/addons/a11y/src/components/A11yContext.tsx

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/components/A11yContext.test.tsx > A11yContext > should clear highlights correctly

TypeError: api.getCurrentStoryStatus is not a function ❯ A11yContextProvider src/components/A11yContext.tsx:95:27 ❯ renderWithHooks ../../node_modules/react-dom/cjs/react-dom.development.js:16305:18 ❯ mountIndeterminateComponent ../../node_modules/react-dom/cjs/react-dom.development.js:20074:13 ❯ beginWork ../../node_modules/react-dom/cjs/react-dom.development.js:21587:16 ❯ HTMLUnknownElement.callCallback ../../node_modules/react-dom/cjs/react-dom.development.js:4164:14 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/event/EventTarget.ts:222:42 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/nodes/element/Element.ts:1170:29 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/event/EventTarget.ts:131:9 ❯ HTMLUnknownElement.dispatchEvent ../../node_modules/happy-dom/src/nodes/element/Element.ts:1170:29 ❯ Object.invokeGuardedCallbackDev ../../node_modules/react-dom/cjs/react-dom.development.js:4213:16

const handleToggleHighlight = useCallback((target: string[], highlight: boolean) => {
setHighlighted((prevHighlighted) =>
Expand Down Expand Up @@ -178,6 +185,28 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
emit(HIGHLIGHT, { elements: highlighted, color: colorsByType[tab] });
}, [emit, highlighted, tab]);

const discrepancy: TestDiscrepancy = useMemo(() => {
const storyStatusA11y = storyStatus[TEST_PROVIDER_ID]?.status;

if (storyStatusA11y) {
if (storyStatusA11y === 'success' && results.violations.length > 0) {
return 'cliPassedBrowserFailed';
}

if (storyStatusA11y === 'error' && results.violations.length === 0) {
if (status === 'ready' || status === 'ran') {
return 'browserPassedCliFailed';
}

if (status === 'manual') {
return 'cliFailedButModeManual';
}
}
}

return null;
}, [results.violations.length, status, storyStatus]);

return (
<A11yContext.Provider
value={{
Expand All @@ -191,6 +220,7 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
setStatus,
error,
handleManual,
discrepancy,
}}
{...props}
/>
Expand Down
73 changes: 73 additions & 0 deletions code/addons/a11y/src/components/TestDiscrepancyMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { useMemo } from 'react';

import { Link } from 'storybook/internal/components';
import { useStorybookApi } from 'storybook/internal/manager-api';
import { styled } from 'storybook/internal/theming';

import { DOCUMENTATION_DISCREPANCY_LINK } from '../constants';

const Wrapper = styled.div(({ theme: { color, typography, background } }) => ({
textAlign: 'start',
padding: '11px 15px',
fontSize: `${typography.size.s2}px`,
fontWeight: typography.weight.regular,
lineHeight: '1rem',
background: background.app,
borderBottom: `1px solid ${color.border}`,
color: color.defaultText,
backgroundClip: 'padding-box',
position: 'relative',
code: {
fontSize: `${typography.size.s1 - 1}px`,
color: 'inherit',
margin: '0 0.2em',
padding: '0 0.2em',
background: 'rgba(255, 255, 255, 0.8)',
borderRadius: '2px',
boxShadow: '0 0 0 1px rgba(0, 0, 0, 0.1)',
},
}));

export type TestDiscrepancy =
| 'browserPassedCliFailed'
| 'cliPassedBrowserFailed'
| 'cliFailedButModeManual'
| null;

interface TestDiscrepancyMessageProps {
discrepancy: TestDiscrepancy;
}
export const TestDiscrepancyMessage = ({ discrepancy }: TestDiscrepancyMessageProps) => {
const api = useStorybookApi();
const docsUrl = api.getDocsUrl({
subpath: DOCUMENTATION_DISCREPANCY_LINK,
versioned: true,
renderer: true,
});

const message = useMemo(() => {
switch (discrepancy) {
case 'browserPassedCliFailed':
return 'Accessibility checks passed in this browser but failed in the CLI.';
case 'cliPassedBrowserFailed':
return 'Accessibility checks passed in the CLI but failed in this browser.';
case 'cliFailedButModeManual':
return 'Accessibility checks failed in the CLI. Run the tests manually to see the results.';
default:
return null;
}
}, [discrepancy]);

if (!message) {
return null;
}

return (
<Wrapper>
{message}{' '}
<Link href={docsUrl} target="_blank" withArrow>
Learn what could cause this
</Link>
</Wrapper>
);
};
5 changes: 5 additions & 0 deletions code/addons/a11y/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@ const RUNNING = `${ADDON_ID}/running`;
const ERROR = `${ADDON_ID}/error`;
const MANUAL = `${ADDON_ID}/manual`;

export const DOCUMENTATION_LINK = 'writing-tests/accessibility-testing';
export const DOCUMENTATION_DISCREPANCY_LINK = `${DOCUMENTATION_LINK}#what-happens-when-there-are-different-results-in-multiple-environments`;

export const TEST_PROVIDER_ID = 'storybook/addon-a11y/test-provider';

export const EVENTS = { RESULT, REQUEST, RUNNING, ERROR, MANUAL };
Loading

0 comments on commit 2eebee1

Please sign in to comment.