Playwright ARIA 快照怎么读:YAML 结构与截图快照的边界

2026-08-18

写页面结构的回归测试,多数人第一反应是截图。截图有个绕不开的麻烦:基线文件是按浏览器和平台分开存的,本机改一版、CI 上再改一版,最后没人敢按更新键。ARIA 快照走的是另一条路——它把页面的无障碍树序列化成 YAML,断言的是结构而不是像素。

这篇不铺 API 目录,就跟着一次 toMatchAriaSnapshot 走完:模板里那几行 YAML 是怎么跟页面对上的,中间经过了哪些函数。

一个节点只有三段

docs/src/aria-snapshots.md 把节点格式写得很干脆:

- role "name" [attribute=value]

三段各有出处。role 是元素的 ARIA 或 HTML role;"name" 是 accessible name,加引号表示精确匹配,写成 /pattern/ 就按正则匹配;方括号里是 ARIA 属性与取值,文档列举了 checkeddisabledexpandedinvalidlevelpressedselected 这几个。缩进表示嵌套关系。

所以下面这段 HTML 与这段快照是对应的(例子原样抄自该文档):

<h1>Title</h1>
<h2>Subtitle</h2>
- heading "Title" [level=1]
- heading "Subtitle" [level=2]

role 与 name 是算出来的,不是读属性读出来的

真正决定快照长相的代码在 packages/injected/src/ariaSnapshot.ts。生成树的函数是 generateAriaTree,每个元素转成节点时做两件事:

const role = roleUtils.getAriaRole(element) ?? defaultRole;
if (!role || role === 'presentation' || role === 'none')
  return null;

const name = roleUtils.getElementAccessibleName(element, false);

第一件事解释了一个常见困惑:为什么有些 DOM 节点在快照里根本找不到。getAriaRole 定义在 packages/injected/src/roleUtils.ts,内部的 computeAriaRole 先取显式 role(getExplicitAriaRole),没有才回落到隐式 role(getImplicitAriaRole)。而一旦算出来是 presentationnoneariaSnapshot.ts 这里直接返回 null,这个节点不进树。你在快照里看不到它,不是没抓到,是它按规则被排除了。

第二件事是名称。getElementAccessibleName 走的是 computeAccessibleNameComposite,源码里有一段值得留意:如果元素的 role 落在一份「禁止命名」的角色列表里(源码中的 elementProhibitsNaming 判断,包含 genericparagraphcodeemphasisstrongtime 等),直接返回空名称。也就是说,给一个 <p>aria-label 并指望它出现在快照里,方向就错了。相邻的 getElementAccessibleDescription 里还留着 precedence 1 / precedence 2 / precedence 4 的注释,以及一条 TODO: handle precedence 3——这类 html-aam 的特例并没有全部实现,源码自己标出来了。

名称写进节点前还过一道 normalizeWhiteSpace。这和文档那句「比较会折叠空白」是同一件事的两端:一端在生成时归一化,另一端在比较时忽略缩进与换行。

方括号里的属性也不是无差别附加的。源码用 kAriaCheckedRoleskAriaDisabledRoleskAriaExpandedRoleskAriaInvalidRoleskAriaLevelRoleskAriaPressedRoleskAriaSelectedRoles 这几组角色白名单判断,角色不在对应白名单里,属性就不会出现。invalid 还多一层转换:'false' 转成 false'true' 转成 true、其它字符串原样保留——这正好对上文档里 aria-invalid="spelling" 渲染成 [invalid=spelling]、值为 false 时整个属性被省略的写法。

模板那一侧:以斜杠开头的键是特殊键

页面这边生成一棵树,模板这边也要解析成一棵树,解析器在 packages/isomorphic/ariaSnapshot.ts。它把 YAML 的键分成几类处理:

  • text 键:值必须是字符串,转成一个文本子节点;
  • /children 键:值只允许 containequaldeep-equal 三个之一,否则报错 Strict value should be "contain", "equal" or "deep-equal",命中后写进 containerMode
  • 其它以 / 开头的键:值必须是字符串,去掉前导斜杠后存进 props/url 就是走这条路径进去的;
  • 剩下的按 role "name" 解析成子节点。

看懂这个分类,docs/src/aria-snapshots.md 里链接那段写法就不神秘了:

- link "Read more about Accessibility":
    - /url: "#more-info"

/url 不是 link 的子节点,它是挂在这个节点上的属性。

/children 的三个取值决定子元素怎么比:contain 是默认,只要模板里列出的子项都在、顺序对就算过;equal 要求子项与模板完全一致且顺序一致;deep-equal 再加上嵌套层。文档明说默认行为是「包含子集即匹配」,所以省略属性、省略名称、省略某些列表项做部分匹配,是默认就成立的,不需要打开什么开关。

全局改这个默认值可以写在配置里:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  expect: {
    toMatchAriaSnapshot: {
      children: 'equal',
    },
  },
});

有意思的是它的实现。packages/playwright/src/matchers/toMatchAriaSnapshot.ts 里,全局配置并不是走一条独立的比较分支,而是在模板文本里没匹配到 ^- \/children: 时,直接往模板字符串最前面拼一行 - /children: <值>。文档说「单个快照可以用显式 /children 覆盖全局设置」,看到这段拼接就知道覆盖是怎么发生的:模板里已经有这行,就不再拼。

生成与更新基线

三种生成路径,文档都写明了。第一种是给断言传空模板,让它当场生成:

await expect(locator).toMatchAriaSnapshot('');

第二种是 @playwright/test--update-snapshots(简写 -u),只更新没匹配上的快照,已匹配的不动:

npx playwright test --update-snapshots

这一节在文档里标了 * langs: js,即只对 JS 的 test runner 成立。更新时源码怎么改,由 --update-source-method 控制,文档给了三个取值:patch(默认,生成可用 git apply 应用的 unified diff)、3way(在源码里生成合并冲突标记)、overwrite(直接覆写)。这里的「默认是 patch」是仓库当前文档里的默认值,随版本可能变动。

npx playwright test --update-snapshots --update-source-method=3way

第三种是把快照存成独立文件,用 name 选项指定 .aria.yml 扩展名:

await expect(page.getByRole('main')).toMatchAriaSnapshot({ name: 'main.aria.yml' });

matcher 源码里还留着一条兼容处理,注释逐字写的是「in 1.51, we changed the default template to use .aria.yml extension」,所以它会先查新路径、不存在再回落查旧的 .yml 路径。另外,生成基线时它把待比较值换成常量 kImpossibleAriaMatch(源码里定义为 - none "Generating new baseline"),让整条管线照常跑一遍必然不匹配的比较。文档这一侧的说法可以对着看:Playwright 会等到配置的最大 expect 超时以确保页面稳定后再取快照,必要时需要调整 --timeout

和截图快照的分界

docs/src/test-snapshots-js.md 讲的是 toHaveScreenshot() 那条线,两个细节值得对着看。

一是基线文件名。截图基线形如 example-test-1-chromium-darwin.png,后半段是浏览器名与平台;文档明说不同浏览器与平台的渲染、字体等不同,需要各自的基线。这意味着 Windows 上生成的截图基线和 Linux CI 上跑出来的不是同一个文件。文档开头还挂了一条警告,列出渲染结果可能受宿主操作系统、版本、设置、硬件、供电方式(电池还是电源适配器)、headless 模式等因素影响,建议在生成基线的同一环境里跑。

二是 ARIA 快照这边的说法正好相反:docs/src/aria-snapshots.md 写明「快照在各浏览器间应当是一致的,因此即使用多个浏览器测试也只保存一份快照」。默认存放目录是测试文件同名的 example.spec.ts-snapshots,路径模板可以用 snapshotPathTemplateexpect.toMatchAriaSnapshot.pathTemplate 改。

由此,选哪种其实不用纠结:要验的是「结构还在不在、层级和顺序有没有乱、控件状态对不对」,用 ARIA 快照;要验的是「像素级外观有没有变」,那只能截图,并接受它跨平台要分别维护基线。文档在「何时使用」一节给的划分是:快照测试适合整页与组件的结构性检查、结构很少变的回归;断言测试适合核心逻辑、计算值和需要精确条件的细粒度检查。它同时列了快照测试的短处,其中一条是「容易在没完全理解差异的情况下就接受快照变更,从而掩盖 bug」。

比较语义还有两条容易踩:比较区分大小写、折叠空白,缩进和换行被忽略;比较对顺序敏感,模板里元素的顺序必须和页面无障碍树里的顺序一致。

语言绑定不要串

字符串模板形式的断言四种绑定都有,docs/src/api/class-locatorassertions.mdLocatorAssertions.toMatchAriaSnapshot 标了 since: v1.49,Java 绑定的别名是 matchesAriaSnapshot;直接对 page 断言的 PageAssertions.toMatchAriaSnapshot 标的是 since: v1.60。对着看还有一处细节:class-locatorassertions.md 里这个方法的示例写的是 expect(page.locator('body')).toMatchAriaSnapshot(...),走的是 locator 而不是直接对 page 断言。

而存成独立文件的那个重载 LocatorAssertions.toMatchAriaSnapshot#2,文档明确标了 * langs: js——Python、Java、C# 绑定这边没有这个形态,别照着 JS 的写法找对应方法。--update-snapshots 那一节同样标了 langs: js

程序化取快照的 Locator.ariaSnapshot() 四种绑定都有(Python 是 aria_snapshot(),C# 是 AriaSnapshotAsync()),返回 YAML 字符串。它还有几个后来加的选项:mode"ai" 时会带上 [ref=e2] 这样的元素引用、不再等待元素匹配(无匹配直接抛错)、并包含目标内 <iframe> 的快照;depth 限制层数;boxestrue 时给每个元素追加 [box=x,y,width,height],坐标相对视口、单位是 CSS 像素,默认 false(这是仓库当前文档里的默认值,随版本可能变动)。另有 Locator.ariaSnapshotJSON(),返回同一棵树的 JSON 形式,节点带 rolenametextchildren 以及状态字段——这个方法在文档里也标了 langs: js

需要按角色和名称反查页面时,配套的入口是 getByRoledocs/src/locators.md 提醒了一句:role 定位器不能替代无障碍审计与合规测试,它只是就 ARIA 指南给出早期反馈。同一句话套在 ARIA 快照上也成立——它是结构回归的手段,不是无障碍达标的证明。

想看清一棵树到底长什么样,文档推荐的办法是用 Chrome DevTools 的 Accessibility 面板;codegen 里也有「Assert snapshot」动作和「Aria snapshot」标签页,可以对选中的 locator 直接看它的快照。

上面配置块与断言写法均原样取自仓库文档。以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。


本文依据 github.com/microsoft/playwright 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的文档与源码。我们没有对文中涉及的功能做过实测, 因此不涉及运行速度、稳定性与实际表现的任何描述。 该项目迭代频繁,文中涉及的 API 签名、配置项与默认值随版本变动,请以仓库最新内容为准。 本文不涉及浏览器版本矩阵与版本清单,相关信息请以官方发布说明为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。