文档
Storybook 文档

无障碍测试

观看视频教程

无障碍是指让网站对所有人都有所包含。这意味着要支持以下要求:键盘导航、屏幕阅读器支持、触控友好、可用的颜色对比度、减少运动以及缩放支持。

无障碍测试会根据 WCAG 规则和其他业界认可的最佳实践,对渲染后的 DOM 进行审核。它们充当 QA 的第一道防线,以捕捉明显的无障碍违规行为。

使用 a11y 插件进行无障碍检查

Storybook 提供了一个官方的 a11y 插件。由 Deque 的 axe-core 提供支持,它可以自动捕捉到高达 57% 的 WCAG 问题

设置 a11y 插件

如果你想使用 插件 检查故事的无障碍性,你需要将它添加到 Storybook。你可以通过运行以下命令来实现:

npx storybook add @storybook/addon-a11y

CLI 的 add 命令会自动执行插件的安装和设置。要手动安装它,请参阅我们的 文档,了解如何安装插件。

启动 Storybook,你会看到 UI 中有一些明显的变化。一个新的工具栏图标和无障碍面板,你可以在其中检查测试结果。

Storybook accessibility addon running

工作原理

Storybook 的 a11y 插件在选定的故事上运行 Axe。允许你在开发过程中捕捉和修复无障碍性问题。例如,如果你正在开发一个按钮组件并包含以下故事集:

Button.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { Button } from './Button';
 
const meta: Meta<typeof Button> = {
  component: Button,
  argTypes: {
    backgroundColor: { control: 'color' },
  },
};
 
export default meta;
type Story = StoryObj<typeof Button>;
 
// This is an accessible story
export const Accessible: Story = {
  args: {
    primary: false,
    label: 'Button',
  },
};
 
// This is not
export const Inaccessible: Story = {
  args: {
    ...Accessible.args,
    backgroundColor: 'red',
  },
};

循环遍历这两个故事,你会发现 Inaccessible 故事包含一些需要修复的问题。打开无障碍面板中的违规选项卡,它会提供对无障碍性问题的清晰描述以及解决方法的指南。

Storybook accessibility addon running

配置

Storybook 的无障碍性插件开箱即用,包含了一组涵盖大多数问题的无障碍规则。你还可以微调 插件配置 或覆盖 Axe 的规则集,以最适合你的需求。

全局 a11y 配置

如果你需要忽略无障碍规则或修改它在所有故事中的设置,可以在你的 storybook/preview.js|ts 中添加以下内容:

.storybook/preview.ts
// Replace your-framework with the framework you are using (e.g., react, vue3)
import { Preview } from '@storybook/your-framework';
 
const preview: Preview = {
  parameters: {
    a11y: {
      // Optional selector to inspect
      element: '#storybook-root',
      config: {
        rules: [
          {
            // The autocomplete rule will not run based on the CSS selector provided
            id: 'autocomplete-valid',
            selector: '*:not([autocomplete="nope"])',
          },
          {
            // Setting the enabled option to false will disable checks for this particular rule on all stories.
            id: 'image-alt',
            enabled: false,
          },
        ],
      },
      // Axe's options parameter
      options: {},
      // Optional flag to prevent the automatic check
      manual: true,
    },
  },
};
 
export default preview;

组件级 a11y 配置

你也可以为组件的所有故事自定义一组规则。更新故事的默认导出并添加一个包含所需配置的参数

MyComponent.stories.ts|tsx
// Replace your-framework with the name of your framework
import type { Meta } from '@storybook/your-framework';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
  parameters: {
    a11y: {
      // Optional selector to inspect
      element: '#storybook-root',
      config: {
        rules: [
          {
            // The autocomplete rule will not run based on the CSS selector provided
            id: 'autocomplete-valid',
            selector: '*:not([autocomplete="nope"])',
          },
          {
            // Setting the enabled option to false will disable checks for this particular rule on all stories.
            id: 'image-alt',
            enabled: false,
          },
        ],
      },
      options: {},
      manual: true,
    },
  },
};
 
export default meta;

故事级 a11y 配置

通过更新你的故事以包含一个新的参数,在故事级别自定义 a11y 规则集

MyComponent.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
export const ExampleStory: Story = {
  parameters: {
    a11y: {
      element: '#storybook-root',
      config: {
        rules: [
          {
            // The autocomplete rule will not run based on the CSS selector provided
            id: 'autocomplete-valid',
            selector: '*:not([autocomplete="nope"])',
          },
          {
            // Setting the enabled option to false will disable checks for this particular rule on all stories.
            id: 'image-alt',
            enabled: false,
          },
        ],
      },
      options: {},
      manual: true,
    },
  },
};

如何禁用 a11y 测试

通过分别在故事的导出或组件的默认导出中添加以下参数,为故事或组件禁用可访问性测试

MyComponent.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
export const NonA11yStory: Story = {
  parameters: {
    a11y: {
      // This option disables all a11y checks on this story
      disable: true,
    },
  },
};

使用测试运行器自动执行可访问性测试

检查可访问性的最准确方法是在真实设备上手动进行。但是,你可以使用自动化工具来捕获常见可访问性问题。例如,Axe 平均可以自动捕获高达 57% 的 WCAG 问题

这些工具通过根据 WCAG 规则和其他行业公认的最佳实践审计渲染的 DOM 来工作。然后,你可以使用 Storybook 的 测试运行器axe-playwright 将这些工具集成到你的测试自动化管道中。

设置

要使用测试运行器启用可访问性测试,你需要采取额外的步骤来正确设置它。我们建议你在继续进行其他必需的配置之前,先查看 测试运行器文档

运行以下命令来安装所需的依赖项。

npm install axe-playwright --save-dev

在你的 Storybook 目录中添加一个新的 配置文件,其中包含以下内容

.storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';
import { injectAxe, checkA11y } from 'axe-playwright';
 
/*
 * See https://storybook.org.cn/docs/writing-tests/test-runner#test-hook-api
 * to learn more about the test-runner hooks API.
 */
const config: TestRunnerConfig = {
  async preVisit(page) {
    await injectAxe(page);
  },
  async postVisit(page) {
    await checkA11y(page, '#storybook-root', {
      detailedReport: true,
      detailedReportOptions: {
        html: true,
      },
    });
  },
};
 
export default config;

preVisitpostVisit 是方便的钩子,允许你扩展测试运行器的默认配置。在这里了解有关它们的更多信息 here

当你执行测试运行器(例如,使用 yarn test-storybook)时,它将运行可访问性审计和可能为每个组件故事配置的任何 组件测试

它从故事的根元素开始遍历 DOM 树,开始检查问题,并根据遇到的问题生成一份详细的报告。

Accessibility testing with the test runner

使用测试运行器的 A11y 配置

测试运行器提供 辅助方法,允许访问故事的信息。你可以使用它们来扩展测试运行器的配置,并提供你可能为特定故事提供的其他选项。例如

.storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';
import { getStoryContext } from '@storybook/test-runner';
 
import { injectAxe, checkA11y, configureAxe } from 'axe-playwright';
 
/*
 * See https://storybook.org.cn/docs/writing-tests/test-runner#test-hook-api
 * to learn more about the test-runner hooks API.
 */
const config: TestRunnerConfig = {
  async preVisit(page) {
    await injectAxe(page);
  },
  async postVisit(page, context) {
    // Get the entire context of a story, including parameters, args, argTypes, etc.
    const storyContext = await getStoryContext(page, context);
 
    // Apply story-level a11y rules
    await configureAxe(page, {
      rules: storyContext.parameters?.a11y?.config?.rules,
    });
 
    const element = storyContext.parameters?.a11y?.element ?? '#storybook-root';
    await checkA11y(page, element, {
      detailedReport: true,
      detailedReportOptions: {
        html: true,
      },
    });
  },
};
 
export default config;

使用测试运行器禁用 a11y 测试

此外,如果你已经 禁用 任何特定故事的可访问性测试,你也可以配置测试运行器以避免测试它。例如

.storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';
import { getStoryContext } from '@storybook/test-runner';
 
import { injectAxe, checkA11y } from 'axe-playwright';
 
/*
 * See https://storybook.org.cn/docs/writing-tests/test-runner#test-hook-api
 * to learn more about the test-runner hooks API.
 */
const config: TestRunnerConfig = {
  async preVisit(page) {
    await injectAxe(page);
  },
  async postVisit(page, context) {
    // Get the entire context of a story, including parameters, args, argTypes, etc.
    const storyContext = await getStoryContext(page, context);
 
    // Do not run a11y tests on disabled stories.
    if (storyContext.parameters?.a11y?.disable) {
      return;
    }
    await checkA11y(page, '#storybook-root', {
      detailedReport: true,
      detailedReportOptions: {
        html: true,
      },
    });
  },
};
 
export default config;

基于浏览器的可访问性测试和基于代码检查器的可访问性测试有什么区别?

基于浏览器的可访问性测试(如 Storybook 中的测试)会评估渲染的 DOM,因为这可以提供最高的准确性。审计尚未编译的代码比实际情况多了一步,因此你不会捕获用户可能遇到的所有问题。

了解其他 UI 测试