Playwright自动化测试实战指南-中级部分

Playwright自动化测试实战指南-中级部分

版本说明:本文基于 Playwright v1.32.0 版本,介绍了自动化测试的进阶应用场景和技巧。由于Playwright更新较快,部分API可能在新版本中有所变化,建议同时参考官方文档获取最新信息。

引言

随着Web应用日益复杂,自动化测试变得愈发重要。Playwright作为一款现代化的自动化测试工具,提供了强大的跨浏览器测试能力。本文适合已经掌握Playwright基础知识,希望进一步提升测试技能的开发者和测试工程师阅读。我们将深入探讨Playwright的进阶应用,帮助你构建更加健壮、高效的测试方案。

目录

中级应用

1. 截图与视频录制

Playwright 提供了强大的截图和视频录制功能,便于调试和记录测试过程。

截图功能
// 截取整个页面
await page.screenshot({ path: 'page.png' });

// 截取特定元素
const element = await page.locator('.header');
await element.screenshot({ path: 'header.png' });

// 全页面截图(包括滚动部分)
await page.screenshot({ 
  path: 'fullpage.png',
  fullPage: true 
});

// 剪裁截图
await page.screenshot({
  path: 'clipped.png',
  clip: {
    x: 100,
    y: 100,
    width: 500,
    height: 300
  }
});

// 忽略HTTPS错误的截图
await page.screenshot({
  path: 'screenshot.png',
  ignoreHTTPSErrors: true
});

// 截图时隐藏某些元素
await page.evaluate(() => {
  const banner = document.querySelector('.cookie-banner');
  if (banner) banner.style.display = 'none';
});
await page.screenshot({ path: 'no-banner.png' });
视频录制

Playwright 可以录制测试执行的视频,非常适合复杂用例的调试和问题复现。

// 在测试配置文件中启用视频录制
// playwright.config.js
module.exports = {
  use: {
    video: {
      mode: 'on', // 始终录制
      // 其他选项:'off', 'on-first-retry', 'retain-on-failure', 'on-first-retry'
      size: { width: 1280, height: 720 } // 自定义尺寸
    }
  }
};

单个测试中启用录制:

// 单个测试中的配置
test.use({ 
  video: 'on' 
});

test('录制登录过程', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.fill('#username', 'testuser');
  await page.fill('#password', 'password123');
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL('https://example.com/dashboard');
});
高级截图和录制技巧
截图比较
// 截图比较和像素匹配
const screenshot = await page.screenshot();
// 使用像素匹配进行对比
expect(screenshot).toMatchSnapshot('reference.png', {
  threshold: 0.2, // 允许20%的像素差异
});
有条件的视频录制
// 仅在特定条件下录制视频
const context = await browser.newContext({
  recordVideo: process.env.RECORD_VIDEO ? {
    dir: 'videos/',
    size: { width: 1024, height: 768 }
  } : undefined
});
截图最佳实践
  1. 用途明确的命名:使用描述性文件名(如login-error-state.png
  2. 保持一致的视口大小:在截图前设置统一的视口大小
  3. 在CI系统中自动执行截图:结合持续集成自动生成和存储截图
  4. 处理动态内容:在截图前隐藏或模拟时间戳等动态元素

2. 测试框架集成

Playwright 可以与多种测试框架无缝集成,提升测试流程效率。

与 Jest 集成
// 安装依赖:
// npm install -D jest playwright @playwright/test jest-playwright-preset

// jest.config.js
module.exports = {
  preset: 'jest-playwright-preset',
  testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'],
  testEnvironmentOptions: {
    'jest-playwright': {
      browsers: ['chromium', 'firefox', 'webkit'],
      launchOptions: {
        headless: true
      }
    }
  }
};

// login.test.js
describe('登录功能', () => {
  beforeAll(async () => {
    await page.goto('https://example.com/login');
  });

  test('成功登录', async () => {
    await page.fill('#username', 'testuser');
    await page.fill('#password', 'password123');
    await page.click('button[type="submit"]');
    
    await expect(page).toHaveURL('https://example.com/dashboard');
    const welcomeText = await page.textContent('.welcome-message');
    expect(welcomeText).toContain('Welcome, Test User');
  });
});
与 Mocha 集成
// 安装依赖:
// npm install -D mocha @playwright/test

// test/login.spec.js
const { chromium } = require('playwright');
const { expect } = require('chai');

describe('登录功能测试', () => {
  let browser;
  let page;

  before(async () => {
    browser = await chromium.launch();
  });

  beforeEach(async () => {
    page = await browser.newPage();
    await page.goto('https://example.com/login');
  });

  afterEach(async () => {
    await page.close();
  });

  after(async () => {
    await browser.close();
  });

  it('应该成功登录', async () => {
    await page.fill('#username', 'testuser');
    await page.fill('#password', 'password123');
    await page.click('button[type="submit"]');
    
    expect(page.url()).to.include('/dashboard');
    const welcomeText = await page.textContent('.welcome-message');
    expect(welcomeText).to.contain('Welcome, Test User');
  });
});
与 Cucumber 集成
// 安装依赖:
// npm install -D @cucumber/cucumber playwright

// features/login.feature
Feature: 用户登录
  用户应该能够登录到系统

  Scenario: 成功登录
    Given 用户在登录页面
    When 用户输入有效的用户名和密码
    And 用户点击登录按钮
    Then 用户应该被重定向到仪表板
    And 用户应该看到欢迎消息

// step-definitions/login.steps.js
const { Given, When, Then } = require('@cucumber/cucumber');
const { chromium } = require('playwright');
const { expect } = require('chai');

let browser;
let page;

Given('用户在登录页面', async function() {
  browser = await chromium.launch();
  page = await browser.newPage();
  await page.goto('https://example.com/login');
});

When('用户输入有效的用户名和密码', async function() {
  await page.fill('#username', 'testuser');
  await page.fill('#password', 'password123');
});

When('用户点击登录按钮', async function() {
  await page.click('button[type="submit"]');
});

Then('用户应该被重定向到仪表板', async function() {
  await page.waitForURL('**/dashboard');
  expect(page.url()).to.include('/dashboard');
});

Then('用户应该看到欢迎消息', async function() {
  const welcomeText = await page.textContent('.welcome-message');
  expect(welcomeText).to.contain('Welcome, Test User');
  await browser.close();
});
测试框架集成最佳实践
  1. 选择适合团队的框架:根据团队熟悉度和项目需求选择
  2. 保持简单的测试结构:使用页面对象模式组织测试代码
  3. 复用测试步骤:提取常见操作到辅助函数中
  4. 配置共享环境:使用配置文件共享浏览器设置
  5. 统一断言风格:在项目中保持一致的断言方式

3. 移动设备模拟

Playwright 提供了完善的移动设备模拟功能,帮助测试响应式网站行为。

设备模拟基础
const { devices } = require('@playwright/test');

// 使用预定义设备
test.use({ ...devices['iPhone 13'] });

test('在iPhone上访问网站', async ({ page }) => {
  await page.goto('https://example.com');
  
  // 验证移动版UI元素
  const mobileMenu = page.locator('.mobile-menu-button');
  await expect(mobileMenu).toBeVisible();
  
  // 测试汉堡菜单点击
  await mobileMenu.click();
  await expect(page.locator('.mobile-menu-dropdown')).toBeVisible();
});
自定义设备配置
// 自定义设备配置
test.use({
  viewport: { width: 375, height: 812 },
  deviceScaleFactor: 3,
  isMobile: true,
  hasTouch: true,
  userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1'
});

test('测试自定义移动设备', async ({ page }) => {
  await page.goto('https://example.com');
  // 检查响应式行为
});
模拟触摸事件
// 模拟触摸事件
test('测试滑动手势', async ({ page }) => {
  await page.goto('https://example.com/photos');
  
  // 获取轮播图元素
  const carousel = page.locator('.image-carousel');
  const boundingBox = await carousel.boundingBox();
  
  // 执行从右向左的滑动手势
  await page.touchscreen.tap(
    boundingBox.x + boundingBox.width - 50, 
    boundingBox.y + boundingBox.height / 2
  );
  await page.mouse.down();
  await page.mouse.move(
    boundingBox.x + 50, 
    boundingBox.y + boundingBox.height / 2
  );
  await page.mouse.up();
  
  // 验证轮播图已切换到下一张
  await expect(page.locator('.carousel-indicator.active')).toHaveAttribute('data-index', '1');
});
测试横竖屏切换
// 测试横竖屏切换
test('测试横竖屏响应', async ({ browser }) => {
  // iPhone 13 竖屏
  const verticalContext = await browser.newContext({
    ...devices['iPhone 13'],
  });
  const verticalPage = await verticalContext.newPage();
  await verticalPage.goto('https://example.com');
  
  // 检查竖屏布局
  const verticalMenuWidth = await verticalPage.locator('nav').evaluate(el => el.offsetWidth);
  
  // iPhone 13 横屏
  const landscapeContext = await browser.newContext({
    ...devices['iPhone 13 landscape'],
  });
  const landscapePage = await landscapeContext.newPage();
  await landscapePage.goto('https://example.com');
  
  // 检查横屏布局
  const landscapeMenuWidth = await landscapePage.locator('nav').evaluate(el => el.offsetWidth);
  
  // 验证响应式设计按预期工作
  expect(landscapeMenuWidth).toBeGreaterThan(verticalMenuWidth);
  
  await verticalContext.close();
  await landscapeContext.close();
});
移动设备模拟最佳实践
  1. 测试关键设备:至少测试iOS和Android的主流屏幕尺寸
  2. 检查触摸操作区域:确保点击目标足够大(至少48×48像素)
  3. 测试手势操作:包括滑动、捏合等常见手势
  4. 验证媒体查询:确认响应式断点正确触发
  5. 测试性能:在移动设备配置下检查页面加载性能

4. 网络请求拦截与模拟

Playwright 提供了强大的网络请求拦截和模拟功能,可以测试各种网络场景。

注意:以下API示例基于Playwright v1.32.0,请参考最新文档了解可能的变更。

基本请求拦截
// 拦截请求并修改响应
test('拦截和修改API响应', async ({ page }) => {
  // 拦截API请求
  await page.route('**/api/users', route => {
    // 返回自定义数据
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([
        { id: 1, name: '测试用户1' },
        { id: 2, name: '测试用户2' }
      ])
    });
  });
  
  await page.goto('https://example.com/users');
  
  // 验证页面显示了模拟数据
  const userElements = page.locator('.user-item');
  await expect(userElements).toHaveCount(2);
  await expect(page.locator('.user-item:first-child')).toContainText('测试用户1');
});
模拟网络错误
// 模拟网络错误
test('处理API错误', async ({ page }) => {
  // 模拟API服务器错误
  await page.route('**/api/products', route => {
    route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: JSON.stringify({ error: '服务器内部错误' })
    });
  });
  
  await page.goto('https://example.com/products');
  
  // 验证错误消息显示
  await expect(page.locator('.error-message')).toBeVisible();
  await expect(page.locator('.error-message')).toContainText('无法加载产品');
});
网络请求验证
// 验证发送的请求参数
test('验证搜索请求参数', async ({ page }) => {
  let requestData = null;
  
  // 拦截并检查请求
  await page.route('**/api/search', route => {
    requestData = route.request().postDataJSON();
    route.continue(); // 允许请求继续
  });
  
  await page.goto('https://example.com/search');
  await page.fill('#search-input', '测试关键词');
  await page.click('#search-button');
  
  // 验证搜索请求参数
  expect(requestData).toEqual({
    query: '测试关键词',
    page: 1
  });
});
模拟慢速网络
// 模拟慢速网络
test('在慢速连接下测试加载状态', async ({ page }) => {
  // 模拟3G网络
  await page.route('**/*', route => {
    // 为所有请求添加延迟
    setTimeout(() => {
      route.continue();
    }, 300); // 添加300ms延迟
  });
  
  await page.goto('https://example.com/dashboard');
  
  // 验证加载指示器显示
  await expect(page.locator('.loading-spinner')).toBeVisible();
  
  // 等待内容最终加载
  await expect(page.locator('.dashboard-content')).toBeVisible();
  await expect(page.locator('.loading-spinner')).toBeHidden();
});
模拟离线状态
// 模拟离线状态
test('测试离线模式', async ({ browser }) => {
  // 创建离线上下文
  const context = await browser.newContext({
    offline: true
  });
  
  const page = await context.newPage();
  
  // 访问页面
  await page.goto('https://example.com').catch(e => {
    // 预期会有网络错误
    expect(e.message).toContain('net::ERR_INTERNET_DISCONNECTED');
  });
  
  // 如果有离线页面,测试其是否正常显示
  if (await page.locator('.offline-message').isVisible()) {
    await expect(page.locator('.offline-message')).toContainText('您当前处于离线状态');
  }
  
  await context.close();
});
高级请求处理
// 条件请求处理
test('根据请求参数条件处理', async ({ page }) => {
  // 处理不同参数的请求
  await page.route('**/api/products**', route => {
    const url = route.request().url();
    const params = new URL(url).searchParams;
    const category = params.get('category');
    
    if (category === 'electronics') {
      route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify([
          { id: 1, name: '智能手机', price: 1999 },
          { id: 2, name: '笔记本电脑', price: 5999 }
        ])
      });
    } else if (category === 'books') {
      route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify([
          { id: 3, name: '编程指南', price: 59 },
          { id: 4, name: '科幻小说', price: 45 }
        ])
      });
    } else {
      // 默认响应
      route.continue();
    }
  });
  
  // 测试电子产品类别
  await page.goto('https://example.com/products?category=electronics');
  await expect(page.locator('.product-item')).toHaveCount(2);
  await expect(page.locator('.product-item:first-child')).toContainText('智能手机');
  
  // 测试图书类别
  await page.goto('https://example.com/products?category=books');
  await expect(page.locator('.product-item')).toHaveCount(2);
  await expect(page.locator('.product-item:first-child')).toContainText('编程指南');
});
网络请求处理最佳实践
  1. 选择性拦截:只拦截需要模拟的请求,允许其他请求正常进行
  2. 保持数据一致性:确保模拟的响应格式与真实API一致
  3. 测试各种网络状况:包括慢速、离线和间歇性连接
  4. 验证请求参数:确保应用程序发送正确的请求数据
  5. 模拟常见错误状态:测试应用如何处理401、404、500等错误

5. 并行测试执行

Playwright 支持并行执行测试,大幅提高测试效率。

基本并行配置

playwright.config.js 中配置并行执行:

// playwright.config.js
module.exports = {
  // 并发执行数,设为"1"禁用并发
  workers: 3,
  
  // 在多个浏览器上并行运行
  projects: [
    {
      name: 'Chromium',
      use: { browserName: 'chromium' }
    },
    {
      name: 'Firefox',
      use: { browserName: 'firefox' }
    },
    {
      name: 'WebKit',
      use: { browserName: 'webkit' }
    }
  ]
};
按测试特性分组
// playwright.config.js
module.exports = {
  workers: 3,
  
  // 按测试特性分组
  projects: [
    {
      name: '桌面版 Chrome',
      use: { 
        browserName: 'chromium',
        viewport: { width: 1280, height: 720 }
      },
      testMatch: /.*desktop.spec.js/
    },
    {
      name: '移动版 Chrome',
      use: { 
        browserName: 'chromium',
        ...devices['Pixel 5'] 
      },
      testMatch: /.*mobile.spec.js/
    },
    {
      name: '电子商务测试',
      use: { browserName: 'chromium' },
      testDir: './tests/e-commerce'
    }
  ]
};
数据依赖处理

处理依赖共享数据的测试:

// 使用全局设置创建测试数据
// global-setup.js
const { chromium } = require('@playwright/test');
const fs = require('fs');

module.exports = async () => {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();
  
  // 登录并获取认证令牌
  await page.goto('https://example.com/login');
  await page.fill('#username', 'admin');
  await page.fill('#password', 'admin123');
  await page.click('button[type="submit"]');
  
  // 等待登录完成
  await page.waitForURL('**/dashboard');
  
  // 获取认证状态(例如,保存 cookies)
  const authData = await context.storageState();
  
  // 保存认证状态到文件
  fs.writeFileSync('auth-state.json', JSON.stringify(authData));
  
  await browser.close();
};

// playwright.config.js
module.exports = {
  globalSetup: './global-setup.js',
  
  // 测试配置
  projects: [
    {
      name: '已验证用户',
      use: {
        // 使用预先保存的认证状态
        storageState: 'auth-state.json'
      }
    }
  ]
};
测试隔离与共享状态
// 每个测试文件独立执行设置
// tests/admin-panel.spec.js
const { test, expect } = require('@playwright/test');

// 文件级别的设置,所有测试共享
test.beforeAll(async ({ browser }) => {
  // 执行测试所需的前置准备
  const context = await browser.newContext();
  const page = await context.newPage();
  
  await page.goto('https://example.com/setup');
  await page.fill('#entity-name', 'Test Entity ' + Date.now());
  await page.click('#create-entity');
  
  // 保存全局状态供测试使用
  test.setTimeout(60000); // 增加超时时间
  test.info().annotations.push({
    type: 'entity',
    description: await page.locator('#entity-id').textContent()
  });
  
  await context.close();
});

// 清理测试数据
test.afterAll(async ({ browser }) => {
  const context = await browser.newContext();
  const page = await context.newPage();
  
  // 删除测试数据
  await page.goto('https://example.com/cleanup');
  const entityId = test.info().annotations.find(a => a.type === 'entity').description;
  await page.fill('#entity-id', entityId);
  await page.click('#delete-entity');
  
  await context.close();
});
并行测试最佳实践
  1. 合理设置工作进程数:通常为CPU核心数的1-2倍
  2. 保持测试独立性:避免测试间的依赖关系
  3. 使用唯一标识符:避免测试数据冲突(使用时间戳或UUID)
  4. 监控资源使用:防止过多的并行测试导致资源耗尽
  5. 使用全局设置:预先创建需要的测试数据
  6. 考虑测试顺序:对于不能并行的测试,使用串行执行
高级并行策略
// playwright.config.js
const os = require('os');

module.exports = {
  // 根据系统资源动态调整并发数
  workers: process.env.CI ? 4 : Math.max(os.cpus().length - 1, 1),
  
  // 分阶段执行测试
  projects: [
    // 先执行关键路径测试
    {
      name: '关键路径',
      testMatch: /.*critical.spec.js/,
      retries: 2 // 关键测试失败时重试
    },
    // 再执行功能测试
    {
      name: '功能测试',
      testMatch: /.*feature.spec.js/,
      dependencies: ['关键路径'] // 等待关键路径测试完成
    },
    // 最后执行其他测试
    {
      name: '边缘情况',
      testMatch: /.*edge-case.spec.js/,
      dependencies: ['功能测试']
    }
  ],
  
  // 全局超时设置
  timeout: 30000,
  
  // 报告配置
  reporter: [
    ['list'],
    ['html', { open: 'never' }],
    ['junit', { outputFile: 'test-results.xml' }]
  ]
};

高级应用

1. 身份验证管理

有效管理身份验证状态对于测试需要登录的应用程序至关重要。Playwright提供了多种方式来处理身份验证。

会话存储与重用
// 保存身份验证状态,避免重复登录
const { chromium } = require('@playwright/test');

async function authenticateAndSaveState() {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();
  
  // 执行登录
  await page.goto('https://example.com/login');
  await page.fill('#username', 'testuser');
  await page.fill('#password', 'password123');
  await page.click('button[type="submit"]');
  
  // 等待登录成功
  await page.waitForURL('**/dashboard');
  
  // 保存认证状态到文件
  await context.storageState({ path: 'auth.json' });
  
  await browser.close();
}

// 在测试中使用保存的认证状态
test.use({ storageState: 'auth.json' });

test('已登录状态测试', async ({ page }) => {
  await page.goto('https://example.com/profile');
  // 无需登录,直接进行已认证状态的测试
  await expect(page.locator('.user-name')).toContainText('testuser');
});
多角色测试
// 不同用户角色的认证管理
// auth-setup.js
const { chromium } = require('@playwright/test');
const fs = require('fs');

async function setupAuth() {
  // 创建角色目录
  if (!fs.existsSync('./auth')) {
    fs.mkdirSync('./auth');
  }
  
  // 管理员登录
  await loginAs('admin', 'admin123', './auth/admin.json');
  
  // 普通用户登录
  await loginAs('user', 'user123', './auth/user.json');
  
  // 访客用户(只做最低权限验证)
  await loginAs('guest', 'guest123', './auth/guest.json');
}

async function loginAs(username, password, savePathFile) {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();
  
  await page.goto('https://example.com/login');
  await page.fill('#username', username);
  await page.fill('#password', password);
  await page.click('button[type="submit"]');
  
  await page.waitForURL('**/dashboard');
  await context.storageState({ path: savePathFile });
  
  await browser.close();
}

module.exports = setupAuth;

// playwright.config.js中配置
// projects: [
//   {
//     name: '管理员测试',
//     use: { storageState: './auth/admin.json' },
//     testMatch: /.*admin.spec.js/
//   },
//   {
//     name: '用户测试',
//     use: { storageState: './auth/user.json' },
//     testMatch: /.*user.spec.js/
//   },
//   {
//     name: '访客测试',
//     use: { storageState: './auth/guest.json' },
//     testMatch: /.*guest.spec.js/
//   }
// ]
处理双因素认证
// 处理双因素认证
test('处理双因素认证', async ({ page }) => {
  // 常规登录
  await page.goto('https://example.com/login');
  await page.fill('#username', 'testuser');
  await page.fill('#password', 'password123');
  await page.click('button[type="submit"]');
  
  // 等待2FA页面加载
  await page.waitForURL('**/2fa');
  
  // 模拟从身份验证器应用获取代码
  // 实际测试中,可能需要集成第三方库生成OTP代码
  const otpCode = generateOtpCode('JBSWY3DPEHPK3PXP'); // 示例密钥
  
  await page.fill('#otp-code', otpCode);
  await page.click('#verify-button');
  
  // 验证登录成功
  await page.waitForURL('**/dashboard');
  await expect(page.locator('.welcome-message')).toBeVisible();
});

// 辅助函数:生成OTP代码(示例)
function generateOtpCode(secret) {
  // 在实际测试中,使用像otplib这样的库
  // const { authenticator } = require('otplib');
  // return authenticator.generate(secret);
  
  // 简化示例,返回固定代码
  return '123456';
}
身份验证最佳实践
  1. 分离认证逻辑:将认证步骤与测试业务逻辑分开
  2. 重用认证状态:通过保存和加载认证状态,避免每次测试都登录
  3. 测试权限边界:确保针对不同用户角色进行测试
  4. 安全处理凭据:避免在代码中硬编码敏感信息,使用环境变量
  5. 模拟认证服务:在单元测试中考虑模拟认证服务

2. 高级事件处理

现代web应用包含许多复杂的事件和异步行为,Playwright提供了强大的工具来测试这些情况。

WebSocket监听与测试
// 测试WebSocket连接
test('WebSocket通信测试', async ({ page }) => {
  // 创建WebSocket消息收集器
  const wsMessages = [];
  
  // 监听WebSocket通信
  page.on('websocket', ws => {
    console.log(`WebSocket连接已打开: ${ws.url()}`);
    
    ws.on('framesent', event => {
      wsMessages.push({type: 'sent', data: event.payload});
    });
    
    ws.on('framereceived', event => {
      wsMessages.push({type: 'received', data: event.payload});
    });
    
    ws.on('close', () => {
      console.log('WebSocket连接已关闭');
    });
  });
  
  // 导航到使用WebSocket的页面
  await page.goto('https://example.com/chat');
  
  // 触发WebSocket通信
  await page.fill('#message-input', '测试消息');
  await page.click('#send-button');
  
  // 等待接收响应
  await page.waitForTimeout(1000);
  
  // 验证WebSocket通信
  const sentMessages = wsMessages.filter(m => m.type === 'sent');
  expect(sentMessages.some(m => m.data.includes('测试消息'))).toBeTruthy();
  
  // 验证UI响应
  await expect(page.locator('.message-list .message').last()).toContainText('测试消息');
});
监听网络事件
// 高级网络事件监听
test('跟踪所有网络请求与响应', async ({ page }) => {
  // 存储请求和响应信息
  const requests = [];
  const responses = [];
  const requestsFailed = [];
  const requestsFinished = [];
  
  // 监听网络事件
  page.on('request', request => {
    requests.push({
      url: request.url(),
      method: request.method(),
      headers: request.headers(),
      postData: request.postData()
    });
  });
  
  page.on('response', response => {
    responses.push({
      url: response.url(),
      status: response.status(),
      headers: response.headers()
    });
  });
  
  page.on('requestfailed', request => {
    requestsFailed.push({
      url: request.url(),
      failure: request.failure()
    });
  });
  
  page.on('requestfinished', request => {
    requestsFinished.push({
      url: request.url(),
      size: request.sizes()
    });
  });
  
  // 访问页面并执行操作
  await page.goto('https://example.com/dashboard');
  await page.click('#refresh-data');
  
  // 等待所有网络活动完成
  await page.waitForLoadState('networkidle');
  
  // 分析网络活动
  console.log(`总请求数: ${requests.length}`);
  console.log(`成功响应数: ${responses.filter(r => r.status < 400).length}`);
  console.log(`失败请求数: ${requestsFailed.length}`);
  
  // 验证关键API调用
  expect(requests.some(r => r.url.includes('/api/dashboard-data'))).toBeTruthy();
  
  // 检查是否有错误请求
  const errResponses = responses.filter(r => r.status >= 400);
  expect(errResponses).toHaveLength(0);
});
Service Worker测试
// Service Worker测试
test('测试Service Worker功能', async ({ page }) => {
  // 监听Service Worker
  page.on('serviceworker', worker => {
    console.log('Service Worker已启动:', worker.url());
  });
  
  // 导航到使用Service Worker的页面
  await page.goto('https://example.com/pwa');
  
  // 确认Service Worker已注册
  const swRegistered = await page.evaluate(() => {
    return navigator.serviceWorker.controller !== null;
  });
  expect(swRegistered).toBeTruthy();
  
  // 测试离线功能
  // 1. 先缓存必要资源
  await page.click('#cache-resources');
  
  // 2. 启用离线模式
  await page.context().setOffline(true);
  
  // 3. 刷新页面测试离线访问
  await page.reload();
  
  // 4. 验证离线内容可访问
  await expect(page.locator('h1')).toBeVisible();
  await expect(page.locator('.offline-ready')).toBeVisible();
  
  // 恢复在线状态
  await page.context().setOffline(false);
});
文件下载与上传测试
// 文件下载测试
test('测试文件下载', async ({ page }) => {
  // 设置下载路径
  const downloadPath = path.join(__dirname, 'downloads');
  if (!fs.existsSync(downloadPath)) {
    fs.mkdirSync(downloadPath);
  }
  
  // 配置下载行为
  page.context().on('download', async download => {
    console.log('开始下载:', download.suggestedFilename());
    
    // 等待下载完成,并获取保存路径
    const downloadedPath = await download.path();
    
    // 将文件保存到指定目录
    const filePath = path.join(downloadPath, download.suggestedFilename());
    await download.saveAs(filePath);
    
    console.log('文件已保存到:', filePath);
  });
  
  // 访问下载页面
  await page.goto('https://example.com/downloads');
  
  // 触发文件下载
  await page.click('#download-csv');
  
  // 等待下载完成
  // 注意:需要一种方式来确认下载完成
  await page.waitForTimeout(3000); // 简化示例,实际应使用更可靠的方法
  
  // 验证文件存在
  const expectedFile = path.join(downloadPath, 'data.csv');
  expect(fs.existsSync(expectedFile)).toBeTruthy();
  
  // 验证文件内容
  const fileContent = fs.readFileSync(expectedFile, 'utf8');
  expect(fileContent).toContain('id,name,value');
});

// 文件上传测试
test('测试文件上传', async ({ page }) => {
  // 创建测试文件
  const testFilePath = path.join(__dirname, 'test-upload.txt');
  fs.writeFileSync(testFilePath, 'This is a test file for upload');
  
  // 访问上传页面
  await page.goto('https://example.com/upload');
  
  // 上传文件
  await page.setInputFiles('input[type="file"]', testFilePath);
  
  // 提交表单
  await page.click('#upload-button');
  
  // 等待上传完成
  await expect(page.locator('.upload-success')).toBeVisible();
  
  // 验证上传后的文件名显示
  const displayedName = await page.textContent('.uploaded-file-name');
  expect(displayedName).toBe('test-upload.txt');
  
  // 清理测试文件
  fs.unlinkSync(testFilePath);
});
事件处理最佳实践
  1. 超时管理:为异步操作设置合理的超时时间
  2. 错误处理:捕获并记录事件处理过程中的错误
  3. 稳定性考虑:处理间歇性网络问题和重试逻辑
  4. 模拟条件:测试各种网络状况和边缘情况
  5. 隔离测试:确保测试间不会相互干扰事件监听

3. 可访问性测试

确保网站对所有用户可访问是现代网页开发的重要部分。Playwright可以集成可访问性测试工具。

基本可访问性审查
// 安装依赖:
// npm install -D axe-playwright

// 使用Axe进行可访问性测试
const { AxeBuilder } = require('@axe-core/playwright');

test('基本可访问性检查', async ({ page }) => {
  await page.goto('https://example.com');
  
  // 运行可访问性检查
  const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
  
  // 断言没有违规
  expect(accessibilityScanResults.violations).toHaveLength(0);
});

// 特定部分的可访问性检查
test('登录表单可访问性', async ({ page }) => {
  await page.goto('https://example.com/login');
  
  // 针对特定元素进行可访问性检查
  const loginForm = await page.locator('#login-form');
  const results = await new AxeBuilder({ page })
    .include('#login-form')
    .analyze();
  
  // 输出违规详情用于调试
  if (results.violations.length > 0) {
    console.log(JSON.stringify(results.violations, null, 2));
  }
  
  expect(results.violations).toHaveLength(0);
});
自定义可访问性规则
// 自定义可访问性规则
test('使用自定义规则集进行检查', async ({ page }) => {
  await page.goto('https://example.com/dashboard');
  
  // 配置特定规则
  const results = await new AxeBuilder({ page })
    .withRules(['color-contrast', 'aria-roles', 'image-alt'])
    .analyze();
  
  expect(results.violations).toHaveLength(0);
});

// 排除特定元素
test('排除第三方内容的可访问性检查', async ({ page }) => {
  await page.goto('https://example.com/with-third-party');
  
  // 排除第三方小部件
  const results = await new AxeBuilder({ page })
    .exclude('.third-party-widget')
    .exclude('iframe[src*="ads.example.com"]')
    .analyze();
  
  expect(results.violations).toHaveLength(0);
});
生成可访问性报告
// 生成详细的可访问性报告
test('生成HTML可访问性报告', async ({ page }) => {
  await page.goto('https://example.com');
  
  // 运行检查
  const results = await new AxeBuilder({ page }).analyze();
  
  // 保存结果为HTML报告
  const reportHTML = `
    <!DOCTYPE html>
    <html>
    <head>
      <title>可访问性审查报告</title>
      <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .violation { background: #ffebee; padding: 10px; margin-bottom: 10px; border-radius: 4px; }
        .pass { background: #e8f5e9; padding: 10px; margin-bottom: 10px; border-radius: 4px; }
        h2 { color: #d32f2f; }
        h3 { margin-top: 0; }
        pre { background: #f5f5f5; padding: 10px; overflow: auto; }
      </style>
    </head>
    <body>
      <h1>可访问性审查报告 - ${new Date().toLocaleString()}</h1>
      <p>URL: ${page.url()}</p>
      
      <h2>违规 (${results.violations.length})</h2>
      ${results.violations.map(v => `
        <div class="violation">
          <h3>${v.id}: ${v.help}</h3>
          <p>影响: ${v.impact} | ${v.nodes.length} 个问题</p>
          <p>${v.helpUrl}</p>
          <pre>${JSON.stringify(v.nodes.map(n => n.html), null, 2)}</pre>
        </div>
      `).join('')}
      
      <h2>通过 (${results.passes.length})</h2>
      ${results.passes.map(p => `
        <div class="pass">
          <h3>${p.id}: ${p.help}</h3>
          <p>${p.nodes.length} 个元素通过</p>
        </div>
      `).join('')}
    </body>
    </html>
  `;
  
  // 保存报告
  fs.writeFileSync('accessibility-report.html', reportHTML);
  
  // 验证是否有严重违规
  const severeViolations = results.violations.filter(v => v.impact === 'critical' || v.impact === 'serious');
  expect(severeViolations).toHaveLength(0);
});
可访问性测试最佳实践
  1. 集成到CI/CD:将可访问性测试作为持续集成的一部分
  2. 设置基线:先建立基准,逐步解决问题
  3. 优先处理严重问题:先解决严重和关键级别的可访问性问题
  4. 测试键盘导航:确保网站可以仅使用键盘操作
  5. 考虑屏幕阅读器:测试与屏幕阅读器的兼容性
  6. 检查颜色对比度:确保文本与背景的对比度符合标准
  7. 检查ARIA属性:验证ARIA属性的正确使用

4. 性能测试与监控

Playwright 可以用于基本的性能测试和监控,帮助识别网站的性能瓶颈。

基本性能指标收集
// 基本性能指标收集
test('收集页面性能指标', async ({ page }) => {
  // 导航到目标页面并收集性能数据
  const navigationTimingJson = await page.evaluate(() => JSON.stringify(performance.timing));
  const navigationTiming = JSON.parse(navigationTimingJson);
  
  // 计算关键性能指标
  const pageLoadTime = navigationTiming.loadEventEnd - navigationTiming.navigationStart;
  const domContentLoadedTime = navigationTiming.domContentLoadedEventEnd - navigationTiming.navigationStart;
  const firstPaintTime = await page.evaluate(() => {
    const paintMetrics = performance.getEntriesByType('paint');
    return paintMetrics.find(metric => metric.name === 'first-paint')?.startTime;
  });
  
  console.log(`页面加载时间: ${pageLoadTime}ms`);
  console.log(`DOM内容加载时间: ${domContentLoadedTime}ms`);
  console.log(`首次绘制时间: ${firstPaintTime}ms`);
  
  // 性能断言(根据实际要求调整阈值)
  expect(pageLoadTime).toBeLessThan(3000); // 页面加载时间小于3秒
  expect(firstPaintTime).toBeLessThan(1000); // 首次绘制时间小于1秒
});
资源加载性能
// 分析资源加载性能
test('分析资源加载性能', async ({ page }) => {
  // 创建性能观察器
  const resourcesInfo = [];
  
  // 监听所有资源请求
  page.on('requestfinished', async request => {
    const response = request.response();
    if (!response) return;
    
    try {
      const responseBody = response.status() !== 304 ? await response.body() : '';
      resourcesInfo.push({
        url: request.url(),
        resourceType: request.resourceType(),
        status: response.status(),
        size: responseBody.length,
        duration: response.timing().responseEnd - response.timing().requestStart,
        timing: response.timing()
      });
    } catch (error) {
      console.error('无法获取响应体:', error);
    }
  });
  
  // 导航到目标页面
  await page.goto('https://example.com', { waitUntil: 'networkidle' });
  
  // 汇总资源类型
  const resourceTypeSummary = resourcesInfo.reduce((acc, resource) => {
    const type = resource.resourceType;
    if (!acc[type]) {
      acc[type] = {
        count: 0,
        totalSize: 0,
        totalDuration: 0
      };
    }
    
    acc[type].count++;
    acc[type].totalSize += resource.size;
    acc[type].totalDuration += resource.duration;
    
    return acc;
  }, {});
  
  // 输出资源加载摘要
  console.log('资源加载摘要:');
  for (const [type, stats] of Object.entries(resourceTypeSummary)) {
    console.log(`类型: ${type}, 数量: ${stats.count}, 总大小: ${stats.totalSize / 1024}KB, 平均加载时间: ${stats.totalDuration / stats.count}ms`);
  }
  
  // 识别大型资源
  const largeResources = resourcesInfo
    .filter(r => r.size > 1024 * 500) // 大于500KB的资源
    .sort((a, b) => b.size - a.size);
  
  console.log('大型资源:');
  largeResources.forEach(r => {
    console.log(`URL: ${r.url}, 类型: ${r.resourceType}, 大小: ${r.size / 1024}KB`);
  });
  
  // 验证性能要求
  const jsResources = resourcesInfo.filter(r => r.resourceType === 'script');
  const totalJsSize = jsResources.reduce((sum, r) => sum + r.size, 0) / 1024;
  
  expect(totalJsSize).toBeLessThan(1000); // JavaScript总大小小于1MB
});
使用Lighthouse集成
// 使用Lighthouse进行性能测试
// 需要安装: npm install -D lighthouse chrome-launcher

const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const { test, expect } = require('@playwright/test');
const fs = require('fs');
const path = require('path');

test('使用Lighthouse进行性能分析', async () => {
  // 启动Chrome
  const chrome = await chromeLauncher.launch({
    chromeFlags: ['--headless', '--disable-gpu', '--no-sandbox']
  });
  
  // 运行Lighthouse分析
  const options = {
    logLevel: 'info',
    output: 'html',
    port: chrome.port,
    onlyCategories: ['performance', 'accessibility', 'best-practices', 'seo']
  };
  
  const result = await lighthouse('https://example.com', options);
  
  // 确保报告目录存在
  const reportDir = path.join(__dirname, 'lighthouse-reports');
  if (!fs.existsSync(reportDir)) {
    fs.mkdirSync(reportDir);
  }
  
  // 保存HTML报告
  const reportPath = path.join(reportDir, `lighthouse-report-${Date.now()}.html`);
  fs.writeFileSync(reportPath, result.report);
  
  // 打印性能分数
  console.log(`性能得分: ${result.lhr.categories.performance.score * 100}/100`);
  console.log(`可访问性得分: ${result.lhr.categories.accessibility.score * 100}/100`);
  console.log(`最佳实践得分: ${result.lhr.categories['best-practices'].score * 100}/100`);
  console.log(`SEO得分: ${result.lhr.categories.seo.score * 100}/100`);
  
  // 性能断言
  expect(result.lhr.categories.performance.score).toBeGreaterThanOrEqual(0.7); // 性能得分>=70
  expect(result.lhr.categories.accessibility.score).toBeGreaterThanOrEqual(0.9); // 可访问性得分>=90
  
  // 关闭Chrome
  await chrome.kill();
});
跟踪用户交互时间
// 测量用户交互响应时间
test('测量页面交互响应性能', async ({ page }) => {
  await page.goto('https://example.com/app');
  
  // 等待页面完全加载
  await page.waitForLoadState('networkidle');
  
  // 性能测量:切换标签页响应时间
  const tabs = page.locator('.tab');
  const tabCount = await tabs.count();
  
  const tabSwitchTimes = [];
  
  for (let i = 0; i < tabCount; i++) {
    // 记录切换开始时间
    const startTime = Date.now();
    
    // 点击标签
    await tabs.nth(i).click();
    
    // 等待内容加载
    await page.locator(`.tab-content[data-tab="${i}"]`).waitFor();
    
    // 计算响应时间
    const responseTime = Date.now() - startTime;
    tabSwitchTimes.push({ tab: i, time: responseTime });
    
    console.log(`标签 ${i} 响应时间: ${responseTime}ms`);
  }
  
  // 测量表单填写和提交响应时间
  await page.click('#open-form-button');
  await page.waitForSelector('form.user-form');
  
  const formStartTime = Date.now();
  
  // 填写表单字段
  await page.fill('#name', 'Test User');
  await page.fill('#email', 'test@example.com');
  await page.selectOption('#country', 'US');
  await page.fill('#comments', 'This is a performance test');
  
  // 提交表单
  await page.click('button[type="submit"]');
  
  // 等待成功消息
  await page.waitForSelector('.submission-success');
  
  const formSubmitTime = Date.now() - formStartTime;
  console.log(`表单提交总时间: ${formSubmitTime}ms`);
  
  // 性能断言
  expect(Math.max(...tabSwitchTimes.map(t => t.time))).toBeLessThan(300); // 标签切换时间小于300ms
  expect(formSubmitTime).toBeLessThan(2000); // 表单提交时间小于2秒
});
性能测试最佳实践
  1. 设置基准指标:为关键页面和操作设置性能基准,并在测试中验证
  2. 持续监控:定期运行性能测试,查找性能退化
  3. 测试真实环境:在模拟的生产环境进行测试,包括网络节流
  4. 测量关键指标:关注核心Web指标(CWV)如LCP(最大内容绘制, Largest Contentful Paint)、FID(首次输入延迟, First Input Delay)和CLS(累积布局偏移, Cumulative Layout Shift)
  5. 监控资源大小:跟踪JS、CSS和图片资源大小
  6. 整合CI/CD:将性能测试整合到CI/CD流程中
  7. 优化关键路径:基于测试结果优化关键渲染路径

5. 高级调试技巧

复杂测试场景需要强大的调试能力,Playwright提供了多种高级调试工具。

使用Trace Viewer
// 启用和使用Trace Viewer
// playwright.config.js中配置
// use: {
//   trace: 'on-first-retry', // 首次失败时记录
// }

// 单个测试中启用
test('使用Trace调试复杂交互', async ({ page }) => {
  // 启用追踪
  await context.tracing.start({ 
    screenshots: true, 
    snapshots: true 
  });
  
  // 执行测试步骤
  await page.goto('https://example.com/dashboard');
  await page.click('.complex-widget');
  await page.waitForResponse('**/api/widget-data');
  await page.locator('.widget-result').waitFor();
  
  // 断言可能失败的地方
  await expect(page.locator('.result-value')).toContainText('Expected Result');
  
  // 测试完成后保存追踪
  await context.tracing.stop({ path: 'trace.zip' });
  
  // 访问生成的文件: npx playwright show-trace trace.zip
});
调试特定元素
// 元素调试技巧
test('调试复杂的元素选择器', async ({ page }) => {
  await page.goto('https://example.com/complex-ui');
  
  // 暂停执行进行调试
  // 仅在无头模式为false时有效
  // await page.pause();
  
  // 获取元素详细信息
  const element = page.locator('.hard-to-select-element');
  
  // 打印元素信息
  const elementInfo = await element.evaluate(el => {
    return {
      tagName: el.tagName,
      id: el.id,
      className: el.className,
      attributes: Array.from(el.attributes).map(attr => ({
        name: attr.name,
        value: attr.value
      })),
      boundingBox: el.getBoundingClientRect(),
      isVisible: el.checkVisibility && el.checkVisibility(),
      computedStyle: {
        display: getComputedStyle(el).display,
        visibility: getComputedStyle(el).visibility,
        position: getComputedStyle(el).position,
        zIndex: getComputedStyle(el).zIndex,
        opacity: getComputedStyle(el).opacity
      }
    };
  });
  
  console.log('元素信息:', JSON.stringify(elementInfo, null, 2));
  
  // 高亮元素进行视觉调试
  await element.evaluate(el => {
    const originalStyle = el.getAttribute('style') || '';
    el.setAttribute('style', `${originalStyle}; border: 2px solid red !important; background-color: rgba(255, 0, 0, 0.2) !important;`);
    
    // 5秒后恢复原样式
    setTimeout(() => {
      el.setAttribute('style', originalStyle);
    }, 5000);
  });
  
  // 等待高亮效果
  await page.waitForTimeout(1000);
  
  // 截图以便后续分析
  await page.screenshot({ path: 'debug-element.png' });
  
  // 继续测试
  await element.click();
  await expect(page.locator('.result')).toBeVisible();
});
输出DOM快照
// 保存DOM快照进行调试
test('在特定状态捕获DOM快照', async ({ page }) => {
  await page.goto('https://example.com/dynamic-content');
  
  // 进行一些操作
  await page.click('#load-data');
  await page.waitForSelector('.data-loaded', { state: 'visible' });
  
  // 捕获某个区域的HTML快照
  const containerHtml = await page.locator('#dynamic-container').evaluate(el => {
    // 清理可能敏感的数据
    const clone = el.cloneNode(true);
    const sensitiveElements = clone.querySelectorAll('.user-data, .api-key');
    sensitiveElements.forEach(el => {
      el.textContent = '[REDACTED]';
    });
    
    return clone.outerHTML;
  });
  
  // 保存HTML快照
  fs.writeFileSync('dom-snapshot.html', `
    <!DOCTYPE html>
    <html>
    <head>
      <title>DOM快照 - ${new Date().toLocaleString()}</title>
      <style>
        body { font-family: Arial, sans-serif; padding: 20px; }
        .snapshot-container { border: 1px solid #ccc; padding: 15px; margin-top: 20px; }
        .timestamp { color: #666; font-style: italic; }
      </style>
    </head>
    <body>
      <h1>页面DOM快照</h1>
      <p class="timestamp">捕获时间: ${new Date().toLocaleString()}</p>
      <p>URL: ${page.url()}</p>
      
      <h2>Container HTML:</h2>
      <div class="snapshot-container">
        ${containerHtml}
      </div>
    </body>
    </html>
  `);
  
  console.log('DOM快照已保存到: dom-snapshot.html');
});
调试JavaScript错误
// 捕获并分析JavaScript错误
test('监控并分析JavaScript错误', async ({ page }) => {
  // 收集所有JS错误
  const jsErrors = [];
  
  // 监听页面错误
  page.on('pageerror', error => {
    jsErrors.push({
      message: error.message,
      stack: error.stack,
      timestamp: new Date().toISOString()
    });
    
    console.error('页面错误:', error.message);
  });
  
  // 监听控制台错误
  page.on('console', msg => {
    if (msg.type() === 'error') {
      jsErrors.push({
        type: 'console',
        message: msg.text(),
        timestamp: new Date().toISOString()
      });
      
      console.error('控制台错误:', msg.text());
    }
  });
  
  // 访问可能有错误的页面
  await page.goto('https://example.com/page-with-errors');
  
  // 触发可能导致错误的操作
  await page.click('#error-button').catch(e => {
    // 继续执行,我们在收集错误而不是让测试失败
  });
  
  // 等待可能的异步错误
  await page.waitForTimeout(2000);
  
  // 分析收集到的错误
  if (jsErrors.length > 0) {
    console.log(`检测到 ${jsErrors.length} 个JavaScript错误`);
    
    // 保存错误日志
    fs.writeFileSync('js-errors.json', JSON.stringify(jsErrors, null, 2));
    
    // 对于已知的可接受错误,可以进行过滤
    const criticalErrors = jsErrors.filter(err => 
      !err.message.includes('已知且可忽略的错误') &&
      !err.message.includes('第三方脚本错误')
    );
    
    // 只对关键错误进行断言
    expect(criticalErrors).toHaveLength(0);
  } else {
    console.log('未检测到JavaScript错误');
  }
});
通过数据库验证测试
// 使用数据库验证测试结果
// 需要安装: npm install -D mysql2
const mysql = require('mysql2/promise');

test('验证表单提交在数据库中创建了记录', async ({ page }) => {
  // 连接数据库(使用测试数据库)
  const connection = await mysql.createConnection({
    host: 'localhost',
    user: 'test_user',
    password: 'test_password',
    database: 'test_db'
  });
  
  try {
    // 生成唯一标识符以便在数据库中识别
    const uniqueId = `test-${Date.now()}`;
    
    // 检查测试前的记录数
    const [initialRows] = await connection.execute(
      'SELECT COUNT(*) as count FROM contact_submissions WHERE email LIKE ?',
      [`%${uniqueId}%`]
    );
    const initialCount = initialRows[0].count;
    
    // 在页面上执行操作
    await page.goto('https://example.com/contact');
    
    // 填写表单
    await page.fill('#name', 'Test User');
    await page.fill('#email', `user-${uniqueId}@example.com`);
    await page.fill('#message', 'This is a test message');
    
    // 提交表单
    await page.click('#submit-button');
    
    // 等待成功消息
    await expect(page.locator('.success-message')).toBeVisible();
    
    // 等待数据库处理(根据实际情况可能需要调整)
    await page.waitForTimeout(1000);
    
    // 验证数据库中的记录
    const [finalRows] = await connection.execute(
      'SELECT * FROM contact_submissions WHERE email = ?',
      [`user-${uniqueId}@example.com`]
    );
    
    // 验证记录已创建
    expect(finalRows.length).toBe(initialCount + 1);
    
    // 验证记录字段
    const submission = finalRows[0];
    expect(submission.name).toBe('Test User');
    expect(submission.message).toBe('This is a test message');
    
    // 清理测试数据(如果需要)
    await connection.execute(
      'DELETE FROM contact_submissions WHERE email = ?',
      [`user-${uniqueId}@example.com`]
    );
    
  } finally {
    // 确保关闭数据库连接
    await connection.end();
  }
});
调试最佳实践
  1. 使用有意义的快照命名:用描述性名称保存截图和跟踪文件
  2. 记录上下文信息:捕获环境变量、浏览器版本和测试配置
  3. 分析测试数据:出现失败时保存DOM状态和网络请求
  4. 使用隔离的测试环境:避免测试之间的状态污染
  5. 增量调试:逐步添加复杂度来识别问题根源
  6. 保留中间状态:在复杂操作间保存状态,以便回溯分析
  7. 限制重试次数:防止重试掩盖了间歇性问题

6. 跨浏览器测试策略

Web应用需要在各种浏览器中正常工作,Playwright的多浏览器支持使跨浏览器测试变得简单。

配置多浏览器测试
// playwright.config.js中的基本配置
module.exports = {
  // 在所有三种浏览器引擎上运行
  projects: [
    {
      name: 'Chromium',
      use: { browserName: 'chromium' }
    },
    {
      name: 'Firefox',
      use: { browserName: 'firefox' }
    },
    {
      name: 'WebKit',
      use: { browserName: 'webkit' }
    }
  ]
};
处理浏览器特定代码
// 处理浏览器差异
test('跨浏览器兼容性测试', async ({ page, browserName }) => {
  await page.goto('https://example.com');
  
  // 根据浏览器执行不同的步骤
  if (browserName === 'webkit') {
    // Safari特定处理
    console.log('在Safari/WebKit中执行特定步骤');
    await page.click('.safari-compatible-button');
  } else if (browserName === 'firefox') {
    // Firefox特定处理
    console.log('在Firefox中执行特定步骤');
    // Firefox可能需要额外的等待时间
    await page.waitForTimeout(200);
    await page.click('.standard-button');
  } else {
    // Chromium默认处理
    console.log('在Chromium中执行标准步骤');
    await page.click('.standard-button');
  }
  
  // 通用验证步骤
  await expect(page.locator('.result')).toBeVisible();
});
分组浏览器特定测试
// 按浏览器组织测试
// playwright.config.js
module.exports = {
  projects: [
    // 在所有浏览器上运行的基本测试
    {
      name: '跨浏览器',
      testMatch: /.*basic\.spec\.js/,
      use: { browserName: 'chromium' }
    },
    {
      name: '跨浏览器',
      testMatch: /.*basic\.spec\.js/,
      use: { browserName: 'firefox' }
    },
    {
      name: '跨浏览器',
      testMatch: /.*basic\.spec\.js/,
      use: { browserName: 'webkit' }
    },
    
    // 浏览器特定测试
    {
      name: 'Chromium特定',
      testMatch: /.*chromium\.spec\.js/,
      use: { browserName: 'chromium' }
    },
    {
      name: 'Firefox特定',
      testMatch: /.*firefox\.spec\.js/,
      use: { browserName: 'firefox' }
    },
    {
      name: 'WebKit特定',
      testMatch: /.*webkit\.spec\.js/,
      use: { browserName: 'webkit' }
    }
  ]
};
检测浏览器功能
// 检测浏览器功能并相应调整测试
test('根据浏览器功能调整测试', async ({ page, browserName }) => {
  await page.goto('https://example.com/features');
  
  // 检测浏览器是否支持特定功能
  const hasWebP = await page.evaluate(() => {
    return document.createElement('canvas')
      .toDataURL('image/webp')
      .indexOf('data:image/webp') === 0;
  });
  
  const hasWebGL = await page.evaluate(() => {
    try {
      const canvas = document.createElement('canvas');
      return !!(window.WebGLRenderingContext && 
        (canvas.getContext('webgl') || canvas.getContext('experimental-webgl')));
    } catch (e) {
      return false;
    }
  });
  
  // 基于检测到的功能修改测试流程
  if (hasWebP) {
    console.log('浏览器支持WebP格式');
    await page.click('#load-webp-images');
    await expect(page.locator('.webp-container img')).toBeVisible();
  } else {
    console.log('浏览器不支持WebP格式,将使用备用格式');
    await page.click('#load-fallback-images');
    await expect(page.locator('.fallback-container img')).toBeVisible();
  }
  
  if (hasWebGL) {
    console.log('浏览器支持WebGL,测试3D功能');
    await page.click('#enable-3d-view');
    await expect(page.locator('#canvas-3d')).toBeVisible();
  } else {
    console.log('浏览器不支持WebGL,跳过3D功能测试');
    // 跳过WebGL测试,但确保2D备用视图可用
    await expect(page.locator('#fallback-2d-view')).toBeVisible();
  }
});
视觉比较策略
// 视觉比较策略
test('跨浏览器视觉比较', async ({ page, browserName }) => {
  await page.goto('https://example.com/ui-test');
  
  // 为不同浏览器设置不同的比较阈值
  let threshold = 0.2; // 默认允许20%像素差异
  
  // 不同浏览器的字体渲染和像素精度有差异
  if (browserName === 'firefox') {
    threshold = 0.3; // Firefox需要更宽松的阈值
  } else if (browserName === 'webkit') {
    threshold = 0.25; // WebKit也需要略宽松的阈值
  }
  
  // 设置视口大小,确保一致的布局基础
  await page.setViewportSize({ width: 1280, height: 720 });
  
  // 截图并与参考图像比较
  const screenshot = await page.screenshot();
  
  // 使用浏览器特定的参考图像
  const referenceImagePath = `./reference-images/ui-${browserName}.png`;
  
  // 使用图像比较库进行比较(示例)
  // 实际使用时可替换为Playwright的toMatchSnapshot
  expect(screenshot).toMatchSnapshot(referenceImagePath, {
    threshold,
    maxDiffPixelRatio: 0.05 // 最多允许5%的像素差异
  });
});
浏览器版本矩阵
// 使用特定版本的浏览器(使用Docker容器示例)
// 需要安装: npm install -D dockerode

const { test, expect } = require('@playwright/test');
const Docker = require('dockerode');
const docker = new Docker();

/**
 * 在特定版本的浏览器中运行测试
 * @param {string} browserType - 浏览器类型,如'chromium'、'firefox'、'webkit'
 * @param {string} version - 浏览器版本号,如'101'、'99'
 * @param {Function} testFn - 测试函数,将在指定浏览器中执行
 * @returns {Object} - 测试结果对象,包含success和output属性
 */
async function runTestInBrowserVersion(browserType, version, testFn) {
  // 容器名称
  const containerName = `playwright-${browserType}-${version}`;
  
  // 拉取特定版本的浏览器Docker镜像
  console.log(`拉取 ${browserType} ${version} 镜像...`);
  
  // 确定Docker镜像名称
  const imageName = `mcr.microsoft.com/playwright:v1.32.0-${browserType}-${version}`;
  
  try {
    // 创建容器
    const container = await docker.createContainer({
      Image: imageName,
      name: containerName,
      Cmd: ['npx', 'playwright', 'test'],
      // 挂载当前目录
      HostConfig: {
        Binds: [`${process.cwd()}:/app`]
      },
      WorkingDir: '/app'
    });
    
    // 启动容器
    await container.start();
    
    // 执行测试
    const exec = await container.exec({
      Cmd: ['node', '-e', testFn.toString()],
      AttachStdout: true,
      AttachStderr: true
    });
    
    // 启动执行
    const stream = await exec.start();
    
    // 读取输出
    let output = '';
    stream.on('data', chunk => {
      output += chunk.toString();
    });
    
    // 等待执行完成
    await new Promise(resolve => stream.on('end', resolve));
    
    // 停止并移除容器
    await container.stop();
    await container.remove();
    
    return { success: !output.includes('Error'), output };
    
  } catch (error) {
    console.error(`${browserType} ${version} 中运行测试失败:`, error);
    return { success: false, output: error.toString() };
  }
}

// 使用示例
test('跨多个浏览器版本测试', async () => {
  // 测试函数
  const testFunction = async () => {
    const { chromium } = require('playwright');
    const browser = await chromium.launch();
    const page = await browser.newPage();
    await page.goto('https://example.com');
    
    // 测试逻辑
    const title = await page.title();
    console.log(`页面标题: ${title}`);
    
    await browser.close();
  };
  
  // 在多个Chrome版本上运行
  // 注意: 这些版本号需要根据Docker镜像的可用性进行调整
  const versions = ['101', '99', '95'];
  const testResults = {};
  
  for (const version of versions) {
    console.log(`在Chrome ${version}中测试...`);
    testResults[version] = await runTestInBrowserVersion('chromium', version, testFunction);
    console.log(`Chrome ${version}测试结果:`, testResults[version].success ? '通过' : '失败');
  }
  
  // 验证所有版本测试都成功
  const failedVersions = Object.entries(testResults)
    .filter(([_, result]) => !result.success)
    .map(([version]) => version);
  
  expect(failedVersions).toHaveLength(0, 
    `在以下Chrome版本中测试失败: ${failedVersions.join(', ')}`);
});
跨浏览器测试最佳实践
  1. 确定关键浏览器:根据用户数据确定优先测试哪些浏览器
  2. 分层测试策略:所有浏览器上运行核心功能测试,专用浏览器上运行特定测试
  3. 使用特性检测:基于浏览器能力而非浏览器标识来调整测试
  4. 维护单独的基准图像:为每个浏览器保留单独的视觉比较参考图像
  5. 编写跨浏览器兼容代码:避免使用浏览器专有API,或提供回退方案
  6. 自动化浏览器差异处理:使用辅助函数隐藏浏览器间的差异
  7. 监控浏览器覆盖率:确保未忽略重要的浏览器或版本

总结

通过本文详细介绍的Playwright进阶技术,您可以构建更加强大、可靠的自动化测试套件,更好地保障应用程序的质量。

我们涵盖了以下进阶主题:

  1. 截图与视频录制:通过视觉证据捕获测试执行,便于问题诊断和复现
  2. 测试框架集成:将Playwright与Jest、Mocha和Cucumber等框架无缝集成
  3. 移动设备模拟:确保应用在各种移动设备上正常工作
  4. 网络请求拦截与模拟:测试应用对不同API响应的处理能力
  5. 并行测试执行:通过并行化显著提高测试执行速度
  6. 身份验证管理:高效处理需要登录的应用测试场景
  7. 高级事件处理:测试WebSocket、Service Worker等复杂异步行为
  8. 可访问性测试:确保应用对所有用户可用
  9. 性能测试与监控:识别和解决性能瓶颈
  10. 高级调试技巧:使用追踪和状态捕获调试复杂问题
  11. 跨浏览器测试策略:确保应用在所有目标浏览器上工作
基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业),个人经导师指导并认可通过的高分设计项目,评审分99分,代码完整确保可以运行,小白也可以亲自搞定,主要针对计算机相关专业的正在做大作业的学生和需要项目实战练习的学习者,可作为毕业设计、课程设计、期末大作业,代码资料完整,下载可用。 基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业)基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业)基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业)基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业)基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业)基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业)基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业)基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业)基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业)基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业)基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业)基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业)基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业)基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业)基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业)基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业)基于Python的天气预测和天气可视化项目源码+文档说明(高分毕设/大作业
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值