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