☰
axe-core Context 参数完全指南:精准控制自动化无障碍测试的测试范围
2026/9/28 3:26:01 网站建设 项目流程
  • 测试

【免费下载链接】axe-core

Accessibility engine for automated Web UI testing

项目地址:https://gitcode.com/gh_mirrors/ax/axe-core
点击查看免费下载

导读

本文围绕 axe-core 的context参数展开,它是控制"测试哪些元素、忽略哪些元素"的核心工具,适用于单元测试(Karma/Jest/jsdom)、端到端测试(Selenium/Puppeteer/Playwright)以及跨 iframe、跨 Shadow DOM 的复杂页面场景。读完本文,你将掌握 CSS 选择器、DOM 节点、include/exclude、fromFrames、fromShadowDom全部六种上下文写法,并理解 axe-core 底层的上下文归一化与包含/排除判定机制,从而让每一次自动化无障碍扫描精准命中目标区域。


一、什么是 context:先理解六种合法输入

axe.run(context, options, callback)的第一个参数context(可选)定义分析的 DOM 范围。默认情况下axe.run会测试整个document,而 context 允许你精确指定测试与忽略的部分。根据 API 文档,context 可以传入以下六种形式之一:

  1. 单个元素引用,如document.getElementById('content');
  2. NodeList,如document.querySelectorAll(...)的返回值;
  3. CSS 选择器或选择器数组;
  4. 含exclude和/或include属性的对象;
  5. 含fromFrames属性的对象;
  6. 含fromShadowDom属性的对象。

在源码层面,context 的类型判定由 lib/core/utils/is-context.js 完成:isContextSpec判断"字符串 / Node / fromFrames 对象 / fromShadowDom 对象 / 类数组"五种可解析形态(isContextProp),以及带include或exclude键的对象(isContextObject)。axe.run的参数归一化见 lib/core/public/run/normalize-run-params.js:若第一个参数不是合法 context,则自动回退为document并把参数依次后移。也就是说,axe.run()、axe.run(options)、axe.run(callback)全部等价于对整页进行测试。

补充:context 对象还可以携带page、initiator、focusable、size等布尔/对象字段,用于运行部分测试(partial result)等进阶场景,详见 lib/core/base/context.js。常规用法下无需关心这些字段。


二、测试特定元素:CSS 选择器与选择器数组

当传入 CSS 选择器或选择器数组时,axe 只测试匹配这些选择器的元素及其内部全部内容:

// 测试每个 <nav> 与 <main> 元素,以及它们内部的所有内容: await axe.run('nav, main'); // 测试每个 <nav>、每个带 sideBar 类的元素、 // 以及带 #header id 的元素,连同它们的所有内容: await axe.run(['nav', '.sideBar', '#header']);

实用技巧:当页面由多个团队协作开发时,可以给自己的模块统一打上一个独特 class,然后让 axe 只测试自己团队负责的区块,避免其他团队尚未完成的区域干扰扫描结果。

在浏览器驱动 API 中使用.include()

axe-core 通常经由浏览器驱动 API(Selenium、Puppeteer、Playwright)使用,这类 API 并不直接接收 context 对象,而是通过AxeBuilder的.include()方法实现同样的效果,具体用法请查阅对应 API 的文档,典型写法如下:

const axe = new AxeBuilder({ page }); // 测试每个 <nav> 与 <main> 元素,以及它们内部的所有内容: axe.include('nav, main');

仓库中的 Puppeteer 示例(doc/examples/puppeteer/axe-puppeteer.js)与 CDP 示例(doc/examples/chrome-debugging-protocol/axe-cdp.js)展示了这一模式的完整接入方式。


三、测试 DOM 节点:Node、NodeList 与数组

axe 可以直接接收原生 DOM 元素进行测试,前提是这些元素必须已挂载到 DOM 树上(document或其可见子树中)。这在 Karma、Jest 等测试环境中非常实用:

// 测试单个 DOM 节点 const img = document.createElement('img'); document.body.appendChild(img); await axe.run(img); // 测试 NodeList,例如 document.querySelectorAll 的返回值: const nodes = document.querySelectorAll('main, header'); await axe.run(nodes); // 测试节点数组: const navbar = document.getElementById('navbar'); const cookiePopup = document.getElementById('cookie-popup'); await axe.run([navbar, cookiePopup]);

源码层面,normalizeContext对window.Node实例不做任何包装(见 normalize-context.js 的注释 "Nodes must not be wrapped in an array"),而parseSelectorArray会通过getNodeFromTree把节点映射到扁平树上的对应节点(见 parse-selector-array.js)。测试 test/core/base/context.js 覆盖了单个节点、数组节点、嵌套 div、含控件的表单等场景,并验证了"未匹配的引用会被移除"这一行为。

组件框架(React / Vue / Angular)

由于 axe 必须依赖真实 DOM 才能测试,React、Vue、Angular 等组件必须先渲染、再测试。以下示例展示渲染<MyApp>React 组件后交给 axe 测试:

const appRoot = document.getElementById('app'); ReactDOM.createRoot(appRoot).render(MyApp); await axe.run(appRoot);

重要限制:组件测试库(如 Enzyme)同时提供render与shallow方法。由于 axe 需要完整渲染并挂载到 DOM 树上的真实结构,无法测试用shallow(浅渲染)构建的组件。仓库中的 Jest + React 示例(doc/examples/jest_react/link.test.js)展示了渲染后进行测试的完整流程。


四、排除元素:exclude与include对象

页面上总有一些开发者无法控制的区域(第三方广告、外嵌视频等)。此时可以用带exclude属性的对象让 axe 跳过这些元素。exclude接受与 include 完全相同的输入类型,包括 CSS 选择器和 DOM 节点:

// 测试除广告横幅以外的所有内容: await axe.run({ exclude: '.ad-banner' }); // 测试除这些 DOM 节点以外的所有内容(NodeList 同样可用): const youtubeVids = document.querySelectorAll('iframe[src^="youtube.com"]'); await axe.run({ exclude: youtubeVids });

在 Playwright 等 API 中,对应方法是AxeBuilder的.exclude():

const axe = new AxeBuilder({ page }); // 测试除广告横幅与 YouTube 帧以外的所有内容: axe.exclude('.ad-banner, iframe[src^="youtube.com"]');

隐式 include:一切选择器都是 include

当你直接传入 CSS 选择器或 DOM 节点数组时,axe 将其视作隐式的 include。上一节所有例子都可以改写为带include属性的对象,二者行为完全等价:

await axe.run('main'); // 等同于: await axe.run({ include: 'main' });

当 axe 未收到任何 include(无论是显式还是隐式)时,默认使用document:

await axe.run({ exclude: '.ad-banner' }); // 等同于: await axe.run({ include: document, exclude: '.ad-banner' });

这一默认行为在源码中有明确体现:normalize-context.js 中include.length === 0时自动include.push(document);测试 test/core/base/context.js 验证了"默认 include 为 document"与"空 include 回退为 document"两条路径。

同时使用include与exclude

想测试页面特定区块、又跳过该区块中的部分内容时,可以同时传入include与exclude:include决定测什么,exclude在已 include 的区域内进一步排除。下面的例子测试main和footer元素,但跳过其中所有.ad-banner:

await axe.run({ include: ['main', 'footer'], exclude: '.ad-banner' });

优先级规则(重要):当一个元素同时被 include 和 exclude 命中时,命中最近祖先的选择器优先。例如:某节点的祖父被 include 但父被 exclude,则该节点被排除;反之祖父被 exclude 但父被 include,则该节点被纳入。

这条规则正是源码 is-node-in-context.js 的实现逻辑:分别收集包含该节点的 include 候选与 exclude 候选,取各自最深的节点(getDeepest),若最深的 exclude 包含最深的 include 则排除,否则包含(第 10-22 行)。这也是 context 判定所有包含/排除关系的最终裁决函数。


五、从先前测试结果中选择:target属性复用

include或exclude可以使用先前测试结果中的节点,从而跳过已发现的问题,或在你修复问题时反复重测同一批节点。这依赖 axe 结果中的target属性:

// 从 color-contrast 违规中取出 target 属性: const violation = priorResult.violations.find( ({ id }) => id === 'color-contrast' ); const targets = violation.nodes.map(issue => issue.target); // 只测试先前 color-contrast 违规的节点: await axe.run(targets); // 或者从本次测试中排除这些节点: await axe.run({ exclude: targets });

重要限制:不能把单个target属性直接传给 axe,必须包裹在数组中。这是因为 axe-core 的target属性本身就是数组(例如['#blog-comments', ['#userComments', '.commentBody']]),详见下文"隐式 frame 与 shadow DOM 选择"一节。


六、限制帧测试:fromFrames选择器对象

在 iframe 内包含/排除特定区块,需要使用fromFrames选择器对象:其属性值为选择器数组,第一个选择器选中 frame 元素,最后一个选择器选中要包含/排除的目标元素。下面的例子测试所有#paymentFrameframe/iframe 内的form元素:

// 测试每个 #paymentFrame frame 或 iframe 内的每个 <form>: axe.run({ fromFrames: ['#paymentFrame', 'form'] });

嵌套帧

由于 axe 会递归测试帧,嵌套多少层就需要多少级选择器。下面的例子测试#outeriframe 内的#inneriframe 内的form:

// 测试 #outer 内、#inner 内、以及其中的 <form>: axe.run({ fromFrames: ['iframe#outer', 'iframe#inner', 'form'] });

在 include / exclude 中组合 fromFrames

fromFrames对象可以作为exclude或include的值,可以单独出现,也可以与其他选择器一起放在数组中。下面的例子同时排除顶层窗口的.ad-banner和第一层 iframe 内的.ad-banner:

// 跳过所有 .ad-banner,以及 iframe 内的 .ad-banner: axe.run({ exclude: [ '.ad-banner', { fromFrames: ['iframe', '.ad-banner'] } ] });

fromFrames同样可以同时用于include和exclude。下面的例子测试#paymentiframe 内的form,但排除该form内的.ad-banner:

axe.run({ include: { fromFrames: ['iframe#payment', 'form'] }, exclude: { fromFrames: ['iframe#payment', 'form > .ad-banner'] } });

注意:fromFrames属性不能与include/exclude放在同一个对象上。源码 normalize-context.js 对此有显式断言:fromFrames必须用在 include 或 exclude 内部,否则抛出Invalid context; fromFrames must be used inside include or exclude...错误;test/core/base/context.js 对fromFrames、fromShadowDom与 include/exclude 同层的非法写法均有对应的抛错测试。


七、限制 Shadow DOM 测试:fromShadowDom选择器对象

包含/排除 Shadow DOM 树中的特定区块,使用fromShadowDom选择器对象。它与fromFrames工作方式类似:属性值为字符串数组,第一个选择器选中 shadow DOM host 元素,最后一个选择器选中目标元素。下面的例子测试.app-header元素所挂 shadow DOM 树中的#search表单:

// 测试每个 <app-header> shadow DOM 树中的每个搜索表单: axe.run({ fromShadowDom: ['.app-header', 'form#search'] });

嵌套 Shadow DOM

选择嵌套 shadow DOM 树中的元素时,每层嵌套都需要一个选择器。下面的例子测试app-root自定义元素 shadow DOM 树内、.header元素 shadow DOM 树中的#search元素:

// 测试每个 <app-root> 内、每个 .header 内的 #search: axe.run({ fromShadowDom: ['app-root', '.header', '#search'] });

Light DOM 与 slot 的特殊规则

要选择嵌套 shadow DOM 树中位于 light DOM 的元素(例如使用了<slot>元素时),选择器应当解析到该元素在light DOM 中的位置,而非其在嵌套 shadow DOM 树中的最终渲染位置。例如要选中下面 DOM 结构中的deeply-nested-element:

custom-element #shadow-root nested-element #shadow-root <slot> deeply-nested-element #shadow-root <slot> button#target

正确的选择器是['custom-element', 'deeply-nested-element'],而不是['custom-element', 'nested-element', 'deeply-nested-element']——因为deeply-nested-element位于custom-element的 light DOM 中(它是nested-element的内容投影,而非nested-element的子树)。

在 include / exclude 中组合 fromShadowDom

fromShadowDom对象可以作为exclude或include的值,单独使用或与其他选择器组成数组。下面的例子排除footer,以及<blog-comments>自定义元素 shadow DOM 树内的所有.comment元素:

// 跳过 footer,以及 <blog-comments> shadow DOM 树内的所有 .comment 元素: axe.run({ exclude: [ 'footer', { fromShadowDom: ['blog-comments', '.comment'] } ] });

同样,fromShadowDom可以同时用于include和exclude。下面的例子测试#root元素 shadow DOM 内的<app-footer>自定义组件,但排除<app-footer>自身 shadow DOM 树内的.ad-banner:

axe.run({ include: { fromShadowDom: ['#root', 'app-footer'] }, exclude: { fromShadowDom: ['#root', 'app-footer', '.ad-banner'] } });

注意:与fromFrames相同,fromShadowDom不能与include/exclude放在同一个对象上。

Slotted 元素

axe 基于扁平化 DOM 树(flattened DOM tree)判断元素的包含/排除。因此当一个 shadow DOM 节点被选中时,所有通过<slot>元素插入的后代节点也会一并被选中。这与 test/core/base/context.js 中"light DOM 选择不会匹配 shadow DOM 节点"的测试互为印证:扁平树决定了实际匹配集合。


八、组合 Shadow DOM 与 Frame Context

fromShadowDom可以作为选择器嵌入fromFrames数组中,从而选中"位于 shadow DOM 树内的 iframe"或"位于 iframe 内的 shadow DOM 树"。下面的例子测试#appRootshadow DOM 树内每个iframe中的main元素:

await axe.run({ fromFrames: [ { fromShadowDom: ['#appRoot', 'iframe'] }, 'main' ] });

这些选择器同样可以用于include或exclude。下面的例子排除footer,以及#blog-commentsiframe 内、#userCommentsshadow DOM 树中的所有.commentBody元素:

await axe.run({ exclude: [ 'footer', { fromFrames: [ 'iframe#blog-comments', { fromShadowDom: ['#userComments', '.commentBody'] } ] } ] });

注意(方向性限制):即使 iframe 位于 shadow DOM 树内部,fromShadowDom选择器对象必须作为fromFrames的组成部分出现;反过来(在fromShadowDom内部嵌套fromFrames)不生效且会报错。这一点在源码 normalize-context.js 中有对应断言:shadow selector must be inside fromFrame instead。


九、隐式 Frame 与 Shadow DOM 选择:target与嵌套数组

在 axe 早期版本中,嵌套数组是包含/排除 iframe 内元素的唯一方式,至今仍被支持,也是 axe 内部的实际工作方式。这种嵌套数组语法体现在target属性中。例如,#blog-commentsiframe 内、#userCommentsshadow DOM 树中的.commentBody元素,其 target 可能是:

result = await axe.run(); result.violations[0].nodes[0].target; // ['#blog-comments', ['#userComments', '.commentBody']]

把target传入 axe 时必须再包裹一层数组,形成三层嵌套结构:最外层数组用于承载多个选择器,中间层数组用于选入帧,内层数组(可选)用于选择 shadow DOM 树内的元素。

虽然这种语法仍然受支持,但官方建议改用fromFrames与fromShadowDom对象选择器:语义更清晰,且无需为了包一层空数组而嵌套多余的层级。从源码看,normalizeContext会把带fromFrames/fromShadowDom键的对象解包为内部的标准嵌套数组(见 normalize-context.js),parseSelectorArray再对帧选择器逐级解析并推入 frame 上下文(见 parse-selector-array.js)——这也是"对象语法与嵌套数组语法等价"的底层原因。


十、底层原理:context 的归一化、解析与判定链路

为了让上述所有语法生效,axe-core 构建了一条清晰的内部处理链,从axe.run到最终判定依次经过:

  1. 参数归一化:normalize-run-params.js 判定第一个参数是否为合法 context,非法时回退到document;
  2. 上下文归一化:normalize-context.js 将"裸选择器 / 节点 / 数组"包装成{ include, exclude },把fromFrames、fromShadowDom解包为标准选择器数组,并对非法组合(如 fromFrames 与 include 同层、fromShadowDom 嵌套 fromFrames)抛出带doc/context.md指引的断言错误;
  3. 选择器解析:parse-selector-array.js 将归一化后的选择器数组逐级解析为扁平树节点,并把命中 iframe 的选择器递归分配到对应的frameContext;
  4. 包含/排除裁决:is-node-in-context.js 基于"最深 include 与最深 exclude 的包含关系"决定每个节点最终是否进入测试范围;
  5. 帧上下文收集:get-frame-contexts.js 在运行部分测试时(axe.runPartial)提取{ frameSelector, frameContext }列表,并通过options.iframes === false关闭帧测试。

核心的Context类在 lib/core/base/context.js 中完成组装:克隆 spec、归一化、解析 include/exclude、收集可见 iframe、判断是否为整页上下文(include仅含document.documentElement时视为整页),并按文档顺序对 include 节点排序。

测试层面,test/core/base/context.js 以 1100 余行的覆盖验证了各类输入形态:单选择器、帧选择器、字符串数组、节点引用、NodeList、jQuery 类数组对象、ShadowRoot 引用、混合数组,以及"无匹配时报错"(No elements found for include in page Context,对应 context.js 的校验逻辑)。


结语:选择最适合你场景的 context 写法

场景推荐写法
只测页面某几个区块axe.run(['nav', '.sideBar', '#header'])
测某区块但跳过其中部分axe.run({ include: [...], exclude: '...' })
测已渲染的组件/DOM 节点axe.run(appRoot)或axe.run(nodes)
进入 iframe 内测试axe.run({ fromFrames: ['iframe#payment', 'form'] })
进入 Shadow DOM 内测试axe.run({ fromShadowDom: ['.app-header', 'form#search'] })
帧内套 Shadow DOMaxe.run({ fromFrames: [{ fromShadowDom: [...] }, 'main'] })
复用上次测试的违规节点axe.run(priorViolationNodes.map(n => n.target))

掌握 context 参数,意味着你可以把每次无障碍扫描的资源精确投放到自己负责的代码区域,规避第三方内容与未完成模块的干扰,也能在修复阶段对同一批节点反复回归。结合 API 文档 中的 context 章节与运行参数(runOnly、rules等)组合使用,即可搭建一套覆盖单元、集成与端到端三层的精准无障碍测试体系。

  • 测试

【免费下载链接】axe-core

Accessibility engine for automated Web UI testing

项目地址:https://gitcode.com/gh_mirrors/ax/axe-core
点击查看免费下载

相关推荐

上一篇:MGV2000-CW 刷Armbian改服务器教程:创维IPTV盒子变身7×24小时小主机
下一篇:MM-Pose 全身关键点估计实战:基于 ResNet 的 Top-Down 热图方法在 COCO-WholeBody 上的配置与基准

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询