Skip to content

第 20 章 · 无障碍测试与 Aria Snapshot

本章目标:理解 aria snapshot 的数据来源与 YAML 语法,学会用 to_match_aria_snapshot() 断言页面的可访问结构,并把无障碍检查纳入日常测试与 CI。

20.1 无障碍测试为什么值得自动化

无障碍(Accessibility,a11y)不是"锦上添花":它直接影响屏幕阅读器用户能否使用产品,也在越来越多地区成为合规要求。更重要的是,对机器友好的页面往往对人也更友好——一个没有正确 role 和可访问名称的按钮,对测试自动化同样是灾难。

传统 a11y 审计工具(如 axe)扫描的是"有没有违规";Playwright 提供了另一个互补的视角:把页面的可访问性树快照下来,当作结构契约来断言——语义结构错了,测试就红。

20.2 认识 Aria Snapshot:YAML 形式的可访问树

Aria snapshot 是 Playwright 对元素/页面可访问性树的 YAML 表示,自 v1.49 起可用。每个节点描述一个可访问元素:

text
- role "name" [attribute=value]
  • role:ARIA 或 HTML 隐含角色,如 headingbuttonlistitem
  • "name":可访问名称(精确字符串;/正则/ 表示模式匹配);
  • [attribute=value]checkeddisabledexpandedlevelpressedselected 等状态属性。

用代码直接查看当前页面的快照:

python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://playwright.dev/python/docs/intro")
    # 打印整个侧边栏的可访问结构(YAML 字符串)
    print(page.get_by_role("navigation").aria_snapshot())
    browser.close()

输出形如:

yaml
- navigation "Docs":
  - list:
    - listitem:
      - link "Intro"
    - listitem:
      - link "Writing tests"

缩进表达层级嵌套——这就是屏幕阅读器"看到"的世界。

20.3 to_match_aria_snapshot():结构断言

核心断言是 expect(page).to_match_aria_snapshot()(整页)或 expect(locator).to_match_aria_snapshot()(局部区域):

python
from playwright.sync_api import expect

def test_heading_structure(page):
    page.goto("https://example.com")

    # 整页断言:标题层级正确
    expect(page).to_match_aria_snapshot("""
      - heading "Example Domain"
      - paragraph
    """)

def test_nav_structure(page):
    page.goto("https://example.com")

    # 局部断言:只关心导航区,带 [level] 属性校验
    expect(page.get_by_role("contentinfo")).to_match_aria_snapshot("""
      - contentinfo:
        - link /更多/
    """)

匹配规则有四个要点,全部来自官方文档:

  1. 大小写敏感,但空白会被折叠(缩进和换行不影响比较);
  2. 顺序敏感:模板中节点的顺序必须与实际树一致;
  3. 支持部分匹配:省略名称或属性即可只校验关心的部分;
  4. /pattern/ 形式支持正则名称,适合动态文本。

20.4 用 level 属性守住文档大纲

heading 层级混乱是最常见的 a11y 问题之一(比如为了视觉效果从 h1 直接跳到 h4)。用 [level=N] 可以把它变成 CI 里的一条硬约束:

python
def test_article_outline(page):
    page.goto("https://blog.example.com/post/1")

    # 文章主体的大纲必须是 h2 → h3 两级结构
    expect(page.get_by_role("article")).to_match_aria_snapshot("""
      - article:
        - heading "概述" [level=2]
        - paragraph
        - heading "安装步骤" [level=2]
        - list:
          - listitem: 安装 Python
          - listitem: 安装依赖
        - heading "常见问题" [level=3]
    """)

一旦有人为了样式把 h3 改成 div class="title" 或跳级到 h4,这条测试会立刻失败并给出 diff。

20.5 与 axe 配合:结构断言 + 违规扫描

to_match_aria_snapshot 擅长守护"结构意图",但不检查对比度、重复 id 等规则类问题——这正是 axe-core 的强项。两者组合才是完整的 a11y 策略:

bash
pip install pytest-playwright axe-playwright-python
python
from axe_playwright_python.sync_playwright import Axe

def test_no_axe_violations(page):
    page.goto("https://example.com")
    results = Axe().run(page)

    # 规则扫描:严重违规必须为零
    assert results.violations_count(0) == 0, results.report()

分工建议:

工具擅长失败时的产物
to_match_aria_snapshot页面语义结构是否符合设计意图YAML diff,直观展示结构差异
axe 扫描规则库类违规(对比度、标签缺失等)违规清单及修复建议

20.6 纳入 CI 的策略

a11y 测试全量跑会很吵,建议分级:

  1. 关键页面结构断言(首页、表单、导航):随每次 PR 跑,用本章的 snapshot 断言;
  2. axe 全站扫描:每日定时任务跑一次,产出趋势报告而非阻塞 PR;
  3. 失败处理约定:snapshot diff 属于"破坏性变更",必须在 PR 里显式更新模板并说明理由——这本身就是一次 a11y review。

20.7 本章小结

  • Aria snapshot 是可访问性树的 YAML 表示,v1.49+ 提供 locator.aria_snapshot
  • 节点语法 - role "name" [attr=value],支持正则名称与部分匹配;
  • expect(locator).to_match_aria_snapshot() 大小写/顺序敏感、空白折叠;
  • [level=N] 守护 heading 大纲是最实用的落地场景;
  • 结构断言(Playwright)+ 规则扫描(axe)互补,CI 中分级执行。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. Aria snapshot 中节点 `- heading "标题" [level=3]` 的含义是?

2. 关于 to_match_aria_snapshot() 的匹配规则,错误的是?

3. locator.aria_snapshot 的作用是?

4. 关于 Playwright 结构断言与 axe 扫描的分工,正确的是?

🛠️ 动手实践

  1. 给你的项目首页写一条整页 to_match_aria_snapshot 断言,先跑一遍拿到真实 YAML,再裁剪成只保留关键区域的"部分匹配"版本。
  2. 在团队博客或文档站上找出一个 heading 跳级问题,写一条 [level=N] 断言证明它的存在,然后修复前端代码让测试变绿。
  3. 安装 axe-playwright-python,对登录页做一次扫描,把 severity 最高的违规项整理成 issue(附复现步骤与修复建议)。

下一章进入实时应用的测试世界:第 21 章 · WebSocket 与实时应用测试