首页
/ Storybook 无障碍测试指南:在单个 Story 中按规则精细化配置 axe-core 检查参数

Storybook 无障碍测试指南:在单个 Story 中按规则精细化配置 axe-core 检查参数

2026-09-06 18:37:33作者:董斯意

导读

Storybook 的 Accessibility(a11y)Addon 基于 Deque 的 axe-core 库对渲染后的 DOM 执行无障碍审计。实际项目中,不同 Story 往往面临不同的无障碍约束——某些表单控件天然携带 autocomplete 属性、某些演示反模式的 Story 无需 image-alt 检查。此时项目级配置无法满足需求,需要把 parameters.a11y.config 的规则级配置落到单个 Story 层级。本篇以 Storybook 仓库文档片段 addon-a11y-config-rules-in-story.md 为主线,讲解如何在单条 Story 内通过 config.rules 数组按 id 开关、限定规则的 axe-core 规则,并覆盖 CSF 3、CSF Next 与 Svelte CSF 三种写法,最后结合仓库源码说明规则配置的底层合并逻辑。

一、先理解参数的层级:Story 级 config 覆盖什么

axe-core 的配置可以通过 Storybook 的 parameters 从粗到细分三层注入:

配置位置 作用范围 典型文件
.storybook/preview.*parameters.a11y 项目内全部 Story 项目级全局规则,如统一关闭 region
Story 文件 meta(default export)的 parameters.a11y 该文件内全部 Story 组件级默认
单个 Story 的 parameters.a11y 仅该条 Story 局部开关规则,见 addon-a11y-config-in-meta-and-story.md

parameters.a11y 中与 axe-core 相关的三个核心字段在源码 params.ts 中被定义为 A11yParameters 接口:

  • context:传给 axe.run 的上下文,决定对哪些元素运行检查(文档默认值为 'body',详见 accessibility-testing.mdx 配置表);
  • config:传给 axe.configure() 的配置对象(即 axe-core 的 Spec),最常见的用途就是按规则配置(individual rules)
  • options:传给 axe.run 的选项,可用于调整 runOnly 规则集。

本篇讨论的单个 Story 中的 config.rules 即属于 config 字段。需要强调的是:axe-core 的 Spec.rules 中每条规则对象可以携带 idenabledselectorany/all/nonetags 等属性,Storybook 场景下最常用的组合是 id + enabled(整条规则开/关)与 id + selector(把规则限定在指定 CSS 选择器命中的元素上)。

二、核心示例:在单个 Story 中配置两条规则

原文档给出的是同一个示例在 11 种写法变体下的完整代码,其技术实质是两条规则:

config: {
  rules: [
    {
      // 基于提供的 CSS 选择器,autocomplete 规则将不会在命中的元素上运行
      id: 'autocomplete-valid',
      selector: '*:not([autocomplete="nope"])',
    },
    {
      // 将 enabled 设为 false,会在该条 Story 上关闭此项规则的检查
      id: 'image-alt',
      enabled: false,
    },
  ],
}

两条规则分别演示了两种典型的“按规则定制”手段:

  • autocomplete-valid 配合 selector: '*:not([autocomplete="nope"])':当被测组件里恰好存在一个值为 nope(反模式演示)的输入框时,这条 CSS 选择器把它从自动完成校验中排除出去,其余输入框仍正常受检;
  • image-alt 直接设 enabled: false:彻底关闭图片替代文本的检查——适合图片本身只是装饰性元素、或当前 Story 刻意演示缺失 alt 的场景。

与默认禁用规则的区别

需要注意一个前提:Storybook 对全部检查默认就已关闭了 region 规则。在 accessibility-testing.mdx 的 “Default parameters.a11y.config” 说明与 a11yRunner.tsDISABLED_RULES 常量中可以看到,原因是组件测试中 landmarks(地标元素)未必存在,检查会产生误报。因此你在 Story 里配置的 rules 是在“默认关闭 region”之上的增量定制,而不是从零重建规则全集。

三、按渲染器与语法选择你的写法

下面按写法族完整给出可运行的示例。

3.1 CSF 3 + TypeScript(common / React / Vue 等)

适用于 React、Vue 等主流渲染器的 Button.stories.ts,import 行换成对应框架(如 @storybook/react-vite@storybook/vue3-vite):

// ...rest of story file

export const IndividualA11yRulesExample: Story = {
  parameters: {
    a11y: {
      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,
          },
        ],
      },
    },
  },
};

3.2 CSF 3 + JavaScript

// ...rest of story file

export const IndividualA11yRulesExample = {
  parameters: {
    a11y: {
      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,
          },
        ],
      },
    },
  },
};

3.3 CSF Next 🧪(preview.meta / meta.story)

CSF Next 通过从 .storybook/preview 导入的 preview.meta() 声明 meta、meta.story() 声明 Story,参数结构保持不变。以 React 为例:

import preview from '../.storybook/preview';

import Button from './Button';

const meta = preview.meta({
  component: Button,
});

export const IndividualA11yRulesExample = meta.story({
  parameters: {
    a11y: {
      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,
          },
        ],
      },
    },
  },
});

其它渲染器的差异仅在导入与 component 的写法上,parameters.a11y.config.rules 本身完全一致:

  • Angularimport { Button } from './button.component';component: Button
  • Vueimport Button from './Button.vue';component: Button
  • Web Components:无需导入组件类,直接 const meta = preview.meta({ component: 'demo-button' });

3.4 Svelte CSF(defineMeta + Story 标签)

Svelte CSF 是唯一语法形态不同的变体,meta 通过 <script module> 里的 defineMeta 定义,Story 通过模板中的 <Story> 标签声明,parameters 以对象字面量形式传入:

<script module>
  import { defineMeta } from '@storybook/addon-svelte-csf';

  import Button from './Button.svelte';

  const { Story } = defineMeta({
    component: Button,
  });
</script>

<Story
  name="IndividualA11yRulesExample"
  parameters={{
    a11y: {
      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,
          },
        ],
      },
    },
  }}
/>

若你的 Svelte 项目使用普通 CSF 3 而非 Svelte CSF,则把同一份 parameters 对象放入具名导出的 Story 即可,与本篇 3.1 / 3.2 节写法相同。

四、底层原理:Story 级 rules 是如何被合并执行的

为什么把 rules 写在单条 Story 里就能只影响这一条?答案在 a11yRunner.tsrun() 函数中,它负责把 A11yParameters 转成对 axe-core 的真正调用:

  1. 合并默认禁用规则configWithDefault 把默认的 DISABLED_RULES(即 region{ id: 'region', enabled: false })拼接到用户传入的 config.rules 之前,再调用 axe.configure(configWithDefault)。由于你写在 Story 上的 rules 来自当前 Story 的 parameters,故每条 Story 运行时都会带着自己的规则集去重新 axe.reset() + axe.configure()
  2. 按顺序覆盖getDisabledRules() 遍历 config.rules 时,注释明确写着 “Rules are applied in order, so a later entry overrides an earlier one for the same id”——同一条 id 的规则,数组中靠后的条目覆盖靠前的条目,这一语义与 axe-core 的规则合并行为一致;
  3. runOnly 下的镜像禁用mergeDisabledRulesIntoRunOptions() 处理一个易被忽略的细节——当 options 里配置了 runOnly(例如只想按 WCAG 2.2 AA 规则集检查)时,axe.run({ runOnly }) 可能会重新启用某些已关闭规则的 tag 匹配。因此该函数会把你显式 enabled: false 的规则镜像写进 axe.runrules 选项,保证禁用规则不被 runOnly 悄悄复活;
  4. 排除内部元素与串行队列:axe-core 运行前会把 Storybook 内部元素(.sb-wrapper#storybook-docs#storybook-highlights-root)加入 exclude 上下文,且通过一个 Promise 队列把多次检查串行化,因为 axe-core 并不适合并行执行。

仓库中的单元测试 a11yRunner.test.ts 直接验证了上述行为:测试 passes disabled configured rules to axe.run when runOnly is present 断言 config.rules{ id: 'target-size', enabled: false } 会同时出现在传给 axe.configure 的 rules 与传给 axe.runrules.target-size = { enabled: false } 中;测试 respects configured rule overrides when collecting disabled rules 则验证了把默认的 region 显式设为 enabled: true 时,runner 不会再向 runOnly 注入禁用项。

五、常见搭配与注意点

  • 与 runOnly 规则集配合:若希望在项目级通过 options.runOnly 切换检查规则集(如 WCAG 2.2 AA),请参考 addon-a11y-config-rulesets-in-preview.md;而单条 Story 的 config.rules 适合做规则级的例外豁免,两者互不冲突;
  • parameters.a11y.test 配合:Story 层配置还可以配合 parameters.a11y.test(取值 'off' / 'todo' / 'error')决定该 Story 的可访问性测试是跳过、作为 TODO 警告还是直接失败。规则豁免应只服务于“确实不需要检查”的合理场景,而不是用它来掩盖未修复的违规——可访问性面板仍会展示 Violations 结果;
  • selector 的取舍:能用 selector 把误报元素精准排除时,优先于全局 enabled: false,因为它保留了该规则对 Story 其余元素的覆盖;
  • 组件级与文件级同理:同一份 config.rules 也可以上移到 meta 级别作用于整个文件,写法参考 addon-a11y-config-in-meta-and-story.md
  • 手动触发不受影响:规则配置只影响自动检查与 Vitest addon 的测试行为,你仍可在 Accessibility 面板中手动运行检查查看 Violations / Passes / Incomplete 三个子标签的结果。

六、小结

Storybook 的 a11y 规则配置是自顶向下继承、按层级合并生效的:项目级预览文件、文件级 meta、单条 Story 三级均可设置 parameters.a11y.config.rules。在本篇展示的 autocomplete-valid + selectorimage-alt + enabled: false 两个组合中,你可以掌握“按 CSS 选择器局部豁免某规则”与“整条规则关闭”两种精细化手段,并能在 CSF 3、CSF Next 与 Svelte CSF 之间自由迁移写法。结合 a11yRunner.ts 的合并逻辑可知:同 id 的 rules 按数组顺序后者覆盖前者,配合 runOnly 时禁用规则会被镜像到 run 选项以保证不被重新启用——理解了这些底层规则,你就能在真实组件库中把自动化无障碍检查的误报降到最低,同时又不牺牲规则覆盖面。

登录后查看全文
热门项目推荐
相关项目推荐