Playwright 异步处理 #
异步处理概述 #
Playwright 的核心优势之一是自动等待机制,但在某些场景下仍需要手动处理异步操作。
自动等待 vs 手动等待 #
text
┌─────────────────────────────────────────────────────────────┐
│ Playwright 等待策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 自动等待(推荐): │
│ ├── click, fill, check 等操作 │
│ ├── expect 断言 │
│ └── Locator 操作 │
│ │
│ 手动等待(必要时): │
│ ├── waitForSelector │
│ ├── waitForFunction │
│ ├── waitForRequest/Response │
│ └── waitForTimeout(不推荐) │
│ │
└─────────────────────────────────────────────────────────────┘
自动等待机制 #
操作自动等待 #
typescript
import { test, expect } from '@playwright/test';
test('操作自动等待', async ({ page }) => {
// click 自动等待
await page.click('button'); // 等待按钮可见、可点击
// fill 自动等待
await page.fill('#input', 'text'); // 等待输入框可编辑
// Locator 操作自动等待
await page.getByRole('button').click();
await page.getByLabel('Email').fill('test@example.com');
});
断言自动等待 #
typescript
test('断言自动等待', async ({ page }) => {
// expect 自动重试
await expect(page.locator('.result')).toBeVisible();
await expect(page.locator('.status')).toHaveText('Complete');
// 自动等待直到条件满足或超时
await expect(page).toHaveURL('/dashboard');
});
显式等待 #
waitForSelector #
typescript
test('waitForSelector', async ({ page }) => {
// 等待元素出现
await page.waitForSelector('.loading', { state: 'hidden' });
await page.waitForSelector('.content', { state: 'visible' });
// 等待状态选项
await page.waitForSelector('.element', {
state: 'attached', // 元素在 DOM 中
timeout: 10000,
});
// 状态选项:
// - 'attached' - 元素在 DOM 中
// - 'detached' - 元素不在 DOM 中
// - 'visible' - 元素可见
// - 'hidden' - 元素隐藏
});
Locator.waitFor #
typescript
test('Locator.waitFor', async ({ page }) => {
const element = page.locator('.dynamic-element');
// 等待元素可见
await element.waitFor({ state: 'visible' });
// 等待元素隐藏
await element.waitFor({ state: 'hidden' });
// 等待元素附加到 DOM
await element.waitFor({ state: 'attached' });
// 等待元素从 DOM 移除
await element.waitFor({ state: 'detached' });
});
waitForFunction #
typescript
test('waitForFunction', async ({ page }) => {
// 等待 JavaScript 条件为真
await page.waitForFunction(() => {
return document.querySelectorAll('.item').length >= 5;
});
// 带参数
await page.waitForFunction(
({ selector, count }) => {
return document.querySelectorAll(selector).length >= count;
},
{ selector: '.item', count: 5 }
);
// 等待元素属性变化
await page.waitForFunction(
(selector) => {
const el = document.querySelector(selector);
return el && el.getAttribute('data-status') === 'ready';
},
'.container'
);
// 等待页面状态
await page.waitForFunction(() => {
return window.appLoaded === true;
});
});
网络请求等待 #
waitForRequest #
typescript
test('waitForRequest', async ({ page }) => {
// 等待特定请求
const requestPromise = page.waitForRequest('**/api/users');
await page.click('button:has-text("Load Users")');
const request = await requestPromise;
console.log(request.url());
console.log(request.method());
console.log(request.postData());
// 使用条件函数
const req = await page.waitForRequest(
request => request.url().includes('/api/users') && request.method() === 'POST'
);
// 等待请求完成
await page.waitForRequest(request =>
request.url() === 'https://api.example.com/users' &&
request.method() === 'GET'
);
});
waitForResponse #
typescript
test('waitForResponse', async ({ page }) => {
// 等待特定响应
const responsePromise = page.waitForResponse('**/api/users');
await page.click('button:has-text("Load Users")');
const response = await responsePromise;
console.log(response.status());
console.log(response.url());
const data = await response.json();
// 使用条件函数
const res = await page.waitForResponse(
response => response.url().includes('/api/users') && response.status() === 200
);
// 等待并验证响应
const apiResponse = await page.waitForResponse(async response => {
if (!response.url().includes('/api/users')) return false;
const json = await response.json();
return json.success === true;
});
});
同时等待请求和响应 #
typescript
test('同时等待请求和响应', async ({ page }) => {
// 方式 1: Promise.all
const [request, response] = await Promise.all([
page.waitForRequest('**/api/users'),
page.waitForResponse('**/api/users'),
page.click('button:has-text("Load")'),
]);
// 方式 2: 分开等待
const requestPromise = page.waitForRequest('**/api/users');
const responsePromise = page.waitForResponse('**/api/users');
await page.click('button:has-text("Load")');
const req = await requestPromise;
const res = await responsePromise;
});
等待所有请求完成 #
typescript
test('等待所有请求完成', async ({ page }) => {
// 收集所有请求
const requests: any[] = [];
page.on('request', request => {
if (request.url().includes('/api/')) {
requests.push(request);
}
});
await page.click('button:has-text("Load All")');
// 等待所有请求完成
await page.waitForResponse(response => {
return requests.some(req => req.url() === response.url());
});
});
事件监听 #
页面事件 #
typescript
test('页面事件', async ({ page }) => {
// 监听控制台消息
page.on('console', msg => {
console.log(`浏览器控制台: ${msg.text()}`);
});
// 监听页面错误
page.on('pageerror', error => {
console.error(`页面错误: ${error.message}`);
});
// 监听对话框
page.on('dialog', async dialog => {
console.log(`对话框: ${dialog.message()}`);
await dialog.accept();
});
// 监听请求
page.on('request', request => {
console.log(`请求: ${request.method()} ${request.url()}`);
});
// 监听响应
page.on('response', response => {
console.log(`响应: ${response.status()} ${response.url()}`);
});
await page.goto('/');
});
等待事件 #
typescript
test('等待事件', async ({ page }) => {
// 等待控制台消息
const consolePromise = page.waitForEvent('console');
await page.evaluate(() => console.log('Hello'));
const msg = await consolePromise;
console.log(msg.text());
// 等待文件下载
const downloadPromise = page.waitForEvent('download');
await page.click('button:has-text("Download")');
const download = await downloadPromise;
// 等待新页面
const pagePromise = page.waitForEvent('popup');
await page.click('a[target="_blank"]');
const newPage = await pagePromise;
// 等待对话框
const dialogPromise = page.waitForEvent('dialog');
await page.evaluate(() => alert('Hello'));
const dialog = await dialogPromise;
await dialog.accept();
});
并发处理 #
Promise.all #
typescript
test('Promise.all 并发', async ({ page }) => {
// 同时执行多个操作
await Promise.all([
page.waitForResponse('**/api/users'),
page.click('button:has-text("Load")'),
]);
// 同时等待多个请求
const [users, products] = await Promise.all([
page.waitForResponse('**/api/users'),
page.waitForResponse('**/api/products'),
page.goto('/dashboard'),
]);
// 同时填充多个表单
await Promise.all([
page.fill('#email', 'test@example.com'),
page.fill('#password', 'password123'),
]);
});
Promise.race #
typescript
test('Promise.race 竞争', async ({ page }) => {
// 等待任意一个完成
const result = await Promise.race([
page.waitForSelector('.success'),
page.waitForSelector('.error'),
]);
// 判断结果
if (await result.getAttribute('class')?.includes('success')) {
console.log('操作成功');
} else {
console.log('操作失败');
}
});
并行测试 #
typescript
test.describe('并行测试', () => {
test.describe.configure({ mode: 'parallel' });
test('测试 1', async ({ page }) => {
// 并行执行
});
test('测试 2', async ({ page }) => {
// 并行执行
});
test('测试 3', async ({ page }) => {
// 并行执行
});
});
超时处理 #
设置超时 #
typescript
test('超时设置', async ({ page }) => {
// 单个操作超时
await page.click('button', { timeout: 10000 });
// 等待超时
await page.waitForSelector('.element', { timeout: 10000 });
// 断言超时
await expect(page.locator('.element')).toBeVisible({ timeout: 10000 });
// 测试超时
test.setTimeout(60000);
});
// 配置文件全局设置
// playwright.config.ts
export default defineConfig({
timeout: 30000, // 测试超时
expect: {
timeout: 5000, // 断言超时
},
});
超时处理 #
typescript
test('超时处理', async ({ page }) => {
try {
await page.waitForSelector('.slow-element', { timeout: 5000 });
} catch (error) {
console.log('元素加载超时,执行备用方案');
// 备用方案
}
});
轮询与重试 #
自定义轮询 #
typescript
test('自定义轮询', async ({ page }) => {
// 使用 waitForFunction 轮询
await page.waitForFunction(() => {
const element = document.querySelector('.status');
return element && element.textContent === 'Complete';
}, { polling: 100 }); // 每 100ms 检查一次
// 手动轮询
let attempts = 0;
const maxAttempts = 10;
while (attempts < maxAttempts) {
const text = await page.locator('.status').textContent();
if (text === 'Complete') break;
await page.waitForTimeout(500);
attempts++;
}
});
重试机制 #
typescript
test('重试机制', async ({ page }) => {
// 使用 toPass 断言
await expect(async () => {
const response = await page.request.get('/api/status');
expect(response.status()).toBe(200);
}).toPass({
timeout: 10000,
intervals: [100, 500, 1000], // 重试间隔
});
// 自定义重试逻辑
async function retry<T>(
fn: () => Promise<T>,
options: { maxAttempts: number; delay: number }
): Promise<T> {
let lastError: Error;
for (let i = 0; i < options.maxAttempts; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
await new Promise(resolve => setTimeout(resolve, options.delay));
}
}
throw lastError;
}
await retry(
async () => {
const text = await page.locator('.status').textContent();
if (text !== 'Complete') throw new Error('Not complete');
return text;
},
{ maxAttempts: 5, delay: 1000 }
);
});
常见异步场景 #
等待加载完成 #
typescript
test('等待加载完成', async ({ page }) => {
// 方式 1: 等待加载器消失
await page.locator('.loading-spinner').waitFor({ state: 'hidden' });
// 方式 2: 等待内容出现
await expect(page.locator('.content')).toBeVisible();
// 方式 3: 等待网络空闲
await page.waitForLoadState('networkidle');
// 方式 4: 等待特定请求完成
await page.waitForResponse('**/api/data');
});
等待动画完成 #
typescript
test('等待动画完成', async ({ page }) => {
// 等待元素稳定
await page.locator('.animated-element').waitFor({ state: 'visible' });
// 等待动画类移除
await page.waitForFunction(() => {
const el = document.querySelector('.animated-element');
return el && !el.classList.contains('animating');
});
// 使用 expect 等待
await expect(page.locator('.animated-element')).not.toHaveClass(/animating/);
});
等待条件满足 #
typescript
test('等待条件满足', async ({ page }) => {
// 等待元素数量
await expect(page.locator('.item')).toHaveCount(5);
// 等待文本变化
await expect(page.locator('.status')).toHaveText('Complete');
// 等待属性变化
await expect(page.locator('.button')).toBeEnabled();
// 等待自定义条件
await page.waitForFunction(() => {
return (window as any).appReady === true;
});
});
避免反模式 #
不要使用固定等待 #
typescript
// ❌ 不推荐 - 固定等待
await page.waitForTimeout(3000);
await page.click('button');
// ✅ 推荐 - 使用断言等待
await expect(page.locator('.button')).toBeVisible();
await page.click('button');
不要过度等待 #
typescript
// ❌ 不推荐 - 过度等待
await page.waitForSelector('.element');
await page.waitForSelector('.element:visible');
await page.click('.element');
// ✅ 推荐 - 依赖自动等待
await page.click('.element');
正确使用 Promise.all #
typescript
// ❌ 不推荐 - 顺序等待
await page.click('button');
await page.waitForResponse('**/api/data');
// ✅ 推荐 - 同时等待
await Promise.all([
page.waitForResponse('**/api/data'),
page.click('button'),
]);
最佳实践 #
1. 优先使用自动等待 #
typescript
// ✅ 推荐
await page.click('button');
await expect(page.locator('.result')).toBeVisible();
2. 使用语义化等待 #
typescript
// ✅ 推荐
await expect(page.locator('.loading')).not.toBeVisible();
await expect(page.locator('.content')).toBeVisible();
// ❌ 不推荐
await page.waitForTimeout(2000);
3. 合理设置超时 #
typescript
// ✅ 推荐 - 根据实际情况设置
await page.waitForResponse('**/api/slow', { timeout: 30000 });
4. 使用事件监听处理复杂场景 #
typescript
// ✅ 推荐
page.on('response', async response => {
if (response.url().includes('/api/important')) {
const data = await response.json();
// 处理响应
}
});
下一步 #
现在你已经掌握了 Playwright 异步处理的方法,接下来学习 Page Object 模式 了解如何组织测试代码!
最后更新:2026-03-28