CKEditor 4 菜单按钮无障碍(ARIA)折叠状态测试指南:从手工验证到屏幕阅读器播报

原创2026-10-14 21:48:471,754 阅读
文章标签:前端UI组件

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:选中文本

  1. Select some text in the editor.

在编辑器中选择一段文本(如测试页面中默认填充的 Lorem ipsum 段落),为后续通过按钮对选区应用颜色或语言标记做准备。

步骤 2:将焦点移动到工具栏的第一个按钮

  1. 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:对选中文本应用颜色或语言

  1. Apply color or language to the selected text.

通过键盘激活工具栏按钮(如 Language 按钮)打开菜单,为选中的文本应用语言或颜色标记。注意:此步骤是通过菜单按钮的弹出菜单完成的,菜单在此过程中会经历“打开→选择→关闭”的完整生命周期。

步骤 4:焦点移回按钮,验证状态

  1. 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:对另一个按钮重复验证

  1. 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 配置;运行方式如下:

  1. 确保已安装 Node.js 依赖(仓库根目录执行 npm install);
  2. 启动 Bender 测试服务器(默认端口 1030);
  3. 在浏览器中打开测试页面列表,选择 tests/plugins/button/manual/menubuttonscreenreader.html;
  4. 开启屏幕阅读器(macOS 使用 VoiceOver,Windows 使用 NVDA 或 JAWS)后按文档步骤操作;
  5. 对照文档中的“预期/意外”与各 AT 播报样本,判定通过与否。

注意该测试为 @bender-ui: collapsed 类型,属于人工交互型用例,不会在自动化 CI 中执行;其回归保障主要由 aria.js 这类自动化用例承担(如默认 aria-expanded="false" 断言)。bender.tools.env.mobile 检测则保证移动端环境自动跳过此用例。

小结

从这份手工测试文档出发,可以梳理出 CKEditor 4 可展开按钮无障碍状态声明的完整闭环:

  1. 渲染层(button):带箭头的按钮默认携带 aria-expanded="false" 与 aria-haspopup;
  2. 交互层(menubutton / panelbutton):菜单/面板生命周期内同步更新 aria-expanded 为 true / false;
  3. 验证层:手工测试(menubuttonscreenreader.md、ariaexpanded.md、label.md)覆盖真实 AT 播报,自动化测试(aria.js)守住默认值与回归底线。

对于需要自行扩展 CKEditor 4 按钮或排查无障碍问题的开发者而言,理解 aria-expanded 的三层实现路径,再结合上述手工测试的步骤与播报样本进行验证,即可确保新增的可展开按钮在 VoiceOver、NVDA 与 JAWS 三类主流屏幕阅读器下表现一致、状态准确。

登录后查看全文
ckeditor4