Cypress 异步处理 #

理解 Cypress 的异步特性 #

命令队列 #

Cypress 命令是异步的,它们会被加入队列按顺序执行:

javascript
// 这些命令不会立即执行
cy.visit('/');           // 加入队列
cy.get('.button').click(); // 加入队列
cy.get('.result');       // 加入队列

// 命令按顺序执行,即使代码看起来是同步的
console.log('这行代码会先执行');  // 立即执行

执行顺序 #

text
代码编写顺序:
1. cy.visit('/')
2. cy.get('.button').click()
3. console.log('这行先执行')
4. cy.get('.result')

实际执行顺序:
1. console.log('这行先执行')  ← 立即执行
2. cy.visit('/')              ← 队列执行
3. cy.get('.button').click()  ← 队列执行
4. cy.get('.result')          ← 队列执行

自动等待机制 #

内置自动等待 #

Cypress 自动等待元素出现:

javascript
// 自动等待元素出现(最多 4 秒)
cy.get('.element').click();

// 自动等待断言通过
cy.get('.status').should('contain', '完成');

// 自动等待元素消失
cy.get('.loading').should('not.exist');

超时配置 #

javascript
// 全局配置
// cypress.config.js
module.exports = {
  e2e: {
    defaultCommandTimeout: 5000,  // 默认 4000ms
    pageLoadTimeout: 60000
  }
};

// 单个命令配置
cy.get('.element', { timeout: 10000 }).click();

// 断言超时
cy.get('.status', { timeout: 10000 }).should('contain', '完成');

显式等待 #

cy.wait() #

javascript
// 等待固定时间(不推荐)
cy.wait(1000);  // 等待 1 秒

// 等待特定请求
cy.intercept('GET', '/api/users').as('getUsers');
cy.visit('/users');
cy.wait('@getUsers');  // 等待请求完成

// 等待多个请求
cy.wait(['@getUsers', '@getProfile']);

等待请求响应 #

javascript
cy.intercept('GET', '/api/data').as('getData');

cy.get('.load-button').click();

// 等待请求并获取响应
cy.wait('@getData').then((interception) => {
  console.log('请求 URL:', interception.request.url);
  console.log('响应状态:', interception.response.statusCode);
  console.log('响应数据:', interception.response.body);
});

// 验证响应
cy.wait('@getData').its('response.statusCode').should('eq', 200);

等待选项 #

javascript
cy.wait('@getData', {
  timeout: 10000,        // 超时时间
  requestTimeout: 5000,  // 请求超时
  responseTimeout: 5000  // 响应超时
});

处理 Promise #

使用 .then() #

javascript
// 使用 then() 处理命令结果
cy.get('.item').then(($item) => {
  const text = $item.text();
  console.log('元素文本:', text);
  
  // 在 then() 中可以使用同步代码
  if (text.includes('成功')) {
    cy.log('操作成功');
  }
});

链式 then() #

javascript
cy.get('.user')
  .then(($user) => {
    return $user.data('id');  // 返回值传递给下一个 then
  })
  .then((userId) => {
    cy.log(`用户 ID: ${userId}`);
    return cy.request(`/api/users/${userId}`);
  })
  .then((response) => {
    expect(response.status).to.eq(200);
  });

保存值到变量 #

javascript
// 使用别名保存值
cy.get('.user-id').invoke('text').as('userId');

// 在后续测试中使用
cy.get('@userId').then((userId) => {
  cy.request(`/api/users/${userId}`);
});

// 使用 this 访问(注意:需要使用 function 而非箭头函数)
describe('使用别名', () => {
  beforeEach(() => {
    cy.get('.user-id').invoke('text').as('userId');
  });

  it('获取用户信息', function() {
    cy.request(`/api/users/${this.userId}`);
  });
});

async/await 的使用 #

为什么不能直接使用 async/await #

javascript
// ❌ 错误用法 - async/await 对 Cypress 命令无效
it('错误示例', async () => {
  await cy.visit('/');  // 这不会按预期工作
  const $el = await cy.get('.element');  // $el 是 Chainable,不是元素
});

正确的处理方式 #

javascript
// ✅ 正确用法 - 使用 then()
it('正确示例', () => {
  cy.visit('/');
  cy.get('.element').then(($el) => {
    // 这里可以处理元素
    console.log($el.text());
  });
});

// ✅ 混合使用 - 在 then() 中使用 async/await
it('混合使用', () => {
  cy.get('.user-id').then(async ($el) => {
    const userId = $el.text();
    // 可以在这里使用 async/await 处理非 Cypress 异步操作
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    console.log(data);
  });
});

处理动态内容 #

等待元素出现 #

javascript
// 等待元素出现
cy.get('.dynamic-element', { timeout: 10000 }).should('be.visible');

// 等待元素消失
cy.get('.loading-spinner').should('not.exist');

// 等待文本变化
cy.get('.status').should('not.contain', '加载中');
cy.get('.status').should('contain', '完成');

等待条件满足 #

javascript
// 使用 should() 等待条件
cy.get('.counter').should(($el) => {
  const count = parseInt($el.text());
  expect(count).to.be.greaterThan(100);
});

// 自定义等待函数
const waitForCondition = (condition, timeout = 5000) => {
  return cy.wrap(null, { timeout }).should(() => {
    expect(condition()).to.be.true;
  });
};

// 使用
waitForCondition(() => document.querySelector('.loaded'));

轮询检查 #

javascript
// 轮询直到条件满足
cy.get('.status').should(($el) => {
  // 自动重试直到条件满足
  expect($el.text()).to.include('完成');
});

处理定时器 #

cy.clock() #

javascript
// 控制时间
cy.clock();

// 访问页面
cy.visit('/timer');

// 快进时间
cy.tick(1000);  // 快进 1 秒
cy.tick(5000);  // 快进 5 秒

// 恢复时间
cy.clock().then((clock) => {
  clock.restore();
});

时间控制示例 #

javascript
describe('倒计时测试', () => {
  beforeEach(() => {
    cy.clock();
    cy.visit('/countdown');
  });

  it('倒计时显示正确', () => {
    cy.get('.countdown').should('contain', '10');
    
    cy.tick(1000);
    cy.get('.countdown').should('contain', '9');
    
    cy.tick(5000);
    cy.get('.countdown').should('contain', '4');
    
    cy.tick(4000);
    cy.get('.countdown').should('contain', '时间到');
  });

  afterEach(() => {
    cy.clock().invoke('restore');
  });
});

cy.tick() #

javascript
// 快进指定时间
cy.tick(1000);  // 1 秒

// 快进并执行回调
cy.tick(1000, { log: true });

// 链式调用
cy.clock()
  .tick(1000)
  .tick(2000);

处理动画 #

等待动画完成 #

javascript
// 等待元素稳定
cy.get('.animated-element').should('be.visible');

// 等待动画类移除
cy.get('.modal').should('not.have.class', 'animating');

// 等待 CSS 过渡完成
cy.get('.panel').should('have.css', 'opacity', '1');

禁用动画 #

javascript
// 在 cypress/support/e2e.js 中
Cypress.on('window:before:load', (win) => {
  // 禁用 CSS 动画
  win.document.body.style.setProperty('animation', 'none', 'important');
  win.document.body.style.setProperty('transition', 'none', 'important');
});

处理轮询 #

轮询 API #

javascript
// 轮询直到状态变化
const pollUntil = (url, condition, options = {}) => {
  const { timeout = 10000, interval = 500 } = options;
  const startTime = Date.now();

  const check = () => {
    if (Date.now() - startTime > timeout) {
      throw new Error('轮询超时');
    }

    return cy.request(url).then((response) => {
      if (condition(response)) {
        return response;
      }
      return cy.wait(interval).then(() => check());
    });
  };

  return check();
};

// 使用
pollUntil('/api/status', (res) => res.body.status === 'completed');

轮询元素状态 #

javascript
// 等待元素状态变化
cy.get('.status', { timeout: 30000 }).should(($el) => {
  expect($el.text()).to.match(/完成|失败/);
});

并行操作 #

并行请求 #

javascript
// 使用 Promise.all 并行执行
cy.then(() => {
  return Promise.all([
    cy.request('/api/users'),
    cy.request('/api/products'),
    cy.request('/api/orders')
  ]);
}).then(([users, products, orders]) => {
  console.log('用户:', users.body);
  console.log('产品:', products.body);
  console.log('订单:', orders.body);
});

Cypress.Promise #

javascript
// 使用 Cypress.Promise
cy.then(() => {
  return Cypress.Promise.all([
    cy.request('/api/data1'),
    cy.request('/api/data2')
  ]);
}).then(([res1, res2]) => {
  // 处理结果
});

重试策略 #

自动重试断言 #

javascript
// 自动重试直到断言通过
cy.get('.element').should('have.text', 'Expected Text');

// 重试自定义条件
cy.get('.element').should(($el) => {
  expect($el.text().length).to.be.greaterThan(0);
});

自定义重试逻辑 #

javascript
// 自定义重试函数
Cypress.Commands.add('retryUntil', (selector, condition, options = {}) => {
  const { timeout = 10000, interval = 500 } = options;

  cy.get(selector, { timeout }).should(($el) => {
    const result = condition($el);
    expect(result).to.be.true;
  });
});

// 使用
cy.retryUntil('.status', ($el) => $el.text() === '完成');

异步最佳实践 #

1. 避免使用固定等待 #

javascript
// ❌ 不好的做法 - 固定等待
cy.wait(2000);
cy.get('.element').click();

// ✅ 好的做法 - 等待条件
cy.get('.element').should('be.visible').click();

2. 正确使用 then() #

javascript
// ✅ 好的做法 - 在 then() 中处理值
cy.get('.user-id').then(($el) => {
  const userId = $el.text();
  cy.request(`/api/users/${userId}`);
});

// ❌ 不好的做法 - 直接使用返回值
const userId = cy.get('.user-id').text();  // 错误!

3. 合理设置超时 #

javascript
// ✅ 根据实际情况设置超时
cy.get('.slow-element', { timeout: 15000 }).should('be.visible');

// ✅ 在配置中设置合理的默认值
// cypress.config.js
module.exports = {
  e2e: {
    defaultCommandTimeout: 5000,
    pageLoadTimeout: 60000
  }
};

4. 使用别名简化代码 #

javascript
// ✅ 使用别名保存和复用值
cy.get('.user-id').invoke('text').as('userId');
cy.get('@userId').then((userId) => {
  cy.log(`用户 ID: ${userId}`);
});

完整示例 #

javascript
describe('异步处理示例', () => {
  beforeEach(() => {
    cy.intercept('GET', '/api/users').as('getUsers');
    cy.visit('/users');
  });

  it('等待数据加载', () => {
    // 等待加载完成
    cy.get('.loading').should('not.exist');
    
    // 等待请求完成
    cy.wait('@getUsers').then((interception) => {
      expect(interception.response.statusCode).to.eq(200);
    });
    
    // 验证数据渲染
    cy.get('.user-item').should('have.length.gt', 0);
  });

  it('处理动态更新', () => {
    cy.clock();
    
    // 触发更新
    cy.get('.refresh-button').click();
    
    // 快进时间
    cy.tick(1000);
    
    // 验证更新
    cy.get('.last-updated').should('contain', '刚刚');
  });

  it('轮询状态变化', () => {
    cy.get('.status').should('contain', '处理中');
    
    // 触发处理
    cy.get('.process-button').click();
    
    // 等待状态变化
    cy.get('.status', { timeout: 30000 }).should(($el) => {
      const status = $el.text();
      expect(status).to.match(/完成|失败/);
    });
  });

  it('并行请求处理', () => {
    cy.then(() => {
      return Cypress.Promise.all([
        cy.request('/api/users'),
        cy.request('/api/stats')
      ]);
    }).then(([usersRes, statsRes]) => {
      cy.get('.user-count').should('contain', usersRes.body.length);
      cy.get('.stats').should('contain', statsRes.body.total);
    });
  });

  it('使用别名传递值', () => {
    cy.get('.first-user-id').invoke('text').as('userId');
    
    cy.get('@userId').then((userId) => {
      cy.get(`[data-user-id="${userId}"]`).click();
    });
    
    cy.get('.user-detail').should('be.visible');
  });
});

下一步 #

现在你已经掌握了 Cypress 异步处理的方法,接下来学习 网络请求模拟 了解如何控制和模拟网络请求!

最后更新:2026-03-28