CKEditor 4 菜单按钮(Menu Button)读屏软件无障碍手动测试指南:从 aria-expanded 状态到 VoiceOver / NVDA / JAWS 的实际朗读验证

原创2026-10-15 19:12:22212 阅读
文章标签:前端UI组件

CKEditor 4 菜单按钮(Menu Button)读屏软件无障碍手动测试指南:从 aria-expanded 状态到 VoiceOver / NVDA / JAWS 的实际朗读验证

导读

本文基于 CKEditor 4 仓库中的官方手动测试用例 tests/plugins/menubutton/manual/menubuttonscreenreader.md,系统讲解如何用读屏软件(屏幕阅读器)验证工具栏上"菜单按钮"(如 Language 语言选择按钮)的无障碍语义:按钮何时被朗读为"带弹出菜单且已折叠"、打开菜单后焦点如何进入菜单、按 Escape 后状态如何复位。读完本文,你将掌握这套手动测试的完整操作步骤与判定标准,理解 aria-haspopup、aria-expanded、role="menu" 等 ARIA 属性在 menubutton、button、menu 插件中的真实实现,并能在 macOS VoiceOver、NVDA、JAWS 三种读屏环境下对照预期输出做无障碍回归验证。

测试背景:为什么要单独为读屏软件做手动测试

在 CKEditor 4 中,工具栏上的"菜单按钮"(CKEDITOR.UI_MENUBUTTON,例如 language 插件 注册的 "Language" 按钮)是一种可展开/折叠弹出菜单的特殊按钮。它由 menubutton 插件 提供,依赖 button 与 menu 两个插件(requires: 'button,menu')。与普通按钮不同,这类按钮需要同时向读屏软件表达两层信息:

  1. 它是一个按钮(role="button");
  2. 它能弹出菜单(aria-haspopup="menu"),并且当前菜单是展开还是折叠(aria-expanded)。

这些问题在历史版本中曾出现过真实缺陷:根据 CHANGES.md 的记录,#5144 修复了"菜单按钮与面板按钮(panel button)在浏览器无障碍树中错误指示弹出菜单打开状态"的问题(aria-expanded 初始值、开关时机),而 #2444 则让可切换(toggle)工具栏按钮在无障碍树中以 toggle button 形态暴露。由于读屏软件的朗读结果无法用普通断言完全覆盖,仓库便以"面向读屏软件的手动测试"(manual test)形式补充验证,本文要讲解的正是这个用例。

测试用例原文结构:Bender 元信息与测试前提

该用例文件头部包含一组 Bender 手动测试标记(bender-tags),它们是运行测试的基础:

@bender-tags: 4.19.0, 2444, feature, button
@bender-ui: collapsed
@bender-ckeditor-plugins: wysiwygarea, toolbar, language
  • 4.19.0:该测试所对应的目标版本;
  • 2444:关联的问题/需求编号(toggle 按钮在无障碍树中暴露);
  • feature, button:测试类别标签;
  • @bender-ui: collapsed:手动测试页面以折叠 UI 呈现;
  • @bender-ckeditor-plugins: wysiwygarea, toolbar, language:本测试页面仅加载 wysiwygarea、toolbar、language 三个插件,其中 language 插件会注册一个菜单按钮,是测试的观察对象。

文档原文还有一句重要声明:

Note This test is intended for screen reader.

即:该测试必须使用读屏软件执行,普通鼠标操作无法判定朗读是否符合预期。测试页面本身非常简单,见 tests/plugins/menubutton/manual/menubuttonscreenreader.html:它创建一个仅含 Language 按钮的编辑器(toolbar: [ [ 'Language' ] ],界面语言固定为 language: 'en'),方便读屏用户快速定位焦点。

三步操作与判定标准(原文完整继承)

文档给出了三条操作步骤,每条都配有 Expected(预期)结果。整个用例的核心是:在未打开菜单、打开菜单、按下 Escape 关闭菜单三种状态下,读屏软件都应该把按钮朗读为"已折叠",且打开菜单后焦点应进入菜单内部。

  1. 把焦点移到编辑器工具栏上的按钮

    • 预期:读屏软件将按钮朗读为"具有弹出菜单(popup)且当前为折叠(collapsed)状态"。
    • 对应实现:按钮渲染模板为按钮元素写入了 aria-haspopup="{hasArrow}" 与初始 aria-expanded="false"(见 plugins/button/plugin.js 与 hasArrowAriaHtml: this.hasArrow ? ' aria-expanded="false"' : '',以及 plugins/menubutton/plugin.js 中 onHide 时置回 'false' 的注释 // Indicates that menu button is closed (#421, #5144))。
  2. 通过键盘激活按钮(打开菜单)

    • 预期:读屏软件朗读出菜单(并提示当前位于菜单中)。
    • 对应实现:点击函数 clickFn 中先 buttonElement.setAttribute( 'aria-expanded', 'true' )(见 plugins/menubutton/plugin.js),再通过 setTimeout 调用 menu.show(...) 展示菜单面板。菜单面板默认带 role="menu",菜单项带 role="menuitem"(可被语言插件覆盖为 menuitemcheckbox),见 plugins/menu/plugin.js 与 role: this.role ? this.role : 'menuitem'。
  3. 按下 Escape

    • 预期:读屏软件再次把按钮朗读为"具有弹出菜单且当前折叠"。
    • 对应实现:菜单的 onEscape 处理——顶层菜单收到 Escape(keystroke 27)时执行 this.hide( 1 )(见 plugins/menu/plugin.js),随后 onHide 回调把 aria-expanded 置回 'false'(见 plugins/menubutton/plugin.js)。

这三步合在一起,验证的正是"菜单开/关过程中 aria-expanded 状态同步"这一无障碍语义闭环。

三种读屏软件的实际朗读输出(原文完整继承)

文档针对三大主流读屏软件给出了可直接对照的样本输出,这是验收时最重要的判据:

VoiceOver(macOS)

  • 按钮:"<button name>, menu pop-up collapsed, button"
  • 菜单:发出 "plop" 提示音,随后朗读 "<first item name> you are currently in the menu"("你当前位于菜单中")

NVDA(Windows)

  • 按钮:<button name> button collapsed submenu
  • 菜单:发出 "beep" 提示音,随后朗读 "<first item name> not checked 1 of 3"("未选中,第 1 项,共 3 项")

JAWS(Windows)

  • 按钮(关闭时):"<button name>, button menu. Press space to activate the menu, then navigate with arrow keys"("按空格激活菜单,然后用方向键导航")
  • 菜单:"Menu, <first item name> not checked 1 of 3. To move through items press up and down arrow"("菜单,第 1 项共 3 项未选中,按上下方向键移动")

说明:输出中的 <button name>、<first item name> 为占位符,实际朗读的是对应按钮与菜单项的真实名称。三个读屏软件朗读措辞不同,但语义一致——关闭时提示"可弹出、已折叠",打开后提示"已进入菜单并可导航"。

源码级印证:aria-haspopup 与 aria-expanded 从何而来

手动测试验证的是行为,而其背后是 ARIA 属性在源码中的精确控制。理解这些属性有助于你在真实项目中复现、排查同类无障碍问题。

按钮模板:aria-haspopup 与初始 aria-expanded

button 插件 渲染工具栏按钮的模板包含:

role="button"
aria-haspopup="{hasArrow}"
aria-disabled="{ariaDisabled}"
{hasArrowAriaHtml}

其中 hasArrow 来自按钮定义——menubutton 在构造时设置 this.hasArrow = 'menu'(见 plugins/menubutton/plugin.js),使 aria-haspopup 的值恰好为 menu(对应 CHANGES.md 中 #2072 的说明:Menu Button 的 aria-haspopup 值现为 menu)。渲染参数中 hasArrowAriaHtml: this.hasArrow ? ' aria-expanded="false"' : ''(plugins/button/plugin.js)则保证按钮初始 aria-expanded="false",这正是测试第 1 步"折叠"语义的来源。

打开与关闭:aria-expanded 的动态切换

在 menubutton 插件 的 clickFn 中:

  • 打开菜单前:buttonElement.setAttribute( 'aria-expanded', 'true' );(L52,注释 // Indicates that menu button is open (#421, #5144));
  • 菜单隐藏回调 onHide 中:buttonElement.setAttribute( 'aria-expanded', 'false' );(L40,注释 // Indicates that menu button is closed (#421, #5144))。

同时,clickFn 开头会检查 _.on && menu——若菜单已打开则直接 menu.hide(),保证 aria-expanded 与菜单真实可见状态始终一致。

菜单本身:role="menu" 与菜单项角色

menu 插件 创建浮动面板时默认补上 attrs.role = 'menu';菜单项模板带 role="{role}"(默认 menuitem,见 role: this.role ? this.role : 'menuitem')。language 插件 在注册语言菜单项时显式设置 role: 'menuitemcheckbox' 并支持 aria-checked,因此 NVDA/JAWS 样本输出中会出现 "not checked 1 of 3" 这类复选框朗读。这也是为何手动测试使用 Language 按钮:它天然包含"可勾选菜单项 + 弹出菜单"两种无障碍语义,验证覆盖面更广。

配套自动化测试:属性级的机器验证

除手动测试外,仓库还提供了自动化测试 tests/plugins/menubutton/aria.js,用于在无读屏环境下验证三个关键属性:

  • aria-haspopup:断言菜单按钮元素的 aria-haspopup 等于 'menu';
  • 初始 aria-expanded:断言初始值为 'false'(对应 #5144);
  • 开/关切换:打开菜单后断言 aria-expanded 变为 'true',隐藏菜单后回到 'false'(对应 #5144)。

手动测试与这套自动化测试互为补充:自动化测试保证属性值在 DOM 层面正确;手动测试则确认属性最终能转化为正确的读屏朗读(不同读屏软件对同一组 ARIA 属性的解释并不完全相同,这正是必须人工验收的原因)。

如何在本地运行该手动测试

该测试属于 Bender 手动测试体系,运行方式如下:

  1. 项目使用 Bender 作为测试框架(见 bender.js 与 package.json 中的 benderjs 系列依赖;端口配置见 bender-runner.config.json,服务端口 9002、测试端口 9001)。
  2. 手动测试(manual test)由配置中的 manualBrowsers 等参数圈定运行浏览器,通常需要人工在浏览器中按步骤操作并勾选"通过/失败"。
  3. 打开测试页面 tests/plugins/menubutton/manual/menubuttonscreenreader.html 后,在系统层面开启读屏软件(macOS 用 VoiceOver、Windows 用 NVDA 或 JAWS),再按上文三步操作执行并对照预期输出验收。

常见失败形态与排查思路

  • 朗读始终为"已展开"(expanded):多为 aria-expanded 未随菜单关闭复位。检查 onHide 回调是否被正确触发(参见 plugins/menubutton/plugin.js),或是否存在 Escape 未命中菜单 onEscape 的情况(keystroke 27 的判断见 plugins/menu/plugin.js)。
  • 打开菜单后焦点未进入菜单:菜单展示依赖 setTimeout( ..., 0 ) 延迟,源码注释明确这是为了给 JAWS 运行时的面板聚焦留出时间(https://dev.ckeditor.com/ticket/9842,见 plugins/menubutton/plugin.js)。若改动相关时序,需回归本用例。
  • 读屏未提示"可弹出":检查按钮模板中的 aria-haspopup 是否被按钮定义覆盖或遗漏(默认按钮无箭头时该属性为 false,仅菜单按钮/面板按钮等会置为 menu 或 listbox)。

小结

tests/plugins/menubutton/manual/menubuttonscreenreader.md 是一份短小但完整的无障碍验收用例:它以三步键盘操作闭环(聚焦 → 激活 → Escape)验证菜单按钮在 VoiceOver、NVDA、JAWS 下的朗读语义,并与 menubutton、button、menu 源码中的 aria-haspopup / aria-expanded / role="menu" 实现一一对应。无论你是 CKEditor 4 的集成维护者,还是关注富文本编辑器无障碍质量的开发者,都可以直接复用这套"手动 + 自动化"双轨验证思路:自动化测试锁定 DOM 属性,手动测试锁定读屏朗读,两者缺一不可。

登录后查看全文
ckeditor4