Skip to content

Commit

Permalink
Merge pull request #30162 from storybookjs/valentin/a11y-refactorings
Browse files Browse the repository at this point in the history
A11y Addon: Adjust default behaviour when using with experimental-addon-test
  • Loading branch information
valentinpalkovic authored Jan 13, 2025
2 parents 909605c + 5a33146 commit 66facc9
Show file tree
Hide file tree
Showing 18 changed files with 1,268 additions and 158 deletions.
2 changes: 1 addition & 1 deletion code/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,4 @@ export const parameters = {
},
};

export const tags = ['test', 'vitest', '!a11ytest'];
export const tags = ['test', 'vitest'];
2 changes: 1 addition & 1 deletion code/addons/a11y/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ export const TEST_PROVIDER_ID = 'storybook/addon-a11y/test-provider';

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

export const A11Y_TEST_TAG = 'a11ytest';
export const A11Y_TEST_TAG = 'a11y-test';
4 changes: 3 additions & 1 deletion code/addons/a11y/src/postinstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ const $ = execa({
});

export default async function postinstall(options: PostinstallOptions) {
await $`storybook automigrate addonA11yAddonTest ${options.yes ? '--yes' : ''}`;
await $({
stdio: 'inherit',
})`storybook automigrate addonA11yAddonTest ${options.yes ? '--yes' : ''}`;
}
2 changes: 1 addition & 1 deletion code/addons/a11y/src/preview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ describe('afterEach', () => {
});
});

it('should run accessibility checks if "a11ytest" flag is not available and is not running in Vitest', async () => {
it('should run accessibility checks if "a11y-test" flag is not available and is not running in Vitest', async () => {
const context = createContext({
tags: [],
});
Expand Down
3 changes: 0 additions & 3 deletions code/addons/a11y/src/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,3 @@ export const initialGlobals = {
manual: false,
},
};

// A11Y_TEST_TAG constant in ./constants.ts. Has to be statically analyzable.
export const tags = ['a11ytest'];
60 changes: 48 additions & 12 deletions code/addons/test/src/components/TestProviderRender.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import React, { type ComponentProps, type FC, useCallback, useMemo, useRef, useState } from 'react';
import React, {
type ComponentProps,
type FC,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';

import {
Button,
Expand All @@ -12,10 +20,11 @@ import {
type TestProviderConfig,
type TestProviderState,
} from 'storybook/internal/core-events';
import { addons } from 'storybook/internal/manager-api';
import { addons, useStorybookState } from 'storybook/internal/manager-api';
import type { API } from 'storybook/internal/manager-api';
import { styled, useTheme } from 'storybook/internal/theming';

import type { Tag } from '@storybook/csf';
import {
AccessibilityIcon,
EditIcon,
Expand Down Expand Up @@ -106,23 +115,38 @@ const statusMap: Record<TestStatus, ComponentProps<typeof TestStatusIcon>['statu
pending: 'pending',
};

export const TestProviderRender: FC<
{
api: API;
state: TestProviderConfig & TestProviderState<Details, Config>;
entryId?: string;
} & ComponentProps<typeof Container>
> = ({ state, api, entryId, ...props }) => {
type TestProviderRenderProps = {
api: API;
state: TestProviderConfig & TestProviderState<Details, Config>;
entryId?: string;
} & ComponentProps<typeof Container>;

export const TestProviderRender: FC<TestProviderRenderProps> = ({
state,
api,
entryId,
...props
}) => {
const [isEditing, setIsEditing] = useState(false);
const theme = useTheme();
const coverageSummary = state.details?.coverageSummary;
const storybookState = useStorybookState();

const isA11yAddon = addons.experimental_getRegisteredAddons().includes(A11Y_ADDON_ID);

const isA11yAddonInitiallyChecked = useMemo(() => {
const internalIndex = storybookState.internal_index;
if (!internalIndex || !isA11yAddon) {
return false;
}

return Object.values(internalIndex.entries).some((entry) => entry.tags?.includes('a11y-test'));
}, [isA11yAddon, storybookState.internal_index]);

const [config, updateConfig] = useConfig(
api,
state.id,
state.config || { a11y: false, coverage: false }
state.config || { a11y: isA11yAddonInitiallyChecked, coverage: false }
);

const isStoryEntry = entryId?.includes('--') ?? false;
Expand Down Expand Up @@ -425,14 +449,22 @@ export const TestProviderRender: FC<
};

function useConfig(api: API, providerId: string, initialConfig: Config) {
const updateTestProviderState = useCallback(
(config: Config) => {
api.updateTestProviderState(providerId, { config });
api.emit(TESTING_MODULE_CONFIG_CHANGE, { providerId, config });
},
[api, providerId]
);

const [currentConfig, setConfig] = useState<Config>(initialConfig);

const lastConfig = useRef(initialConfig);

const saveConfig = useCallback(
debounce((config: Config) => {
if (!isEqual(config, lastConfig.current)) {
api.updateTestProviderState(providerId, { config });
api.emit(TESTING_MODULE_CONFIG_CHANGE, { providerId, config });
updateTestProviderState(config);
lastConfig.current = config;
}
}, 500),
Expand All @@ -450,5 +482,9 @@ function useConfig(api: API, providerId: string, initialConfig: Config) {
[saveConfig]
);

useEffect(() => {
updateTestProviderState(initialConfig);
}, []);

return [currentConfig, updateConfig] as const;
}
28 changes: 21 additions & 7 deletions code/addons/test/src/postinstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,20 +325,13 @@ export default async function postInstall(options: PostinstallOptions) {
existsSync
);

const a11yAddon = info.addons.find((addon) => addon.includes(addonA11yName));

const imports = [
`import { beforeAll } from 'vitest';`,
`import { setProjectAnnotations } from '${annotationsImport}';`,
];

const projectAnnotations = [];

if (a11yAddon) {
imports.push(`import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview';`);
projectAnnotations.push('a11yAddonAnnotations');
}

if (previewExists) {
imports.push(`import * as projectAnnotations from './preview';`);
projectAnnotations.push('projectAnnotations');
Expand All @@ -357,6 +350,27 @@ export default async function postInstall(options: PostinstallOptions) {
`
);

const a11yAddon = info.addons.find((addon) => addon.includes(addonA11yName));

if (a11yAddon) {
try {
logger.plain(`${step} Setting up ${addonA11yName} for @storybook/experimental-addon-test:`);
await $({
stdio: 'inherit',
})`storybook automigrate addonA11yAddonTest ${options.yes ? '--yes' : ''}`;
} catch (e) {
printError(
'🚨 Oh no!',
dedent`
We have detected that you have ${addonA11yName} installed but could not automatically set it up for @storybook/experimental-addon-test.
Please refer to the documentation to complete the setup manually:
${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/accessibility-testing#test-addon-integration`)}
`
);
}
}

// Check for existing Vitest workspace. We can't extend it so manual setup is required.
const vitestWorkspaceFile = await findFile('vitest.workspace');
if (vitestWorkspaceFile) {
Expand Down
13 changes: 11 additions & 2 deletions code/addons/test/src/vitest-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,16 +216,25 @@ export const storybookTest = async (options?: UserOptions): Promise<Plugin[]> =>
getInitialGlobals: () => {
const envConfig = JSON.parse(process.env.VITEST_STORYBOOK_CONFIG ?? '{}');

const isA11yEnabled = process.env.VITEST_STORYBOOK
const shouldRunA11yTests = process.env.VITEST_STORYBOOK
? (envConfig.a11y ?? false)
: true;

return {
a11y: {
manual: !isA11yEnabled,
manual: !shouldRunA11yTests,
},
};
},
getTags: () => {
const envConfig = JSON.parse(process.env.VITEST_STORYBOOK_CONFIG ?? '{}');

const shouldSetTag = process.env.VITEST_STORYBOOK
? (envConfig.a11y ?? false)
: false;

return shouldSetTag ? ['a11y-test'] : [];
},
},
// if there is a test.browser config AND test.browser.screenshotFailures is not explicitly set, we set it to false
...(inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test?.browser &&
Expand Down
5 changes: 3 additions & 2 deletions code/addons/test/src/vitest-plugin/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import { setViewport } from './viewports';
declare module '@vitest/browser/context' {
interface BrowserCommands {
getInitialGlobals: () => Promise<Record<string, any>>;
getTags: () => Promise<string[] | undefined>;
}
}

const { getInitialGlobals } = server.commands;
const { getInitialGlobals, getTags } = server.commands;

export const testStory = (
exportName: string,
Expand All @@ -28,7 +29,7 @@ export const testStory = (
const composedStory = composeStory(
story,
meta,
{ initialGlobals: (await getInitialGlobals?.()) ?? {} },
{ initialGlobals: (await getInitialGlobals?.()) ?? {}, tags: await getTags?.() },
undefined,
exportName
);
Expand Down
4 changes: 2 additions & 2 deletions code/core/template/stories/tags-add.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ export const Inheritance = {
tags: ['story-one', '!vitest'],
play: async ({ canvasElement, tags }: PlayFunctionContext<any>) => {
const canvas = within(canvasElement);
if (tags.includes('a11ytest')) {
if (tags.includes('a11y-test')) {
await expect(JSON.parse(canvas.getByTestId('pre').innerText)).toEqual({
tags: ['a11ytest', 'story-one'],
tags: ['a11y-test', 'story-one'],
});
} else {
await expect(JSON.parse(canvas.getByTestId('pre').innerText)).toEqual({
Expand Down
4 changes: 2 additions & 2 deletions code/core/template/stories/tags-config.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ export const Inheritance = {
tags: ['story-one', '!vitest'],
play: async ({ canvasElement, tags }: PlayFunctionContext<any>) => {
const canvas = within(canvasElement);
if (tags.includes('a11ytest')) {
if (tags.includes('a11y-test')) {
await expect(JSON.parse(canvas.getByTestId('pre').innerText)).toEqual({
tags: [
'dev',
'test',
'a11ytest',
'a11y-test',
'component-one',
'component-two',
'autodocs',
Expand Down
4 changes: 2 additions & 2 deletions code/core/template/stories/tags-remove.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ export const Inheritance = {
tags: ['story-one', '!vitest'],
play: async ({ canvasElement, tags }: PlayFunctionContext<any>) => {
const canvas = within(canvasElement);
if (tags.includes('a11ytest')) {
if (tags.includes('a11y-test')) {
await expect(JSON.parse(canvas.getByTestId('pre').innerText)).toEqual({
tags: ['dev', 'test', 'a11ytest', 'component-one', 'autodocs', 'story-one'],
tags: ['dev', 'test', 'a11y-test', 'component-one', 'autodocs', 'story-one'],
});
} else {
await expect(JSON.parse(canvas.getByTestId('pre').innerText)).toEqual({
Expand Down
Loading

0 comments on commit 66facc9

Please sign in to comment.