EUI 组件测试实战指南:基于 Cypress Component Testing 的测试、无障碍与调试体系
【免费下载链接】euiElastic UI Framework 🙌项目地址: https://gitcode.com/GitHub_Trending/eu/eui
本指南围绕 Elastic UI Framework(EUI)仓库中的 wiki/contributing-to-eui/testing/cypress-testing.md 文档展开,系统讲解 EUI 如何以 Cypress Component Testing 作为组件级测试方案:从运行命令、主题切换、React 版本选择,到编写组件测试、无障碍(a11y)测试、真实事件(Real Events)测试,再到失败录像、截图产物与 CI 调试策略。读完本文,你将掌握 EUI 仓库内 Cypress 测试的完整工作流,并能在本地复现、扩展与调试同类测试。
为什么 EUI 选择 Cypress Component Testing
EUI 目前只使用 Cypress 组件测试(Component Testing),而非其完整版 E2E 测试运行器。二者最大的区别在于:组件测试只挂载(mount)单个组件并围绕它做交互断言,不需要启动整个应用或真实后端,因此非常适合隔离、聚焦特定组件的行为验证,同时大幅缩短传统 E2E 测试的启动时间。
这一设计决策在仓库的 cypress.config.ts 中也有直接体现:component配置块指定了specPattern: ['./src/**/*.spec.tsx', './src/**/*.a11y.tsx'],即测试文件直接散落在源码目录中、紧邻被测组件,而不是集中放在独立的 E2E 目录下;同时测试框架被声明为framework: 'react'、bundler: 'webpack',并复用 cypress/webpack.config 作为构建配置。
与 Jest/JSDom 相比,Cypress 运行在真实浏览器环境中,因此擅长覆盖 Jest 难以模拟的场景,文档明确列举了四类典型用例:
- Focus state:焦点管理与键盘导航行为;
- Portals:
EuiPortal、EuiModal、EuiFlyout等渲染到 DOM 其他位置的组件; - 3rd party dependencies:依赖第三方库能力(如复制到剪贴板)的功能;
- Native DOM behaviors:原生 DOM 行为,例如滚动、label 与 input 的联动点击。
运行测试:三个命令入口
EUI 在 packages/eui/package.json 中定义了三个 Cypress 测试命令,它们全部汇聚到 scripts/test-cypress.js 这个统一入口:
"test-cypress": "node ./scripts/test-cypress", "test-cypress-dev": "yarn test-cypress --dev", "test-cypress-a11y": "yarn test-cypress --a11y"| 命令 | 行为 |
|---|---|
yarn test-cypress | 无头模式(headless)运行组件测试,不弹出窗口,适用于 CI 与日常回归 |
yarn test-cypress-a11y | 无头模式运行组件无障碍测试(基于 axe-core) |
yarn test-cypress-dev | 启动由 Cypress 控制的 Chrome 窗口,列出已发现的测试,可在窗口内逐条执行与交互 |
从 scripts/test-cypress.js 的实现可以看到,--dev与--a11y标志会组合出不同的底层 Cypress 命令:
- 开发模式(
dev)走open --component,打开交互式运行器; - 无头模式(headless)走
run --component --browser chrome,并分别用--spec="./src/**/*.spec.tsx"或--spec="./src/**/*.a11y.tsx"精确限定运行哪类测试——这与cypress.config.ts中specPattern的注释("scripts/cypress.js splits this using the CLI --spec argument")相互印证。
设置主题:light 与 dark
默认情况下测试使用 light(浅色)主题运行;如需深色模式,传入--theme=dark即可:
yarn test-cypress --theme=dark yarn test-cypress-a11y --theme=dark该选项在 scripts/test-cypress.js 中被限定为choices: ['light', 'dark'],默认light;最终以环境变量THEME=${theme}的形式注入 Cypress 进程。同时,EUI 的挂载命令 cypress/support/setup/mount.tsx 会以<EuiProvider colorMode="LIGHT">包裹被测组件,并允许通过providerProps覆盖colorMode等 EuiProviderProps 配置。
跳过 CSS 编译
为确保测试使用最新的样式,运行器会在启动 Cypress 前将仓库的 SCSS 编译为 CSS。这一步会占用额外的处理时间,而本地往往已存在仍然有效的构建产物,因此可传入--skip-css跳过编译,加速本地迭代:
yarn test-cypress --skip-css yarn test-cypress-dev --skip-css yarn test-cypress-a11y --skip-css指定 React 版本
默认情况下,EUI 的 Cypress 测试使用当前支持的最新版 React(仓库内默认18)。可以通过--react-version切换到16、17或18验证多版本兼容性:
yarn test-cypress --react-version=16 yarn test-cypress --react-version=17在 scripts/test-cypress.js 中该参数被定义为type: 'number'且choices: [16, 17, 18],最终以REACT_VERSION环境变量传递。这一变量直接影响挂载方式:mount.tsx 通过比对process.env.REACT_VERSION === '18'来选择@cypress/react18还是@cypress/react的mount实现(注释特别说明必须直接与字符串比较,才能让 tree-shaking 正常工作、避免缺包报错)。
透传 Cypress CLI 参数
test-cypress脚本本身基于 yargs 解析,并将多余参数原样透传给 Cypress(scripts/test-cypress.js 中'unknown-options-as-args': true即为此服务)。因此 Cypress 官方 CLI 参数 都可直接使用,文档给出的典型示例:
# 只运行单个测试文件,例如 onBoarding.js yarn test-cypress --spec '**/{file}.spec.tsx' # 选择 Chrome 无头运行 yarn test-cypress --headless # 覆盖配置项,例如开启视频录制 yarn test-cypress-dev --config video=true # 覆盖环境变量 yarn test-cypress-dev --env password=foobar编写组件测试
何时该写 Cypress 测试
判断标准很清晰:优先为 Jest/JSDom 无法真实复现的功能编写 Cypress 测试,例如焦点状态、Portals、第三方依赖、原生 DOM 行为(滚动、label 与 input 点击联动)。换句话说,常规渲染与纯逻辑断言仍归 Jest,浏览器交互类行为才交给 Cypress。
基本写法
Cypress 拥有自己的一套cy.API/命令,日常最常用的是cy.get()、cy.find(),配合cy.click()或cy.type()与 DOM 交互。文档给出的最小示例:
import { mount } from '@cypress/react'; import TestComponent from './test_component'; describe('TestComponent', () => { it('takes user input, submits it, and displays the resulting output', () => { mount(<TestComponent />); cy.get('[data-test-subj="someInput"]').type('hello world'); cy.get('[data-test-subj="submitButton"]').click(); cy.get('[data-test-subj="someOutput"]').contains('HELLO WORLD'); }); });注意两点细节:
- 示例中的
mount直接来自@cypress/react;而 EUI 内部实际使用自定义的cy.mount()命令,其实现见 cypress/support/setup/mount.tsx,会自动用EuiProvider包裹被测组件,保证主题、全局样式等上下文就绪; - 交互与断言全部围绕
data-test-subj属性定位节点,这是 EUI 全仓库统一的测试契约。
测试文件命名规范
测试文件放在与被测组件相同的目录下(与{component_name}.tsx同目录),按后缀区分用途:
{component name}.spec.tsx:完整的组件测试,随每次构建运行;{component name}.a11y.tsx:无障碍测试,使用 Cypress Axe 规则执行。
在仓库中可看到大量实例,例如 accordion.a11y.tsx、basic_table.a11y.tsx、breadcrumbs.a11y.tsx 等,均与对应组件源码同目录存放,总计 47 个.a11y.tsx文件分布于src/components下。
Do's and don'ts
- DO:通读 Cypress 官方 best practices 建议;
- DO:使用
data-test-subj属性标记后续要find的组件部位; - DON'T:尽量不依赖 class 名或其他实现细节来定位节点(避免测试与内部实现强耦合);
- DON'T:不要扩展
cy.全局命名空间——优先直接导入辅助函数。
仓库对此的践行体现在 cypress/support/component.tsx:所有自定义命令都通过Cypress.Commands.add以受控方式注册,例如mount、realMount、checkAxe、repeatRealPress等,而辅助逻辑(如wait_for_position_to_settle)则作为可导入的 helper 而非全局 API 存在。
在 CI 上记录失败的 Cypress 测试
EUI 支持将失败的 Cypress 测试录制为 Buildkite CI 产物(artifact)。该功能默认关闭,通过修改 cypress.config.ts 中的video: false为video: true即可开启。验证方式:故意让一个测试失败,然后本地运行yarn test-cypress,视频文件会存放在cypress/videos/目录。
仓库配置还包含两个与录像相关的细节:
retries: { runMode: 2, openMode: 2 }:Cypress 运行/交互模式下各最多重试 2 次;videoCompression: 32:压缩级别 32,处理时间更长但上传的产物文件更小;after:spec钩子(cypress.config.ts):当config.video开启时,只有失败的 spec 保留录像;通过的测试其视频会被unlinkSync删除——这正是文档所说"EUI 团队配置 Cypress 只为失败测试保留视频"的实现来源。
Cypress Axe:自动化无障碍测试
EUI 组件以定时任务(scheduled task)的方式执行无障碍测试,借此更全面地覆盖 DOM 变化场景——例如手风琴(Accordion)展开、模态框(Modal)触发等。底层使用 cypress-axe 访问 axe-core 的 API 方法与规则集。
如何编写 cypress-axe 测试
文件名必须符合{component name}.a11y.tsx模式才会被正确纳入 a11y 测试运行:
// accordion.a11y.tsx describe('Automated accessibility check', () => { it('has zero violations when expanded', () => { cy.mount( <EuiAccordion {...noArrowProps}> <EuiPanel color="subdued"> Any content inside of <strong>EuiAccordion</strong> will appear here. We will include <a href="#">a link</a> to confirm focus. </EuiPanel> </EuiAccordion> ); cy.get('button.euiAccordion__button').click(); cy.checkAxe(); }); });仓库中真实的实现与文档示例高度一致:accordion.a11y.tsx 先cy.mount(<EuiAccordion ...>),点击展开按钮后调用cy.checkAxe()断言"零违规"。
配置cy.checkAxe()
EUI 的自定义cy.checkAxe()命令实现在 cypress/support/a11y/checkAxe.ts,其签名接收四个可选参数:
| 参数 | 说明 | 默认值 |
|---|---|---|
skipFailures | 设为true进入 report-only(仅报告)模式,整个套件完整运行而不会提前失败 | false |
context | 扫描范围,可以是 document 或某个选择器(class、id、元素) | div[data-cy-root](即 Cypress 挂载容器) |
axeConfig | 修改 axe.run API 配置,可包含/排除元素、单条规则或整个规则集 | defaultAxeConfig |
callback | 自定义违规回调(violation callback),用于增加副作用或改变报告结构 | 内置的日志/抛错回调 |
实现细节(checkAxe.ts)显示:命令内部先cy.injectAxe(),再以context ?? defaultContext、axeConfig ?? defaultAxeConfig调用cy.checkA11y,并根据skipFailures选择"仅打印违规并抛错"还是"仅打印违规"的处理函数。违规信息会通过cy.task('log' / 'table')输出到控制台,表格包含id、description、impact、违规节点数等字段(对应 cypress.config.ts 中注册的log与table任务)。
默认规则集定义在 cypress/support/a11y/defaultAxeConfig.ts:runOnly覆盖section508、wcag2a、wcag2aa、wcag21a、wcag21aa全部标签,以帮助满足欧美无障碍合规要求;同时显式关闭color-contrast规则——因为 EUI 拥有经过充分测试的调色板,在 Cypress 中该规则容易产生误报(详见 cypress-axe 的 issue #98)。
基于这些默认值,可以按文档示例扩展规则集(例如追加best-practices标签):
// 基于 EUI 默认规则集创建自定义规则集 import { defaultAxeConfig } from '../../cypress/support/a11y/axeCheck'; const customAxeConfig = { ...defaultAxeConfig, runOnly: { type: 'tag', // 在既有规则集基础上增加 best-practices values: [...defaultAxeConfig.runOnly.values, 'best-practices'], }, }; // 违规将使用自定义规则集并使测试失败 cy.checkAxe(false, customAxeConfig);需要说明:文档中的导入路径../../cypress/support/a11y/axeCheck与当前仓库实际结构略有出入——真实文件为 cypress/support/a11y/checkAxe.ts 与 cypress/support/a11y/defaultAxeConfig.ts,实际使用时请按此路径导入defaultAxeConfig。
Cypress Real Events:真实浏览器事件
Cypress 默认事件是模拟的:cy.click、cy.type等都由 JavaScript 触发,因此事件是"不受信任"的(event.isTrusted为false),行为可能与真实原生事件略有差异。某些场景根本无法用模拟事件完成,例如填写原生 alert 弹窗或复制到剪贴板。Cypress Real Events 插件正是为此而生。
为什么用真实事件
Cypress Real Events 通过 Chrome Devtools Protocol 以真实浏览器的方式处理行为,从而能更可靠地测试复杂事件(如鼠标悬停、键盘焦点)。EUI 用真实事件 + 断言的方式验证键盘与读屏器可访问性,观察用户改变本地状态时的表现。
如何编写真实事件测试
该插件的 API 与现有cy()方法无缝协作:想用真实事件按按钮,可用realPress('Tab')替代合成的cy.tab();要按多个键(即 chord 组合键),向辅助方法传入数组,如['Shift', 'Tab']。所有方法都以"real"前缀命名。文档示例:
import TestComponent from './test_component'; describe('TestComponent', () => { it('presses a button using the Enter key', () => { /* 使用 realMount() 在测试窗口中设置焦点 */ cy.realMount(<TestComponent />); /* 用真实键盘事件激活按钮 */ cy.get('[data-test-subj="submitButton"]').realPress('Enter'); /* 断言按钮获得焦点且 aria-expanded 属性已更新 */ cy.focused().invoke('attr', 'aria-expanded').should('equal', 'true'); }); it('presses a button using the Space key', () => { /* 断言按钮同样接受空格键键击 */ cy.realMount(<TestComponent />); cy.get('[data-test-subj="submitButton"]').realPress('Space'); cy.focused().invoke('attr', 'aria-expanded').should('equal', 'true'); }); });仓库侧,cy.realMount()实现在 cypress/support/setup/realMount.tsx:它在普通cy.mount()的基础上额外渲染一个 1px×1px 的data-test-subj="cypress-real-event-target"目标元素并对其realClick,以此在测试窗口内建立真实焦点起点,随后realPress才能真正落到被测组件上。此外 cypress/support/keyboard/repeatRealPress.ts 还封装了repeatRealPress(keyToPress, count = 2, options),用于连续多次按键(如多次 Tab 导航)的场景。
真实事件的 Do's and don'ts
- DO:遵循上文 编写 Cypress 测试 的全部建议;
- DO:选对挂载方式——
- 组件不会自动接收焦点时用
cy.realMount(); - 组件在渲染时会自动获取焦点则用
cy.mount();
- 组件不会自动接收焦点时用
- DO:持续关注 Cypress Real Events 的新特性。
调试测试
本地调试失败时推荐yarn test-cypress-dev:它允许你运行单个测试套件,并在浏览器窗口中运行测试,从而可以使用 DevTools 按需暂停与检查 DOM。
一般建议:测试运行期间不要乱点页面。用户干扰可能引入难以复现的偶发(flaky)行为与超时。
产物(Artifacts)
- 所有失败测试都会向
cypress/screenshots/输出一张截图。调试失败测试时强烈建议先看截图,从中检查错误信息与失败瞬间的 UI 状态; - 若静态截图信息不足,可传入
--config video=true为全部测试(含成功与失败)录制视频到cypress/videos/,以获取失败点之前更多的上下文。
ℹ️ 仓库配置默认关闭视频以缩短测试耗时(尤其是 CI),但深度调试时建议重新开启。这正对应 cypress.config.ts 中的
video: false默认值。
CI 调试
失败截图产物(以及 Cypress 日志)由 Jenkins CI 生成。
TODO:官方文档此处留有 TODO,待补充"在哪里点击查看相关产物/截图"的指引——本地调试请优先使用上文提到的
cypress/screenshots/目录。
总结
围绕 cypress-testing.md 这份文档,EUI 的 Cypress 测试体系可以概括为三条主线:
- 运行入口统一:
test-cypress/test-cypress-a11y/test-cypress-dev三个命令背后都是 scripts/test-cypress.js,通过--theme、--skip-css、--react-version及任意 Cypress CLI 参数完成高度灵活的测试编排; - 三类测试互补:
*.spec.tsx覆盖交互功能、*.a11y.tsx基于 axe-core 覆盖无障碍合规、Cypress Real Events 覆盖真实键盘/鼠标行为,三者共用data-test-subj定位契约与EuiProvider挂载环境; - 失败可追溯:截图、录像(默认仅保留失败用例)、CI 产物与自定义
cy.checkAxe报告共同构成完整的调试闭环。
对 EUI 的贡献者而言,理解这套体系意味着:新增组件时知道该补哪种测试文件、命名放在哪里;排查 CI 失败时知道先看截图、必要时开录像;维护无障碍合规时知道如何扩展defaultAxeConfig规则集。这些能力同样可以迁移到其他基于 Cypress Component Testing 的 React 组件库项目中。
【免费下载链接】euiElastic UI Framework 🙌项目地址: https://gitcode.com/GitHub_Trending/eu/eui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考