测试配置实战:基于 axe 的 Hook 集成指南)
Storybook Test Runner 无障碍a11y测试配置实战基于 axe 的 Hook 集成指南【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybookStorybook 的 Test Runner 可以把每一个 story 变成可执行的测试而配合axe-playwright与preVisit/postVisit测试钩子你可以在每次渲染前后对页面执行无障碍Accessibility简称 a11y扫描把 WCAG 违规项直接变成 CI 上的失败用例。本文以 docs/_snippets/test-runner-a11y-config.md 为核心完整讲解该配置的 JS / TS 两种写法、钩子生命周期并结合仓库内 test-runner 官方文档、accessibility-testing 文档 与 a11y 插件源码 进行纵深展开读完你就能在自己的 Storybook 项目中落地一套“渲染即扫描”的无障碍回归测试。前置条件需要安装哪些依赖在编写.storybook/test-runner.js|ts之前请确保项目满足以下前提已安装并配置 Storybook Test Runner它是与 Storybook 并行运行的独立、框架无关的测试工具基于 Jest 与 Playwright 构建。运行test-storybook --eject可以在项目根目录生成可修改的test-runner-jest.config.js。已安装 Accessibility 插件storybook/addon-a11ytest-runner 本身不扫描无障碍问题它负责把 story 渲染成页面真正的 axe 扫描由axe-playwright驱动。仓库内插件实现位于 code/addons/a11y其中 a11yRunner.ts 是核心运行逻辑。安装axe-playwright它提供injectAxe、checkA11y、configureAxe三个在本文中会用到的 API分别负责向页面注入 axe 脚本、执行扫描并断言、以及按 story 参数调整 axe 规则。安装完成后在package.json中添加脚本{ scripts: { test-storybook: test-storybook } }运行方式为先启动本地 Storybook默认端口6006再另开终端执行yarn test-storybook。test-runner 要求有一个本地运行中的 Storybook 实例或一个已发布的 Storybook 地址可用--url指定。核心配置在 Test Runner 钩子中注入并运行 axe在 Storybook 目录下新建.storybook/test-runner.js或 TypeScript 项目使用.storybook/test-runner.ts内容如下。这是 docs/_snippets/test-runner-a11y-config.md 的完整配置也是整个无障碍测试体系的基石const { injectAxe, checkA11y } require(axe-playwright); /* * See https://storybook.js.org/docs/writing-tests/integrations/test-runner#test-hook-api * to learn more about the test-runner hooks API. */ module.exports { async preVisit(page) { await injectAxe(page); }, async postVisit(page) { await checkA11y(page, body, { detailedReport: true, detailedReportOptions: { html: true, }, }); }, };import type { TestRunnerConfig } from storybook/test-runner; import { injectAxe, checkA11y } from axe-playwright; /* * See https://storybook.js.org/docs/writing-tests/integrations/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, body, { detailedReport: true, detailedReportOptions: { html: true, }, }); }, }; export default config;这份配置做的事情可以拆成两半preVisit(page)在每个 story 被渲染之前调用injectAxe(page)把 axe 核心脚本注入当前 Playwright 页面。此时 story 尚未渲染注入 axe 是为了让后续扫描可用。postVisit(page)在 story 渲染完成包括其play函数执行完毕之后对body元素执行checkA11y扫描。detailedReport: true让 CLI 输出包含每条违规的详细说明detailedReportOptions.html: true则额外输出违规元素的 HTML 片段方便直接定位问题 DOM。TypeScript 版本的关键区别在于TestRunnerConfig类型它来自storybook/test-runner为setup、preVisit、postVisit等钩子提供完整的类型约束IDE 中能获得参数自动补全。钩子生命周期测试在何时触发扫描要理解上述配置为什么有效需要先了解 test-runner 的测试钩子 API。在 test-runner 官方文档 的 “Test hook API” 一节中定义了四个可以全局覆写的钩子钩子说明prepare为测试准备浏览器签名async prepare({ page, browserContext, testRunnerConfig })setup在所有测试运行之前执行一次setup() {}preVisit在 story 首次被访问并渲染到浏览器之前执行async preVisit(page, context) {}postVisit在 story 被访问并完全渲染之后执行async postVisit(page, context) {}默认导出对象module.exports/export default必须是一个包含这些钩子的配置对象。除setup外其余钩子均为异步函数preVisit与postVisit额外接收两个参数Playwright 的page对象以及包含 story 的id、title、name的 context 对象。一次完整的测试执行流程如下setup先于所有测试运行生成包含 story 信息的 context 对象Playwright 导航到该 story 的页面执行preVisit此时注入 axestory 渲染若有play函数则执行之执行postVisit此时扫描 a11y 并断言。可以看出checkA11y放在postVisit而不是preVisit是因为只有 story 完全渲染后DOM 才是最终可访问性状态而injectAxe放在preVisit则保证渲染过程中页面就具备扫描能力。进阶一应用 story 级别的 axe 规则与扫描元素基础配置对所有 story 一视同仁地扫描body。但在真实项目中不同 story 往往需要不同的规则集——例如某个 story 展示了反模式用法或某个组件包含不需要检查的区域。此时可以使用getStoryContext与configureAxe把 story 的parameters.a11y配置带进扫描过程。完整方案见仓库内的 test-runner-a11y-configure.mdconst { injectAxe, checkA11y, configureAxe } require(axe-playwright); const { getStoryContext } require(storybook/test-runner); module.exports { 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 ?? body; await checkA11y(page, element, { detailedReport: true, detailedReportOptions: { html: true, }, }); }, };import type { TestRunnerConfig } from storybook/test-runner; import { getStoryContext } from storybook/test-runner; import { injectAxe, checkA11y, configureAxe } from axe-playwright; 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 ?? body; await checkA11y(page, element, { detailedReport: true, detailedReportOptions: { html: true, }, }); }, }; export default config;相比基础配置这里新增了两个关键点getStoryContext(page, context)test-runner 导出的辅助函数用于读取 story 的完整上下文包括parameters、args、argTypes等。它让你在钩子中拿到 Storybook 侧声明的配置实现“测试配置与 story 参数同源”。configureAxe(page, { rules })把 story 的parameters.a11y.config.rules应用到当前页面的 axe 实例上从而在扫描前动态启用 / 禁用指定规则。element的兜底逻辑优先使用parameters.a11y.element指定的选择器作为扫描上下文缺省时回退到body。这正对应 accessibility-testing.mdx 中“Excluded elements”一节的机制——通过自定义 axe 的 context 参数可以排除某些不参与检查的元素例如忽略带有no-a11y-check类的节点。进阶二按 story 跳过无障碍测试并非每个 story 都需要或能够通过无障碍检查——例如用于演示反模式的 story、覆盖内部工具的页面等。你可以通过parameters.a11y.disable在 story 或 meta 级别关闭自动检查test-runner 侧则需在postVisit中主动判断并提前返回。仓库内的 test-runner-a11y-disable.md 给出了实现const { getStoryContext } require(storybook/test-runner); const { injectAxe, checkA11y } require(axe-playwright); module.exports { 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, body, { detailedReport: true, detailedReportOptions: { html: true, }, }); }, };import type { TestRunnerConfig } from storybook/test-runner; import { getStoryContext } from storybook/test-runner; import { injectAxe, checkA11y } from axe-playwright; 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, body, { detailedReport: true, detailedReportOptions: { html: true, }, }); }, }; export default config;在 story 文件中对应的关闭写法是export const AntiPatternStory { parameters: { a11y: { disable: true, }, }, };这样当 test-runner 访问该 story 时postVisit检测到a11y.disable为真即直接返回跳过扫描其余 story 仍按默认规则扫描body。理解背后的参数体系parameters.a11y上文反复出现的parameters.a11y是整个无障碍测试的参数入口。根据 accessibility-testing.mdx 与 a11y 插件类型定义它支持在项目级.storybook/preview.*、组件级story 文件的 meta / default export以及 story 级三层配置主要子项包括参数作用a11y.configaxe 的配置对象其中config.rules用于启用 / 禁用 / 配置单独规则config.runOnly用于切换规则集如 WCAG 2.2 AA、WCAG 2.x AAAa11y.element指定扫描的 DOM 选择器axe context用于排除或限定检查范围a11y.disable布尔值为true时关闭该 story / 组件 / 项目的自动无障碍检查a11y.test控制无障碍测试行为取值off、todo、error其中a11y.test的三个取值值得单独说明也见 accessibility-testing.mdxoff不运行无障碍测试仍可在插件面板中手动检查todo运行测试违规项在 Storybook UI 中作为警告展示不阻断 CIerror运行测试违规项在 UI 与 CLI / CI 中均作为失败处理。todo的语义是代码库中的一个字面TODO标记对已知有缺陷但暂未修复的 story 使用它既保持问题可见又不阻塞开发。配合 推荐工作流一节 的做法——先在preview中把a11y.test设为error全量卡关再对存量问题组件降级为todo逐组件修复后移除标记——可以在不打断迭代的前提下渐进式提升可访问性。源码视角a11y 插件内部如何处理规则为了让配置不流于表面值得看一眼插件内部的实现。在 code/addons/a11y/src/a11yRunner.ts 中axe 运行的核心逻辑对规则做了两层处理默认参数与默认禁用规则DEFAULT_PARAMETERS { config: {}, options: {} }第 16 行定义了缺省值DISABLED_RULES常量默认禁用了region规则第 18-22 行注释说明“在组件测试中地标landmark并不总是存在该规则检查可能产生误报”因此默认关闭。禁用规则的合并策略getDisabledRules第 24-41 行遍历config.rules把enabled: false的规则收集起来mergeDisabledRulesIntoRunOptions第 43-60 行则把这些禁用项合并进axe.run的runOnly选项——因为runOnly可能会重新启用带标签的规则所以必须把用户配置的禁用项镜像进运行选项且不修改用户的原始参数对象。这段实现解释了为什么在 story 中写a11y.config.rules例如把某条规则enabled: false会被 test-runner 侧通过configureAxe(page, { rules: storyContext.parameters?.a11y?.config?.rules })正确消费规则最终进入 axe 的 run options且遵循“后出现的规则覆盖同 id 先出现的规则”的合并顺序。插件自身的规则映射与定义可进一步查看 AccessibilityRuleMaps.ts运行逻辑的单元测试则在 a11yRunner.test.ts。运行、调试与 CI 落地配置完成后运行方式与 test-runner 保持一致yarn test-storybook常用 CLI 参数完整清单见 test-runner.mdx包括参数说明--url url指定远程 Storybook 地址例如test-storybook --url http://the-storybook-url-here.com--maxWorkers n限制并行 worker 数CI 低内存环境建议--maxWorkers2--browsers chromium firefox指定运行浏览器可选 chromium / firefox / webkit--watch/--watchAll监听模式--failOnConsole浏览器 console 报错即失败--shard1/8分片执行适用于大规模 CI 集群本地调试时需要注意默认错误输出会被截断为 1000 字符可通过环境变量调整DEBUG_PRINT_LIMIT5000 yarn test-storybook若遇到Timeout - Async callback was not invoked within the 15000 ms timeout通常是 story 数量过多或 CI 内存过低用--maxWorkers限制并行度test-runner 基于 PlaywrightCI 中可能需要对应的 docker 镜像或系统依赖。在 CI 中落地无障碍卡关时还有一条容易被忽略的规则见 accessibility-testing.mdx无障碍测试只有在parameters.a11y.test设为error时才会在 CI 中产生失败如果设为todoCI 中不会有任何错误或警告输出。因此“全量卡关”与“渐进修复”两种策略对应着error与todo的选择请根据团队节奏明确设定。小结从 test-runner-a11y-config.md 出发本文覆盖了 Storybook Test Runner 无障碍测试的完整链路injectAxe注入、checkA11y扫描、getStoryContext读取 story 参数、configureAxe应用规则、a11y.disable按需跳过以及parameters.a11y参数体系的test/config/element分层配置。结合 a11y 插件源码 可以看到规则合并与默认禁用如region等行为都有明确的实现依据。这套配置不仅让每个 story 在渲染后自动接受 WCAG 规则扫描还能在 CI 中作为硬性质量门禁配合todo标记实现渐进式无障碍改进是组件库质量体系中性价比极高的一环。【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考