使用 Storybook 进行可访问性测试
美国有 26% 的成年人 至少患有一种残疾。当您提高可访问性时,它会对您当前和未来的客户产生巨大的影响。这也是一项法律要求。
检查可访问性的最准确方法是在真实设备上手动检查。但这需要专业的知识和大量的时间,而前端团队这两者都很稀缺。
这就是为什么许多公司现在使用自动化和手动测试相结合的方式。自动化可以低成本地帮助开发者发现常见的可访问性问题。手动 QA 则保留用于需要人工关注的更棘手的问题。
有很多资源深入探讨了可访问性原则,因此我们在此不再赘述。相反,我们将重点介绍如何使用 Storybook 自动化可访问性测试。这是一种务实的方法,可以查找和修复您可能遇到的大多数问题。
为什么要自动化?
在开始之前,让我们检查一下常见的残疾类型:视觉、听觉、行动、认知、言语和神经系统。这些用户残疾产生了如下应用需求:
- ⌨ 键盘导航
- 🗣 屏幕阅读器支持
- 👆 触摸友好
- 🎨 足够高的颜色对比度
- ⚡️ 减少动画
- 🔍 缩放
过去,您需要通过检查浏览器、设备和屏幕阅读器的组合来验证每个组件的这些要求。但这手工操作是不切实际的,因为应用程序有数十个组件,并且 UI 不断更新。
自动化加速您的工作流程
自动化工具会根据 WCAG 规则和其他行业认可的最佳实践,针对一组启发式方法审核渲染的 DOM。它们充当 QA 的第一道防线,以捕获明显的违反可访问性的行为。
例如,Axe 平均可以自动发现 57% 的 WCAG 问题。这使团队能够将其专家资源集中在需要手动审查的更复杂的问题上。
许多团队使用 Axe 库,因为它与大多数现有测试环境集成。例如,Twilio Paste 团队使用 jest-axe 集成。而 Shopify Polaris 和 Adobe Spectrum 团队则使用 Storybook 插件 版本。
Storybook 插件在浏览器中运行检查(与 Jest 的 jsdom 相反),因此可以捕获诸如低对比度之类的问题。但是,它确实需要您手动验证每个 Story。
可访问性测试工作流程
通过在整个开发过程中运行这些检查,您可以缩短反馈循环并更快地修复问题。以下是工作流程的样子:
- 👨🏽💻 开发期间: 使用 Storybook 专注于一次一个组件。使用 A11y 插件模拟视觉缺陷并在组件级别运行可访问性审核。
- ✅ 对于 QA: 将 Axe 审核集成到您的功能测试管道中。对所有组件运行检查以捕获回归。
让我们看看这个工作流程的实际应用。
安装可访问性插件
Storybook 的 Accessibility 插件在活动的 Story 上运行 Axe。它在面板中可视化测试结果,并概述所有存在违规的 DOM 节点。
要安装插件,请运行:yarn add --dev @storybook/addon-a11y
。然后,将 '@storybook/addon-a11y'
添加到 .storybook/main.js
中的 addons 数组中
/** @type { import('@storybook/react-vite').StorybookConfig } */
const config = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
staticDirs: ['../public'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
+ '@storybook/addon-a11y',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
};
export default config;
在编码时测试可访问性
我们已经隔离了 Task 组件,并将它的所有用例捕获为 Story。在开发阶段,您可以循环浏览这些 Story 以发现可访问性问题。
import Task from './Task';
export default {
component: Task,
title: 'Task',
argTypes: {
onArchiveTask: { action: 'onArchiveTask' },
onTogglePinTask: { action: 'onTogglePinTask' },
onEditTitle: { action: 'onEditTitle' },
},
};
export const Default = {
args: {
task: {
id: '1',
title: 'Buy milk',
state: 'TASK_INBOX',
},
},
};
export const Pinned = {
args: {
task: {
id: '2',
title: 'QA dropdown',
state: 'TASK_PINNED',
},
},
};
export const 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 = {
args: {
task: {
id: '4',
title: longTitleString,
state: 'TASK_INBOX',
},
},
};
请注意插件如何发现两个违规。“元素必须满足最小颜色对比度阈值”是特定于 archived
状态的。本质上,这意味着任务标题和背景之间的对比度不够。我们可以通过在应用程序的 CSS 中将文本颜色更改为较深的灰色来快速修复它(位于 src/index.css
中)。
.list-item.TASK_ARCHIVED input[type="text"] {
- color: #a0aec0;
+ color: #4a5568;
text-decoration: line-through;
}
第二个违规,“某些 ARIA 角色必须包含在特定的父元素中”表示 DOM 结构不正确。Task 组件仅渲染一个 <li>
元素。因此,我们需要更新我们的 Story,将组件包装在 <ul>
元素中。
import Task from './Task';
export default {
component: Task,
title: 'Task',
argTypes: {
onArchiveTask: { action: 'onArchiveTask' },
onTogglePinTask: { action: 'onTogglePinTask' },
onEditTitle: { action: 'onEditTitle' },
},
};
/*
*👇 Wraps the component with a custom render function.
* See https://storybook.org.cn/docs/api/csf
* to learn how to use render functions.
*/
export const Default = {
render: (args) => (
<ul>
<Task {...args} />
</ul>
),
args: {
task: {
id: '1',
title: 'Buy milk',
state: 'TASK_INBOX',
},
},
};
export const Pinned = {
render: (args) => (
<ul>
<Task {...args} />
</ul>
),
args: {
task: {
id: '2',
title: 'QA dropdown',
state: 'TASK_PINNED',
},
},
};
export const Archived = {
render: (args) => (
<ul>
<Task {...args} />
</ul>
),
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 = {
render: (args) => (
<ul>
<Task {...args} />
</ul>
),
args: {
task: {
id: '4',
title: longTitleString,
state: 'TASK_INBOX',
},
},
};
您现在可以对所有其他组件重复此过程。
将可访问性测试集成到 Storybook 中可以简化您的开发工作流程。您不必在处理组件时在不同的工具之间跳转。您需要的一切都在浏览器中。您甚至可以模拟视觉障碍,例如红色盲、绿色盲或蓝色盲。
使用测试运行器自动捕获回归
通常,对组件的更改可能会无意中引入新的可访问性问题。为了捕获此类回归,您需要在打开拉取请求之前测试所有 Story。但是,Accessibility 插件仅在您查看 Story 时运行检查。要一次测试所有 Story,我们可以使用 Storybook 测试运行器。它是一个独立的实用程序(由 Jest 和 Playwright 提供支持),用于检查 Story 中的渲染错误。
让我们继续配置测试运行器以运行 Axe。我们将首先安装 axe-playwright。
yarn add --dev axe-playwright
在您的 Storybook 目录中添加一个新的配置文件,内容如下:
const { injectAxe, checkA11y } = require('axe-playwright');
module.exports = {
async preVisit(page) {
await injectAxe(page);
},
async postVisit(page) {
await checkA11y(page, "#storybook-root", {
detailedReport: true,
detailedReportOptions: {
html: true,
},
});
},
};
preVisit
和 postVisit
是方便的钩子,允许您配置测试运行器以执行其他任务。我们使用这些钩子将 Axe 注入到 Story 中,然后在它渲染后运行可访问性测试。
您会注意到传递到 checkA11y
函数中的一些选项。我们已将 Axe 设置为从 Story 的根元素开始,然后遍历 DOM 树以检查问题。它还将根据它遇到的问题生成详细的报告,并输出违反可访问性规则的 HTML 元素列表。
要运行测试,请在一个终端窗口中使用 yarn storybook
启动 Storybook,并在另一个终端窗口中使用 yarn test-storybook
启动测试运行器。
捕获集成问题
UI 通过组合组件并将它们连接到数据和 API 来组装。这有很多潜在的故障点。接下来,我们将研究如何使用 Cypress 通过一次性测试系统的所有层来捕获集成问题。