返回博客

使用 Storybook 进行无障碍功能测试

通过集成工具快速反馈

loading
Varun Vachhar
@winkerVSbecks
最后更新

美国 26% 的成年人至少患有一种残疾。 当您提高可访问性时,它会对您当前和未来的客户产生巨大的影响。 这也是法律要求。

检查可访问性最准确的方法是在真实设备上手动检查。 但这需要专门的专业知识和大量时间。 这两者在前端团队中都很稀缺。

Twilio、Adobe 和 Shopify 的团队结合使用自动化和手动测试。 自动化可以轻松捕获常见的可访问性问题。 手动 QA 保留用于需要人工关注的棘手问题。

有很多资源深入探讨了可访问性原则,因此我们在此不再赘述。 本文向您展示如何使用 Storybook 自动化可访问性测试。 这是一种务实的方法,可以查找和修复您可能遇到的大多数问题。

为什么要自动化?

在我们开始之前,让我们检查一下常见的残疾类型:视觉、听觉、行动、认知、言语和神经系统。 这些用户残疾产生了如下应用程序需求:

  • ⌨ 键盘导航
  • 🗣 屏幕阅读器支持
  • 👆 触摸友好
  • 🎨 足够高的颜色对比度
  • ⚡️ 减少运动
  • 🔍 缩放

过去,您需要通过检查各种浏览器、设备和屏幕阅读器上的每个组件来验证这些要求。 但手动执行是不切实际的,因为应用程序有数十个组件,并且 UI 不断更新。

自动化加速您的工作流程

自动化工具根据 WCAG 规则和其他行业公认的最佳实践,审核渲染的 DOM 是否符合一组启发式规则。 它们充当 QA 的第一道防线,以捕获明显的无障碍功能违规行为。

例如,Axe 平均可以自动发现 57% 的 WCAG 问题。 这使团队能够将其专家资源集中在需要人工审查的更复杂的问题上。

许多团队使用 Axe 库,因为它与大多数现有测试环境集成。 例如,Twilio Paste 团队使用 jest-axe 集成。 而 Shopify Polaris 和 Adobe Spectrum 团队使用 Storybook 插件 版本。

通过在整个开发过程中运行这些检查,您可以缩短反馈循环并更快地修复问题。 工作流程如下所示

  1. 👨🏽‍💻 开发期间: 使用 Storybook 一次专注于一个组件。 使用 A11y 插件模拟视觉缺陷并在组件级别运行可访问性审核。
  2. 对于 QA: 将 Axe 审核集成到您的功能测试管道中。 对所有组件运行检查以捕获回归。

教程

让我们看看实际的工作流程。 我们将使用我在之前的文章中介绍的 Taskbox 应用程序。 获取代码并继续操作。

安装 accessibility 插件

Storybook 的 Accessibility 在活动故事上运行 Axe。 它在面板中可视化测试结果,并概述所有存在违规的 DOM 节点。

要安装插件,请运行:yarn add -D @storybook/addon-a11y。 然后,将 '@storybook/addon-a11y' 添加到 .storybook/main.js 中的 addons 数组中

// .storybook/main.js
const path = require('path');
 
const toPath = (_path) => path.join(process.cwd(), _path);
 
module.exports = {
 stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
 addons: [
   '@storybook/addon-links',
   '@storybook/addon-essentials',
   '@storybook/preset-create-react-app',
   '@storybook/addon-a11y',
 ],
 webpackFinal: async (config) => {...},
};

在编码时测试可访问性

之前博客文章中,我们学习了如何隔离组件并捕获其所有用例作为故事。 例如,以下是 Task 组件的所有故事。 在开发阶段,您可以循环浏览每个故事以验证组件的外观并发现任何可访问性问题。

// src/components/Task.stories.js
import React from 'react';
import { Task } from './Task';
 
export default {
 component: Task,
 title: 'Task',
 argTypes: {
   onArchiveTask: { action: 'onArchiveTask' },
   onTogglePinTask: { action: 'onTogglePinTask' },
   onEditTitle: { action: 'onEditTitle' },
 },
};
 
const Template = (args) => <Task {...args} />;
 
export const Default = Template.bind({});
Default.args = {
 task: {
   id: '1',
   title: 'Buy milk',
   state: 'TASK_INBOX',
 },
};
 
export const Pinned = Template.bind({});
Pinned.args = {
 task: {
   id: '2',
   title: 'QA dropdown',
   state: 'TASK_PINNED',
 },
};
 
export const Archived = Template.bind({});
Archived.args = {
 task: {
   id: '3',
   title: 'Write schema for account menu',
   state: 'TASK_ARCHIVED',
 },
};
 
const longTitleString = `This task's name is absurdly large. In fact, I think if I keep going I might end up with content overflow. What will happen? The star that represents a pinned task could have text overlapping. The text could cut-off abruptly when it reaches the star. I hope not!`;
 
export const LongTitle = Template.bind({});
LongTitle.args = {
 task: {
   id: '4',
   title: longTitleString,
   state: 'TASK_INBOX',
 },
};

请注意插件如何找到两个违规行为。 第一个,“确保前景色和背景色之间的对比度符合 WCAG 2 AA 对比度比率阈值”特定于存档状态。 本质上,它的意思是文本和背景之间的对比度不够。 我们可以通过将文本颜色更改为稍深的灰色(从 gray.400gray.600)来解决此问题。

// src/components/Task.js
import React from 'react';
import PropTypes from 'prop-types';
import {
 Checkbox,
 Flex,
 IconButton,
 Input,
 Box,
 VisuallyHidden,
} from '@chakra-ui/react';
import { StarIcon } from '@chakra-ui/icons';
 
export const Task = ({
 task: { id, title, state },
 onArchiveTask,
 onTogglePinTask,
 onEditTitle,
 ...props
}) => (
 
 // code omitted for brevity
 
   <Box width="full" as="label">
     <VisuallyHidden>Edit</VisuallyHidden>
     <Input
       variant="unstyled"
       flex="1 1 auto"
       color={state === 'TASK_ARCHIVED' ? 'gray.600' : 'gray.700'}
       textDecoration={state === 'TASK_ARCHIVED' ? 'line-through' : 'none'}
       fontSize="sm"
       isTruncated
       value={title}
       onChange={(e) => onEditTitle(e.target.value, id)}
     />
   </Box>
 
   // code omitted for brevity
 </Flex>
);
 
Task.propTypes = {
 task: PropTypes.shape({
   id: PropTypes.string.isRequired,
   title: PropTypes.string.isRequired,
   state: PropTypes.string.isRequired,
 }),
 onArchiveTask: PropTypes.func.isRequired,
 onTogglePinTask: PropTypes.func.isRequired,
 onEditTitle: PropTypes.func.isRequired,
};

第二个违规行为“确保语义化地使用 <li> 元素”表明 DOM 结构不正确。 Task 组件渲染一个 <li> 元素。 但是,它没有在其故事中用 <ul> 包裹。 这是有道理的。 这些故事是针对 Task 组件的。 <ul> 实际上由 TaskList 提供。 因此,DOM 结构在 TaskList 故事中得到验证。 因此,忽略此错误是安全的。 实际上,我们可以继续为所有 Task 故事禁用此规则。

// src/components/Task.stories.js
import React from 'react';
import { Task } from './Task';
 
export default {
 component: Task,
 title: 'Task',
 argTypes: {
   onArchiveTask: { action: 'onArchiveTask' },
   onTogglePinTask: { action: 'onTogglePinTask' },
   onEditTitle: { action: 'onEditTitle' },
 },
 parameters: {
   a11y: {
     config: {
       rules: [{ id: 'listitem', enabled: false }],
     },
   },
 },
};
 
// remaining code omitted for brevity

您现在可以为所有其他组件重复此过程。

将可访问性测试集成到 Storybook 中可以简化您的开发工作流程。 您不必在处理组件时在不同的工具之间跳转。 您需要的一切都在浏览器中。 您甚至可以模拟视觉障碍,例如红色盲、绿色盲或蓝色盲。

防止回归

组件是相互依赖的——一个组件的更改可能会意外破坏其他组件。 为了确保不引入可访问性违规行为,我们需要在合并更改之前在所有组件上运行 Axe。

故事以基于 ES6 模块的格式编写,允许您将它们与其他测试框架重用。 在上一篇文章中,我们研究了将故事导入到 Jest 并使用 Testing Library 验证交互。 以下是 InboxScreen 的测试文件。

// src/InboxScreen.test.js
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import {
 render,
 waitFor,
 cleanup,
 within,
 fireEvent,
} from '@testing-library/react';
import { composeStories } from '@storybook/testing-react';
import { getWorker } from 'msw-storybook-addon';
import * as stories from './InboxScreen.stories';
 
describe('InboxScreen', () => {
 afterEach(() => {
   cleanup();
 });
 
 // Clean up after all tests are done, preventing this
 // interception layer from affecting irrelevant tests
 afterAll(() => getWorker().close());
 
 const { Default } = composeStories(stories);
 
 it('should pin a task', async () => {
   const { queryByText, getByRole } = render(<Default />);
 
   await waitFor(() => {
     expect(queryByText('You have no tasks')).not.toBeInTheDocument();
   });
 
   const getTask = () => getByRole('listitem', { name: 'Export logo' });
 
   const pinButton = within(getTask()).getByRole('button', { name: 'pin' });
 
   fireEvent.click(pinButton);
 
   const unpinButton = within(getTask()).getByRole('button', {
     name: 'unpin',
   });
 
   expect(unpinButton).toBeInTheDocument();
 });
 
 // More interaction tests
 it('should archive a task', async () => {...});
 it('should edit a task', async () => {...});
});

同样,我们可以使用 Jest Axe 集成在组件上运行可访问性测试。 让我们首先安装它:yarn add -D jest-axe

接下来,添加一个 it 块,该块运行 Axe 并检查违规行为。 Jest-axe 还为您提供了一个方便的断言 toHaveNoViolations,可以使用一个函数调用来验证这一点。

// src/InboxScreen.test.js
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import {
 render,
 waitFor,
 cleanup,
 within,
 fireEvent,
} from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { composeStories } from '@storybook/testing-react';
import { getWorker } from 'msw-storybook-addon';
import * as stories from './InboxScreen.stories';
 
expect.extend(toHaveNoViolations);
 
describe('InboxScreen', () => {
 afterEach(() => {
   cleanup();
 });
 
 // Clean up after all tests are done, preventing this
 // interception layer from affecting irrelevant tests
 afterAll(() => getWorker().close());
 
 const { Default } = composeStories(stories);
 
 // Run axe
 it('Should have no accessibility violations', async () => {
   const { container, queryByText } = render(<Default />);
 
   await waitFor(() => {
     expect(queryByText('You have no tasks')).not.toBeInTheDocument();
   });
 
   const results = await axe(container);
   expect(results).toHaveNoViolations();
 });
 
 it('should pin a task', async () => {...});
 it('should archive a task', async () => {...});
 it('should edit a task', async () => {...});
});

运行 yarn test 以启动 Jest。 它将执行所有交互测试并运行可访问性审核。 您现在可以在每次修改代码时运行整个测试套件。 允许您捕获回归。

我们已经了解了如何将自动化可访问性测试分层到 UI 开发工作流程中。 这并不能使您的应用程序完全可访问。 您仍然需要使用辅助技术(例如 VoiceOver 或 NVDA)测试界面。 但是,自动化确实可以节省您的时间并及早发现问题。

结论

Web 可访问性并不容易——在可访问性、迫在眉睫的截止日期、业务目标和技术债务之间取得平衡可能会让人感到不知所措。

Axe 和 Storybook Accessibility 插件等工具集成到您现有的开发工作流程中,并提供快速的反馈循环。 您可以在构建 UI 时查找和修复问题,从而节省时间。 更重要的是,使界面可访问可以为您的所有用户带来更好的体验!

将测试用例编写为故事意味着我们可以将它们重用于所有类型的测试。 故事文件成为组件所做一切的单一事实来源。 接下来,我们将研究另一个例子——用户流程测试。 我们将研究如何使用 Cypress 验证跨多个组件执行的任务。 加入邮件列表以获取有关更多 UI 测试文章发布的通知。

加入 Storybook 邮件列表

获取最新新闻、更新和版本

6,730位开发者和计数

我们正在招聘!

加入 Storybook 和 Chromatic 背后的团队。 构建被数十万开发人员在生产中使用的工具。 远程优先。

查看职位

热门文章

交互测试抢先看

使用 Storybook 的 play 函数测试连接的组件
loading
Dominic Nguyen

测试用户流程

验证您的 UI 端到端工作
loading
Varun Vachhar

如何测试组件交互

了解如何模拟用户行为并运行功能检查
loading
Varun Vachhar
加入社区
6,730位开发者和计数
为什么为什么选择 Storybook组件驱动的 UI
文档指南教程更新日志遥测
社区插件参与其中博客
案例展示探索项目组件词汇表
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI