CKEditor 4 菜单按钮无障碍(ARIA)折叠状态测试指南:从手工验证到屏幕阅读器播报
CKEditor 4 菜单按钮无障碍(ARIA)折叠状态测试指南:从手工验证到屏幕阅读器播报
导读
本文围绕 CKEditor 4 中菜单按钮(menu button)与面板按钮(panel button)的 aria-expanded 状态在无障碍访问树(accessibility tree)中的表现展开,详细解读仓库内的屏幕阅读器手工测试用例 menubuttonscreenreader.md,并结合 button、menubutton、panelbutton 三个核心插件的源码与自动化测试,说明该状态的底层实现原理、测试步骤、预期结果,以及 VoiceOver、NVDA、JAWS 等主流屏幕阅读器的实际播报差异。读完本文,你将能复现该手工测试、理解 aria-expanded 的取值时机,并掌握用 Bender 手工测试体系验证无障碍行为的完整方法。
背景:菜单按钮的“展开/收起”状态为何需要被声明
CKEditor 4 工具栏中有一类特殊按钮:点击后并不会直接执行命令,而是弹出一个菜单或面板,例如“语言”(Language)菜单按钮和“文本颜色”(Text Color)面板按钮。这类按钮在 WAI-ARIA 中被称为可展开按钮(expandable button),它必须向辅助技术(assistive technology,AT)声明自己的弹出状态,用户才能通过屏幕阅读器感知“菜单已打开 / 已关闭”。
在 CKEditor 4.19.1 之前,菜单按钮和面板按钮的弹出状态在浏览器的 accessibility tree 中表示有误——这是仓库 CHANGES.md 中记录的缺陷 #5144:
Fixed: Menu buttons and panel buttons incorrectly indicate the open status of their associated pop-up menus in the browser's accessibility tree.
该缺陷的修复涉及三个层面:
- 默认值:带下拉箭头的按钮在渲染时默认携带
aria-expanded="false"; - 动态更新:菜单/面板打开时置为
"true",关闭时恢复为"false"; - 回归验证:通过手工测试(manual test)与自动化测试双重把关。
本文要解读的 menubuttonscreenreader.md 正是为验证该修复而编写的一份面向屏幕阅读器的手工测试用例。
测试用例全景:menubuttonscreenreader
测试元数据与运行前提
该测试文件头部是 Bender 测试框架的标签声明:
@bender-tags: 4.19.1, 5144, bug, button
@bender-ui: collapsed
| 标签 | 含义 |
|---|---|
4.19.1 |
该测试自 CKEditor 4.19.1 起生效(即缺陷 #5144 修复所在的版本) |
5144 |
关联的 GitHub issue 编号,即“菜单/面板按钮在 accessibility tree 中错误表示弹出状态” |
bug |
测试类型为缺陷回归测试 |
button |
涉及的功能域为按钮插件 |
@bender-ui: collapsed |
测试页面 UI 默认折叠展示 |
文档开头还特别注明:
Note This test is intended for screen reader.
即该测试必须借助屏幕阅读器(如 VoiceOver、NVDA、JAWS)才能完整验证,因为它检查的是 AT 的播报输出,而非单纯的 DOM 属性值。测试中还使用了 bender.tools.env.mobile 检测:在移动设备上直接 bender.ignore() 跳过该测试(见 menubuttonscreenreader.html),因为屏幕阅读器验证主要面向桌面端 AT。
测试页面结构
对应页面 menubuttonscreenreader.html 创建了一个包含示例文本的编辑器实例,并只加载与测试相关的插件:
CKEDITOR.replace( 'editor', {
plugins: [
'toolbar',
'wysiwygarea',
'language',
'colorbutton'
]
} );
其中:
language插件注册了一个菜单按钮(CKEDITOR.UI_MENUBUTTON);colorbutton插件注册了文本颜色与背景颜色两个面板按钮(CKEDITOR.UI_PANELBUTTON);toolbar与wysiwygarea提供工具栏容器与编辑区。
这样最小化的插件组合,恰好覆盖了菜单按钮与面板按钮两类可展开按钮,是验证 #5144 修复的典型场景。
测试步骤与预期结果逐条解读
步骤 1:选中文本
- Select some text in the editor.
在编辑器中选择一段文本(如测试页面中默认填充的 Lorem ipsum 段落),为后续通过按钮对选区应用颜色或语言标记做准备。
步骤 2:将焦点移动到工具栏的第一个按钮
- Move focus to the first button in the editor's toolbar.
Expected Screen reader announces the button and optionally informs it's collapsed.
预期:屏幕阅读器播报该按钮的名称,并且可选地提示当前处于折叠(collapsed)状态。
这里的关键词是“optionally”——是否播报“collapsed”取决于具体屏幕阅读器对 aria-expanded="false" 的处理策略:有些 AT 会播报折叠状态,有些则保持沉默。这正是本文末尾“各屏幕阅读器播报样本”一节存在的原因。
步骤 3:对选中文本应用颜色或语言
- Apply color or language to the selected text.
通过键盘激活工具栏按钮(如 Language 按钮)打开菜单,为选中的文本应用语言或颜色标记。注意:此步骤是通过菜单按钮的弹出菜单完成的,菜单在此过程中会经历“打开→选择→关闭”的完整生命周期。
步骤 4:焦点移回按钮,验证状态
- Move the focus back to the button.
Expected Screen reader announces the button and optionally informs it's collapsed.
Unexpected Screen reader announces the button as expanded.
这是本测试的核心断言:
- 预期:菜单已经关闭,屏幕阅读器播报按钮时不应提示“展开/expanded”,最多提示“折叠/collapsed”;
- 意外(缺陷):屏幕阅读器把按钮播报为已展开(expanded)——这正是 #5144 修复前在 accessibility tree 中的错误表现:菜单明明已关闭,
aria-expanded却残留为true,导致 AT 播报“expanded”。
步骤 5:对另一个按钮重复验证
- Repeat it for the other button.
对工具栏中另一个可展开按钮(如 Text Color 面板按钮)重复步骤 2–4,确保菜单按钮与面板按钮两类组件都符合预期。
各屏幕阅读器播报样本:三大 AT 的输出对照
原文档给出了三种主流屏幕阅读器的播报样本(<button name> 为按钮名称占位符):
VoiceOver(macOS 内置)
<button name>, menu pop-up collapsed, button
VoiceOver 将按钮描述为“menu pop-up”(菜单弹出项),并在收起时附加 "collapsed",最后标注元素角色 "button"。
NVDA(Windows,开源)
<button name>, button, collapsed, submenu
NVDA 的播报顺序略有不同:先报角色 "button",再报 "collapsed",最后以 "submenu"(子菜单)表明该按钮带有弹出菜单。
JAWS(Windows,商业)
<button name>, button menu
JAWS 的播报最为简洁,通常仅报告 "button menu",不显式播报折叠状态——这正是预期中“optionally informs it's collapsed”的由来:不同 AT 对 aria-expanded="false" 的播报策略存在差异,测试应接受“不播报”或“播报 collapsed”两种结果,但绝不能出现 "expanded"。
这份对照表的价值在于:无障碍验证不是“看 DOM 属性”而是“听 AT 输出”,同一属性在不同 AT 中会产生不同口播,测试预期必须按 AT 分别设定。
源码级原理解析:aria-expanded 的生命周期
渲染默认值:button 插件的模板
所有工具栏按钮(含可展开按钮)都由 button 插件 的模板渲染。模板中与弹出状态相关的部分如下(plugin.js):
aria-haspopup="{hasArrow}"
aria-disabled="{ariaDisabled}"
{hasArrowAriaHtml}
其中 hasArrow(是否带下拉箭头)决定了两件事(plugin.js):
hasArrow: typeof this.hasArrow === 'string' && this.hasArrow || ( this.hasArrow ? 'true' : 'false' ),
...
hasArrowAriaHtml: this.hasArrow ? ' aria-expanded="false"' : '',
aria-haspopup:若hasArrow为字符串(如'menu'、'listbox'),则直接作为aria-haspopup的属性值(WAI-ARIA 1.1 规范允许menu、listbox、dialog等取值);若为布尔值true,则渲染为字符串'true';aria-expanded的初始值:只要按钮带箭头(hasArrow为真),渲染时就默认携带aria-expanded="false"——即使菜单尚未创建,按钮在 accessibility tree 中也被声明为“当前收起”。
自动化测试 aria.js 专门验证了这一默认值行为:
// (#5144)
'test button label with arrow has [aria-expanded] attribute added with the default value of false':
function() {
var button = buttonTools.getUiItem( this.editor, 'arrow2_btn' ),
buttonElement = buttonTools.getButtonDomElement( button ),
ariaExpanded = buttonElement.getAttribute( 'aria-expanded' );
assert.areEqual( 'false', ariaExpanded, '[aria-expanded] value is incorrect' );
}
测试中 arrow2_btn 以 hasArrow: true 注册,断言渲染后 aria-expanded 必须为 "false",而不是缺失或 null。在修复前,这类属性要么缺失、要么不随菜单状态更新,导致 AT 无法获得正确的展开状态。
动态更新:menubutton 插件的开/关逻辑
菜单按钮的展开与收起由 menubutton 插件 管理。该插件依赖 button,menu 两个插件,其点击处理函数在每次点击时同步 aria-expanded:
打开菜单时(plugin.js):
this.setState( CKEDITOR.TRISTATE_ON );
_.on = 1;
// Indicates that menu button is open (#421, #5144).
buttonElement.setAttribute( 'aria-expanded', 'true' );
// This timeout is needed to give time for the panel get focus
// when JAWS is running. (https://dev.ckeditor.com/ticket/9842)
setTimeout( function() {
menu.show( CKEDITOR.document.getById( _.id ), 4 );
}, 0 );
关闭菜单时(plugin.js):
menu.onHide = CKEDITOR.tools.bind( function() {
...
_.on = 0;
// Indicates that menu button is closed (#421, #5144).
buttonElement.setAttribute( 'aria-expanded', 'false' );
}, this );
可以看到,aria-expanded 的更新与菜单的 show/hide 生命周期严格绑定:菜单对象 _.menu 首次点击时延迟创建(new CKEDITOR.menu( editor, ... )),菜单面板带有 aria-label 以提供可访问名称;菜单打开前置 true,onHide 回调中置回 false。代码注释中的 #421 与 #5144 表明该逻辑同时服务两个历史缺陷的修复。
hasArrow 的值在 menubutton 构造时被固定为字符串 'menu'(plugin.js),因此渲染出的按钮带有 aria-haspopup="menu" 与初始 aria-expanded="false"。
同类实现:panelbutton 插件
面板按钮(如 Text Color)走的是 panelbutton 插件,逻辑与菜单按钮对称:
- 构造时将
hasArrow固定为'listbox'(plugin.js),渲染出aria-haspopup="listbox"; panel.onShow中置aria-expanded="true"(plugin.js);panel.onHide中置aria-expanded="false"(plugin.js)。
可见菜单按钮(menu)与面板按钮(listbox)在弹出状态声明上采用了完全一致的策略,这也是为什么 #5144 的手工测试同时覆盖了 language 与 colorbutton 两类按钮。
补充佐证:相关的其他手工测试与自动化测试
围绕 aria-expanded 状态,仓库还提供了两份密切相关的测试,可与本文主题互相印证:
ariaexpanded:实时属性值观察表
ariaexpanded.md 对应页面 ariaexpanded.html,它会以每 250ms 一次的频率扫描工具栏中所有 .cke_button,将每个按钮的 aria-expanded 当前值渲染成表格,供测试者直观观察:
- 打开菜单时对应按钮应显示
true; - 关闭菜单时显示
false; - 普通(无箭头)按钮则不携带该属性。
这份测试不依赖屏幕阅读器,直接以 DOM 属性值验证,可作为本文所述手工测试的“纯技术侧”补充。
label:语言菜单按钮的展开播报
label.md(关联 #421)描述的是语言菜单按钮的行为:打开菜单时 aria-expanded 应为 true、屏幕阅读器播报 "expanded",关闭菜单时播报 "leaving menu";若属性值为 null、标签带 "Selected" 或播报异常,则视为缺陷。它与本文测试一脉相承,均涉及菜单按钮展开状态的 AT 验证。
如何在仓库中运行这些测试
CKEditor 4 的测试体系基于 Bender.js(devDependencies 中声明了 benderjs 及相关适配器,见 package.json)。手工测试(manual test)的入口是 bender.js 配置;运行方式如下:
- 确保已安装 Node.js 依赖(仓库根目录执行
npm install); - 启动 Bender 测试服务器(默认端口 1030);
- 在浏览器中打开测试页面列表,选择
tests/plugins/button/manual/menubuttonscreenreader.html; - 开启屏幕阅读器(macOS 使用 VoiceOver,Windows 使用 NVDA 或 JAWS)后按文档步骤操作;
- 对照文档中的“预期/意外”与各 AT 播报样本,判定通过与否。
注意该测试为 @bender-ui: collapsed 类型,属于人工交互型用例,不会在自动化 CI 中执行;其回归保障主要由 aria.js 这类自动化用例承担(如默认 aria-expanded="false" 断言)。bender.tools.env.mobile 检测则保证移动端环境自动跳过此用例。
小结
从这份手工测试文档出发,可以梳理出 CKEditor 4 可展开按钮无障碍状态声明的完整闭环:
- 渲染层(button):带箭头的按钮默认携带
aria-expanded="false"与aria-haspopup; - 交互层(menubutton / panelbutton):菜单/面板生命周期内同步更新
aria-expanded为true/false; - 验证层:手工测试(menubuttonscreenreader.md、ariaexpanded.md、label.md)覆盖真实 AT 播报,自动化测试(aria.js)守住默认值与回归底线。
对于需要自行扩展 CKEditor 4 按钮或排查无障碍问题的开发者而言,理解 aria-expanded 的三层实现路径,再结合上述手工测试的步骤与播报样本进行验证,即可确保新增的可展开按钮在 VoiceOver、NVDA 与 JAWS 三类主流屏幕阅读器下表现一致、状态准确。