Playwright错误处理与重试机制实现

23 阅读4分钟

关注 霍格沃兹测试学院公众号,回复「资料」, 领取人工智能测试开发技术合集

在实际的自动化测试和网络爬虫开发中,稳定性是衡量脚本质量的重要指标。即使编写了最完善的Playwright脚本,也不可避免地会遇到各种运行时错误:元素加载延迟、网络波动、页面响应超时等。本文将分享如何构建健壮的Playwright错误处理与重试机制。

为什么需要错误处理机制?

我曾遇到过这样的场景:一个精心编写的爬虫脚本在本地运行完美,但放到服务器上却频繁失败。调查后发现,服务器与目标网站之间的网络延迟较高,导致元素加载时间超出预期。这就是缺乏错误处理机制的典型表现。

基础错误处理策略

1. 智能等待替代硬性等待

新手常犯的错误是使用page.waitForTimeout(5000)这样的固定等待。更好的做法是使用Playwright内置的智能等待方法:

// 不推荐 - 硬性等待
await page.waitForTimeout(5000);
await page.click('#submit');

// 推荐 - 智能等待
await page.waitForSelector('#submit', { state: 'visible' });
await page.click('#submit');

2. 异常捕获基础

最简单的错误处理是try-catch块:

async function safeClick(page, selector) {
  try {
    await page.click(selector);
    return true;
  } catch (error) {
    console.warn(`点击元素 ${selector} 失败:`, error.message);
    return false;
  }
}

实现重试机制

基础重试函数

下面是一个通用的重试函数,可以包装任何可能失败的操作:

async function withRetry(
  operation,
  maxAttempts3,
  delay1000,
  backoffFactor2
) {
let lastError;

for (let attempt1; attempt <= maxAttempts; attempt++) {
    try {
      returnawait operation();
    } catch (error) {
      lastError = error;
      console.log(`尝试 ${attempt}/${maxAttempts} 失败:`, error.message);
      
      if (attempt === maxAttempts) break;
      
      const waitTime = delay * Math.pow(backoffFactor, attempt - 1);
      console.log(`等待 ${waitTime}ms 后重试...`);
      awaitnewPromise(resolve => setTimeout(resolve, waitTime));
    }
  }

throw lastError;
}

页面操作重试包装器

针对常见的页面操作,我们可以创建专门的重试包装器:

class RetryablePage {
constructor(page) {
    this.page = page;
  }

async clickWithRetry(selector, options = {}) {
    const maxAttempts = options.maxAttempts || 3;
    
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        // 确保元素可见且可点击
        awaitthis.page.waitForSelector(selector, {
          state'visible',
          timeout10000
        });
        
        awaitthis.page.click(selector);
        return;
      } catch (error) {
        console.log(`点击 ${selector} 失败 (尝试 ${attempt}/${maxAttempts}):`, error.message);
        
        if (attempt === maxAttempts) {
          thrownewError(`多次尝试点击 ${selector} 均失败: ${error.message}`);
        }
        
        // 等待时间递增
        awaitthis.page.waitForTimeout(1000 * attempt);
      }
    }
  }

async navigateWithRetry(url, options = {}) {
    const maxAttempts = options.maxAttempts || 3;
    
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        const response = awaitthis.page.goto(url, {
          waitUntil'networkidle',
          timeout30000
        });
        
        if (response && !response.ok()) {
          thrownewError(`HTTP ${response.status()}${response.statusText()}`);
        }
        
        return response;
      } catch (error) {
        console.log(`访问 ${url} 失败 (尝试 ${attempt}/${maxAttempts}):`, error.message);
        
        if (attempt === maxAttempts) {
          throw error;
        }
        
        // 如果是网络错误,尝试刷新页面
        if (error.message.includes('net::') || error.message.includes('Navigation')) {
          awaitthis.page.reload();
        }
        
        awaitthis.page.waitForTimeout(2000 * attempt);
      }
    }
  }
}

Playwright Test中的重试机制

如果你使用Playwright Test框架,它内置了重试功能:

// playwright.config.js
module.exports = {
// 全局重试配置
retries: process.env.CI ? 2 : 1,

use: {
    // 操作失败时的截图配置
    screenshot: 'only-on-failure',
    
    // 视频录制配置
    video: 'retain-on-failure',
  },

// 项目级别的重试配置
projects: [
    {
      name: 'chromium',
      retries: 2,
      use: { browserName: 'chromium' },
    },
  ],
};

自定义测试重试逻辑

对于需要特殊处理的重试场景,可以在测试内部实现:

import { test, expect } from'@playwright/test';

test('重要的支付流程测试'async ({ page }) => {
let paymentSuccessful = false;

for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      // 执行支付流程
      await page.goto('/payment');
      await page.fill('#card-number''4111111111111111');
      await page.click('#pay-now');
      
      // 验证支付成功
      await expect(page.locator('.success-message')).toBeVisible({
        timeout10000
      });
      
      paymentSuccessful = true;
      break;
    } catch (error) {
      console.log(`支付测试尝试 ${attempt} 失败:`, error.message);
      
      if (attempt === 3) {
        throw error;
      }
      
      // 清理状态,准备重试
      await page.goto('/cart');
      await page.waitForTimeout(2000 * attempt);
    }
  }

  expect(paymentSuccessful).toBeTruthy();
});

高级错误处理策略

1. 错误分类与不同处理策略

class ErrorHandler {
static shouldRetry(error) {
    const retryableErrors = [
      'TimeoutError',
      'NetworkError',
      'net::',
      'Target closed',
      'Element not found'
    ];
    
    const errorMessage = error.toString();
    
    return retryableErrors.some(retryableError =>
      errorMessage.includes(retryableError)
    );
  }

static classifyError(error) {
    const message = error.toString();
    
    if (message.includes('Timeout')) {
      return'TIMEOUT';
    } elseif (message.includes('net::')) {
      return'NETWORK';
    } elseif (message.includes('not found') || message.includes('not visible')) {
      return'ELEMENT_NOT_FOUND';
    } else {
      return'UNKNOWN';
    }
  }
}

asyncfunction resilientOperation(operation) {
const maxAttempts = 3;
let lastError;

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      returnawait operation();
    } catch (error) {
      lastError = error;
      
      if (!ErrorHandler.shouldRetry(error) || attempt === maxAttempts) {
        break;
      }
      
      const errorType = ErrorHandler.classifyError(error);
      const delay = this.calculateDelay(attempt, errorType);
      
      console.log(`[${errorType}] 尝试 ${attempt} 失败,${delay}ms 后重试`);
      awaitnewPromise(resolve => setTimeout(resolve, delay));
    }
  }

throw lastError;
}

2. 上下文恢复与状态重置

某些错误需要重置浏览器上下文:

async function withContextRecovery(browser, operation) {
let context;
let page;

for (let attempt1; attempt <= 3; attempt++) {
    try {
      if (!context || context._closed) {
        context = await browser.newContext();
        page = await context.newPage();
      }
      
      returnawait operation(page);
    } catch (error) {
      console.log(`操作失败,尝试 ${attempt}/3:`, error.message);
      
      if (attempt === 3) {
        throw error;
      }
      
      // 清理旧上下文
      if (context && !context._closed) {
        await context.close();
      }
      
      // 短暂等待后继续
      awaitnewPromise(resolve => setTimeout(resolve, 1000 * attempt));
    }
  }
}

Playwright mcp技术学习交流群

伙伴们,对AI测试、大模型评测、质量保障感兴趣吗?我们建了一个 「Playwright mcp技术学习交流群」,专门用来探讨相关技术、分享资料、互通有无。无论你是正在实践还是好奇探索,都欢迎扫码加入,一起抱团成长!期待与你交流!👇

image.png

实战:完整的爬虫错误处理示例

const { chromium } = require('playwright');

class RobustCrawler {
constructor() {
    this.maxRetries3;
    this.requestTimeout30000;
  }

async crawl(url) {
    const browser = await chromium.launch();
    const results = [];
    
    try {
      for (let retry = 1; retry <= this.maxRetries; retry++) {
        try {
          const page = await browser.newPage();
          
          // 设置超时
          page.setDefaultTimeout(this.requestTimeout);
          
          // 监听请求失败
          page.on('requestfailed', request => {
            console.warn(`请求失败: ${request.url()} - ${request.failure().errorText}`);
          });
          
          // 访问页面
          console.log(`尝试 ${retry}/${this.maxRetries}: 访问 ${url}`);
          const response = await page.goto(url, {
            waitUntil'domcontentloaded',
            timeoutthis.requestTimeout
          });
          
          if (!response.ok()) {
            thrownewError(`HTTP ${response.status()}${response.statusText()}`);
          }
          
          // 提取数据
          const data = awaitthis.extractData(page);
          results.push(data);
          
          await page.close();
          break// 成功则退出重试循环
          
        } catch (error) {
          console.log(`尝试 ${retry} 失败:`, error.message);
          
          if (retry === this.maxRetries) {
            thrownewError(`爬取 ${url} 失败: ${error.message}`);
          }
          
          // 指数退避
          awaitnewPromise(resolve =>
            setTimeout(resolve, 1000 * Math.pow(2, retry - 1))
          );
        }
      }
    } finally {
      await browser.close();
    }
    
    return results;
  }

async extractData(page) {
    // 使用选择器重试提取数据
    const extractWithRetryasync (selector, extractor) => {
      for (let i = 0; i < 3; i++) {
        try {
          await page.waitForSelector(selector, { timeout5000 });
          returnawait extractor();
        } catch (error) {
          if (i === 2throw error;
          await page.waitForTimeout(1000);
        }
      }
    };
    
    const title = await extractWithRetry('h1'async () => {
      returnawait page.textContent('h1');
    });
    
    return { title };
  }
}

监控与日志记录

完善的错误处理还需要良好的监控:

class MonitoringErrorHandler {
constructor() {
    this.errors = [];
    this.stats = {
      totalOperations: 0,
      failedOperations: 0,
      retriedOperations: 0,
      recoveredOperations: 0
    };
  }

async trackOperation(operationName, operation) {
    this.stats.totalOperations++;
    const startTime = Date.now();
    
    try {
      const result = await operation();
      return result;
    } catch (error) {
      this.stats.failedOperations++;
      this.errors.push({
        operation: operationName,
        error: error.message,
        timestamp: newDate().toISOString(),
        duration: Date.now() - startTime
      });
      
      // 可以发送到监控系统
      this.reportToMonitoringSystem(error, operationName);
      
      throw error;
    }
  }

  reportToMonitoringSystem(error, operationName) {
    // 发送到Sentry, Datadog等
    console.error(`[MONITORING] ${operationName} failed:`, error.message);
  }

  getStats() {
    return {
      ...this.stats,
      successRate: ((this.stats.totalOperations - this.stats.failedOperations) / 
                   this.stats.totalOperations * 100).toFixed(2) + '%'
    };
  }
}

最佳实践总结

  1. 分级处理策略:根据错误类型采取不同的重试策略,网络错误可以立即重试,业务错误可能需要延迟重试。
  2. 避免无限重试:始终设置最大重试次数,避免陷入死循环。
  3. 指数退避算法:重试间隔应逐渐增加,避免对目标服务器造成压力。
  4. 上下文隔离:每次重试前清理状态,确保测试的独立性。
  5. 详细日志记录:记录每次重试的上下文信息,便于问题排查。
  6. 监控集成:将错误信息集成到现有监控系统,实现主动告警。
  7. 用户可配置:将重试参数(次数、延迟等)设计为可配置项,适应不同场景需求。

结语

错误处理不是Playwright脚本的事后考虑,而是应该在设计初期就纳入架构的重要部分。一个健壮的脚本不仅要能完成任务,更要能优雅地处理失败。通过实现智能的重试机制,你的自动化脚本将能够在生产环境中稳定运行,显著减少人工干预的需要。

记住,好的错误处理机制是透明的一一当它正常工作时,用户几乎感觉不到它的存在;当问题出现时,它又能提供足够的信息帮助快速定位问题。这才是真正有价值的自动化解决方案。

推荐学习

Ai自动化智能体与工作流平台课程,限时免费,机会难得。扫码报名,参与直播,希望您在这场课程中收获满满,开启智能自动化测试的新篇章!

image.png