5月28日 01:08

Cypress 的自动等待机制是如何工作的?

Cypress 区别于 Selenium 等传统测试框架的核心能力之一,就是在执行每条命令时自动等待目标元素就绪,而不需要开发者手动插入 wait()sleep()。理解这套自动等待(包括重试)机制的运行方式,是写出稳定 E2E 测试的前提。

自动等待解决了什么问题

前端测试中,异步操作无处不在——DOM 渲染需要时间,网络请求需要等待响应,CSS 动画需要播放完毕。传统做法是手动加等待时间,但固定等待既浪费时间又不可靠:等短了容易 flaky,等长了拖慢整个测试套件。

Cypress 的思路是不猜时间,而是反复检查条件。当执行一条命令时,Cypress 会在超时窗口内持续轮询,直到目标满足条件才继续下一条命令。如果超时仍未满足,测试失败并给出清晰的错误信息。

命令执行的自动等待流程

当你在测试中写下一行代码:

javascript
cy.get('#submit-btn').click();

Cypress 并不会立即查找 #submit-btn 并点击。实际执行流程是:

  1. 启动计时器:记录当前时间戳,默认超时 4 秒(defaultCommandTimeout
  2. 轮询检查:每隔约 100ms 重新查询 DOM,依次验证三个条件:
    • 元素存在于 DOM 中(exists
    • 元素可见(visible,未被 display:nonevisibility:hidden 隐藏)
    • 元素可交互(enabled,未被 disabled 属性禁用,且不在动画中)
  3. 条件满足:立即执行 .click() 操作,计时器销毁
  4. 超时失败:4 秒内未满足条件,抛出 TimedOutError,测试终止

这个流程对开发者完全透明——你只写了 cy.get().click(),Cypress 在内部完成了全部等待逻辑。

重试机制(Retry-ability)

自动等待的核心实现是 retry-ability。Cypress 不仅等待元素出现,还会重新执行整条命令链来应对 DOM 变化。

断言也会触发重试

javascript
cy.get('.notification').should('contain', '保存成功');

这行代码中,.should() 断言失败时,Cypress 不会立即报错,而是回到 cy.get('.notification') 重新查询 DOM,再次执行断言。这个"查询 → 断言 → 失败 → 重新查询"的循环会一直持续到断言通过或超时。

这意味着:如果 .notification 元素还没渲染出来,或者文本还在加载中,Cypress 会自动重试,不需要你加任何额外代码。

重试的范围

重试只发生在同一个命令链内。看这个例子:

javascript
// 这两条命令各自独立等待 cy.get('#name').type('Alice'); cy.get('#email').type('alice@example.com');

#name 的等待和 #email 的等待互不影响——第一条命令完成后,才开始第二条的等待。

但如果写成链式调用:

javascript
cy.get('#form').within(() => { cy.get('input[name="name"]').type('Alice'); cy.get('input[name="email"]').type('alice@example.com'); });

within 内部的命令共享同一个上下文,但每条命令仍然独立等待自己的目标。

Actionability:元素可操作性检查

Cypress 在执行交互命令(clicktypeselect 等)前,会执行严格的 actionability 检查:

  • 元素存在:在 DOM 中可以找到
  • 元素可见:没有被遮挡、没有 display:nonevisibility:hiddenopacity:0
  • 元素未禁用:没有 disabled 属性
  • 元素不在动画中:Cypress 内置动画检测,会等待 CSS 动画或过渡完成
  • 元素可滚动到视口内:如果元素在视口外,Cypress 会自动滚动到该元素

只有全部条件通过,交互操作才会执行。这就是为什么用 Cypress 很少遇到"元素找到了但点不到"的问题。

动画检测的配置

Cypress 通过 animationDistanceThreshold 判断元素是否还在动画中,默认值 5px。可以调整灵敏度:

javascript
// cypress.config.js module.exports = { e2e: { animationDistanceThreshold: 10, // 增大阈值,更宽松 waitForAnimations: true // 关闭设为 false } };

超时配置

命令级超时

在单条命令上覆盖默认超时:

javascript
// 给这条命令 10 秒等待时间 cy.get('#slow-element', { timeout: 10000 }).click();

全局默认超时

修改所有命令的默认超时时间:

javascript
// cypress.config.js module.exports = { e2e: { defaultCommandTimeout: 8000 // 全局默认 8 秒 } };

不同命令的默认超时

Cypress 中不同类型的命令有不同的默认超时值:

命令类型默认超时配置项
DOM 查询命令4 秒defaultCommandTimeout
页面加载(cy.visit60 秒pageLoadTimeout
网络请求(cy.request5 秒requestTimeout
文件读取(cy.readFile1 秒fileServerFolder 相关

什么时候需要手动等待

自动等待覆盖了大部分场景,但有些情况仍需显式处理:

等待网络请求完成

javascript
// 用 cy.intercept + cy.wait 等待特定 API 响应 cy.intercept('POST', '/api/login').as('loginReq'); cy.get('#submit').click(); cy.wait('@loginReq'); // 等待请求完成,比等元素更可靠

等待非 DOM 的条件

javascript
// 等待某个 JavaScript 变量变化 cy.waitUntil(() => cy.window().then(win => win.appLoaded === true));

避免的错误做法

javascript
// 错误:用固定时间等待 cy.wait(5000); cy.get('.result').should('be.visible'); // 正确:让 Cypress 自动等待 cy.get('.result', { timeout: 10000 }).should('be.visible');

cy.wait(固定毫秒) 是反模式——它不验证任何条件,只是盲目等待,既可能等不够也可能浪费时间。

自动等待 vs 显式等待:对比总结

维度Cypress 自动等待Selenium 显式等待
默认行为所有命令自动等待需要手动配置 WebDriverWait
重试机制内置 retry-ability需要自己写重试逻辑
断言集成断言失败自动重试查询断言与等待分离
动画检测内置
超时配置每条命令可单独配置全局或每个等待单独配置

常见问题与排查

元素明明存在却超时

通常是 actionability 检查未通过——元素被其他元素遮挡、有 pointer-events:none、或仍在动画中。用 .debug() 查看详细信息:

javascript
cy.get('#my-btn').debug().click();

父元素变化导致查询失效

Cypress 的重试会重新执行查询,但如果 DOM 大面积重绘,之前的元素引用可能失效。解决方案是让查询更稳定:

javascript
// 不稳定:依赖元素顺序 cy.get('li').eq(2).click(); // 更稳定:用 data 属性定位 cy.get('[data-cy="third-item"]').click();

条件测试的陷阱

自动等待的前提是"你知道元素会出现"。如果要测试"元素不应该出现",不能用自动等待:

javascript
// 错误:Cypress 会等待元素出现,超时才通过,浪费 4 秒 cy.get('.error-msg').should('not.exist'); // 正确:先确认元素不存在,再断言 cy.get('body').should('not.contain', '.error-msg');

Cypress 从底层设计了自动等待与重试机制,让测试代码更简洁、更稳定。理解这套机制的边界——哪些场景自动处理,哪些需要显式等待——是写出高质量 E2E 测试的关键。

标签:Cypress