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