可访问性(a11y)和多语言(i18n)是自动化体系里最容易被排到最后的两块——“我们用户又不用读屏”“我们只面向中文”。但事实是:a11y 问题 80% 同时也是普通用户的问题(焦点管理坏的页面,键盘用户和触屏用户都遭殃),而"只面向中文"的系统,文案写死在代码里,出海那天全都要重写。这篇讲怎么把这两块变成可自动化的常规测试。
一、可访问性:从"合规项"到"质量项"
1. 核心问题清单(按发生频率排序)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 1. 焦点管理(40%) - 弹窗打开后焦点没进去(Tab 还在底层页面) - 弹窗关闭后焦点没回来 - 单页应用路由切换后焦点回到 <body>(读屏用户"迷失") 2. 可访问名称缺失(25%) - 纯图标按钮没有 aria-label("这个按钮是干嘛的?") - 图片没有 alt / 装饰图没有 alt="" 3. 对比度不足(15%) - 灰字灰底(WCAG AA 要求正文 4.5:1) - 深色模式下的"设计稿看着行" 4. 语义结构(10%) - 用 <div> 假装按钮(点不了、读不了) - 标题层级跳级(h1 → h3) 5. 动效(10%) - 闪烁内容(光敏癫痫风险) - 没尊重 prefers-reduced-motion
|
2. 自动化:axe-core 进回归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| from playwright_axe import Ax
@pytest.fixture def axe(page): yield Ax(page)
@pytest.mark.a11y def test_login_page_a11y(page, axe): page.goto("/login") results = axe.run() critical = [v for v in results.violations if v.impact in ("critical", "serious")] assert not critical, format_violations(critical)
|
落地策略:
- 门禁只卡 critical/serious(焦点陷阱、不可名按钮这类"功能级"问题),minor 进治理看板——一上来卡全部规则,报告红成一片,没人看;
- 每条 UI 用例自动带 a11y 扫描(不只"专门的 a11y 用例")——页面在用例里被真实操作过,扫描的是"交互后的状态"(弹窗打开时、表单报错时),比单独访问页面扫得多;
- 基线豁免(allowlist):历史问题登记进基线(带 issue 号 + 期限),基线只能减不能增——新页面必须 a11y 干净,老页面逐步消化。
3. 键盘走查:自动化测不到的 20%
axe 扫不出"交互逻辑"问题,键盘走查脚本补位:
1 2 3 4 5 6 7 8 9 10 11 12
| def test_modal_focus_trap(page): page.goto("/order") page.get_by_role("button", name="详情").click() assert page.evaluate("document.activeElement.closest('[role=dialog]')") is not None for _ in range(20): page.keyboard.press("Tab") assert in_dialog(page) page.keyboard.press("Escape") assert page.evaluate("document.activeElement.textContent") == "详情"
|
二、多语言:把"文案"从代码里赶出去
1. i18n 问题清单
1 2 3 4 5 6 7 8 9 10 11 12
| 1. 文案硬编码(60%) "订单已取消" 写死在组件里 → 翻译无法生效 检测:i18n lint —— 源码中的中文字符串必须走 t("key") 2. 断言写死文案(25%) assert text == "提交成功" → 切英文必挂 解法:断言用 data-testid / role,不用文案;必须断言文案时从 i18n 资源取期望值 3. 布局溢出(10%) 中文 4 字的按钮,德语 12 字符 → 截断/换行/溢出 解法:长文案压测(取各语言 Top 长度文案渲染截图对比) 4. 数字/日期/货币格式(5%) 1,000.00 vs 1.000,00 vs 1 000.00 解法:ICU 格式化 + 各 locale 快照
|
2. 自动化三件套
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
@pytest.mark.parametrize("locale", ["zh-CN", "en-US", "ja-JP", "de-DE"]) def test_core_flow_multilang(page, locale): page.set_default_locale(locale) page.goto("/order") assert page.get_by_role("button", name=expected_name("submit", locale)).is_visible() for el in page.locator("[data-i18n]"): assert el.evaluate("el => el.scrollWidth <= el.clientWidth + 2")
|
语言矩阵的取舍:不是所有语言都全量跑——en-US 全量(出海主市场),ja-JP/de-DE/... 跑核心流程 + 截图对比。语言是数据,不是环境:同一个 UI 套件 × N 个 locale 参数,成本是线性的。
3. 深色模式 × 语言:组合爆炸的正确处理
深色/浅色 × 中/英/日 = 6 个视觉态。策略:
- 截图对比只在"发版前"跑全组合(6 态 × 核心 10 页面 = 60 张,可接受);
- 日常 CI 只跑默认态(浅色中文)——组合态的日常回归性价比太低;
- 用例前置固定系统态(主题/语言写进 fixture),杜绝"用例依赖本机设置"(这是 flaky 的暗源之一)。
三、和体系的其他部分怎么接
1 2 3 4 5
| a11y/i18n 在测试体系中的位置: - 用例设计:可访问性是"环境维度"的一部分(键盘用户 = 一类"设备") - 风险评级:a11y critical 缺陷 = 功能级缺陷(不是体验建议),按 P1 走 - 缺陷管理:a11y 问题单带"影响人群"字段(读屏用户/键盘用户/弱视用户) - 度量:a11y 基线收敛速度 + i18n 覆盖语言数,进质量看板
|
四、小结
两块"被忽略的体验"的落地路径:a11y = axe 门禁(critical 为 0)+ 焦点走查脚本 + 基线只减不增;i18n = lint 赶文案 + role 化断言 + 语言矩阵参数化 + 发版前全组合截图。核心认知:a11y 问题 80% 是普通用户问题(修它就是修质量),i18n 债务拖到出海那天是重写成本(修它就是修架构)。