Playwright自动化测试实战指南-基本部分

Playwright自动化测试实战指南

基础部分

1. Playwright安装与环境配置

Playwright是Microsoft开发的一款强大的自动化测试工具,支持Chromium、Firefox和WebKit三大浏览器引擎。它能够在Windows、macOS和Linux系统上运行,提供了跨浏览器、跨平台的自动化测试能力。

版本说明:本指南基于Playwright v1.32.0,某些API在未来版本中可能有变化。

安装Node.js

Playwright基于Node.js运行,首先确保您已安装Node.js(v14或更高版本):

# 检查Node.js版本
node -v

# 检查npm版本
npm -v

如果您尚未安装Node.js,可以从Node.js官网下载最新的LTS版本。

安装Playwright

可以通过npm安装Playwright及其依赖:

# 创建新项目
mkdir playwright-project
cd playwright-project
npm init -y

# 安装Playwright
npm install -D @playwright/test

# 安装浏览器(安装所有支持的浏览器)
npx playwright install

# 也可以只安装特定浏览器
npx playwright install chromium
npx playwright install firefox
npx playwright install webkit
项目初始化

使用Playwright CLI快速创建初始测试环境:

# 生成基础配置和示例测试
npx playwright init

这个命令会引导您完成以下步骤:

  1. 选择测试语言(JavaScript或TypeScript)
  2. 是否需要GitHub Actions配置
  3. 安装浏览器引擎
  4. 创建示例测试

执行完成后,将创建基本的配置文件playwright.config.js(或.ts)和示例测试。

目录结构详解

初始化后,项目结构如下:

playwright-project/
├── playwright.config.js     # Playwright配置文件
├── package.json             # 项目依赖配置
├── package-lock.json        # 依赖锁定文件
├── .github/                 # GitHub Actions配置(如有选择)
│   └── workflows/
│       └── playwright.yml
└── tests/                   # 测试目录
    ├── example.spec.js      # 示例测试
    └── example-fixtures/    # 测试资源目录(可选)

推荐的完整项目结构:

playwright-project/
├── playwright.config.js     # 主配置文件
├── package.json             # 项目依赖
├── tests/                   # 所有测试
│   ├── e2e/                 # 端到端测试
│   ├── api/                 # API测试
│   ├── component/           # 组件测试
│   └── fixtures/            # 测试数据和资源
├── test-results/            # 测试结果和报告(自动生成)
│   ├── screenshots/         # 失败时的截图
│   ├── videos/              # 测试录像
│   └── traces/              # 测试轨迹文件
└── playwright-report/       # HTML测试报告(自动生成)
配置文件详解

playwright.config.js是Playwright的核心配置文件,包含浏览器设置、超时时间、测试环境、报告等重要配置:

// playwright.config.js
const { defineConfig, devices } = require('@playwright/test');

module.exports = defineConfig({
  // 测试文件所在目录
  testDir: './tests',
  
  // 测试超时时间(毫秒)
  timeout: 30000,
  
  // 断言超时时间
  expect: {
    timeout: 5000
  },
  
  // 失败时重试次数
  retries: 2,
  
  // 测试并发运行的工作进程数
  workers: process.env.CI ? 1 : undefined,
  
  // 测试报告配置
  reporter: [
    ['html'], // HTML报告
    ['json', { outputFile: 'test-results/test-results.json' }], // JSON报告
    ['list'] // 控制台报告
  ],
  
  // 全局共享的浏览器上下文配置
  use: {
    // 是否使用无头浏览器
    headless: true,
    
    // 浏览器视口尺寸
    viewport: { width: 1280, height: 720 },
    
    // 忽略HTTPS错误
    ignoreHTTPSErrors: true,
    
    // 截图策略
    screenshot: 'only-on-failure',
    
    // 视频录制策略
    video: 'on-first-retry',
    
    // 跟踪记录
    trace: 'on-first-retry',
    
    // 浏览器上下文持久化
    // storageState: 'storageState.json',
    
    // 默认导航超时
    navigationTimeout: 30000,
    
    // 默认操作超时
    actionTimeout: 15000,
    
    // 基本URL配置,可用于相对URL导航
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
  },
  
  // 针对不同浏览器的测试项目配置
  projects: [
    {
      name: 'chromium',
      use: { 
        browserName: 'chromium',
        // 特定浏览器的额外配置
        launchOptions: {
          args: ['--disable-gpu', '--no-sandbox'],
          slowMo: process.env.CI ? 100 : 0, // 放慢操作速度(毫秒)
        }
      },
    },
    {
      name: 'firefox',
      use: { browserName: 'firefox' },
    },
    {
      name: 'webkit',
      use: { browserName: 'webkit' },
    },
    {
      name: 'mobile-chrome',
      use: { 
        ...devices['Pixel 5'],
      },
    },
    {
      name: 'mobile-safari',
      use: { 
        ...devices['iPhone 13'],
      },
    },
  ],
  
  // CI/本地环境的特殊配置
  webServer: {
    command: 'npm run start',
    port: 3000,
    timeout: 120000,
    reuseExistingServer: !process.env.CI,
  },
});
环境变量配置

Playwright支持通过环境变量配置多种行为:

# 调试模式
PWDEBUG=1 npx playwright test

# 使用特定浏览器运行测试
BROWSER=firefox npx playwright test

# 设置基础URL
BASE_URL=https://staging.example.com npx playwright test

# 设置测试报告目录
PLAYWRIGHT_HTML_REPORT=./my-report npx playwright test

# 设置测试重试次数
PLAYWRIGHT_RETRIES=3 npx playwright test
安装Playwright命令行工具

为了更方便地使用Playwright,可以全局安装Playwright CLI:

npm install -g playwright

# 现在可以直接使用playwright命令
playwright --version
验证安装

安装完成后,可以运行示例测试验证安装是否成功:

npx playwright test

# 或运行特定浏览器的测试
npx playwright test --browser=chromium

成功运行测试后,可以查看HTML报告:

npx playwright show-report

至此,您已经完成了Playwright的安装和基本配置,可以开始编写自动化测试了。

2. Playwright基础API与操作

Playwright提供了丰富的API用于控制浏览器和页面,这些API设计精良,易于使用,能够处理现代Web应用的各种复杂场景。下面详细介绍Playwright的核心API和基本操作。

测试框架基础

Playwright推荐使用内置的测试框架(@playwright/test)来组织和运行测试。基本测试结构如下:

// tests/basic.spec.js
const { test, expect } = require('@playwright/test');

// 定义测试套件
test.describe('基本操作示例', () => {
  // 单个测试用例
  test('打开网页并验证标题', async ({ page }) => {
    // 导航到页面
    await page.goto('https://playwright.dev/');
    
    // 验证页面标题
    const title = await page.title();
    expect(title).toContain('Playwright');
  });
  
  // 另一个测试用例
  test('执行搜索操作', async ({ page }) => {
    await page.goto('https://playwright.dev/');
    await page.fill('[placeholder="Search docs"]', 'locator');
    await page.press('[placeholder="Search docs"]', 'Enter');
    await expect(page).toHaveURL(/.*locator/);
  });
});
浏览器和上下文管理

虽然在使用@playwright/test框架时会自动管理浏览器实例,但了解底层API仍然很重要:

const { chromium, firefox, webkit, devices } = require('@playwright/test');

// 启动浏览器实例
const browser = await chromium.launch({
  headless: false,        // 有头模式(显示浏览器界面)
  slowMo: 100,            // 放慢操作速度
  args: ['--window-size=1920,1080'] // 浏览器启动参数
});

// 创建浏览器上下文(相当于隔离的浏览器实例)
const context = await browser.newContext({
  viewport: { width: 1920, height: 1080 }, // 视口大小
  userAgent: 'Mozilla/5.0 Custom User Agent',  // 自定义UA
  geolocation: { longitude: 12.492507, latitude: 41.889938 }, // 地理位置
  permissions: ['geolocation'],           // 权限设置
  locale: 'zh-CN',                        // 语言
  timezoneId: 'Asia/Shanghai',            // 时区
  colorScheme: 'dark',                    // 暗黑模式
  httpCredentials: {                      // HTTP认证
    username: 'user',
    password: 'pass'
  }
});

// 创建页面实例
const page = await context.newPage();

// 使用完成后关闭资源
await page.close();
await context.close();
await browser.close();
可持久化状态(存储认证状态)

Playwright允许保存和恢复浏览器状态,特别适合处理登录状态:

// 执行登录操作
await page.goto('https://example.com/login');
await page.fill('#username', '[email protected]');
await page.fill('#password', 'password123');
await page.click('#login-button');

// 保存浏览器状态(cookies、localStorage等)
await context.storageState({ path: 'auth.json' });

// ... 之后的测试可以使用已保存的认证状态
const authenticatedContext = await browser.newContext({
  storageState: 'auth.json'
});
页面导航与操作

Playwright提供丰富的导航操作:

// 基本导航
await page.goto('https://example.com', {
  waitUntil: 'networkidle',  // 等待网络空闲
  timeout: 30000             // 超时时间
});

// 导航历史
await page.goBack();    // 后退
await page.goForward(); // 前进
await page.reload();    // 刷新页面

// 等待新页面打开(例如点击会打开新标签页的链接)
const [newPage] = await Promise.all([
  context.waitForEvent('page'),
  page.click('a[target="_blank"]')
]);

// 获取当前URL
const currentUrl = page.url();

// 等待导航完成
await Promise.all([
  page.waitForNavigation(), // 等待导航
  page.click('a.nav-link')  // 点击导航链接
]);

// 处理多Tab页面
const pages = context.pages(); // 获取所有Tab页
await pages[1].bringToFront(); // 切换到第二个Tab页
交互操作详解

Playwright提供了丰富的交互方法,用于模拟用户操作:

// 鼠标点击操作
await page.click('button.submit');     // 基本点击
await page.dblclick('.item');          // 双击
await page.click('.menu', { button: 'right' }); // 右键点击
await page.click('.item', { modifiers: ['Shift'] }); // 按住Shift并点击
await page.click('.position', { position: { x: 10, y: 15 } }); // 指定位置点击
await page.click('.delayed', { delay: 1000 }); // 点击并保持1秒

// 鼠标拖放操作
await page.dragAndDrop('#source', '#target'); // 拖放
// 更精细控制的拖放
await page.locator('#source').dragTo(page.locator('#target'));

// 悬停操作
await page.hover('.tooltip'); // 悬停触发提示

// 键盘操作
await page.keyboard.press('Enter');  // 按Enter键
await page.keyboard.type('Hello');   // 输入文本
await page.keyboard.down('Shift');   // 按下Shift键
await page.fill('input#name', 'John Doe'); // 填充表单字段
await page.type('input#name', 'John Doe', { delay: 100 }); // 模拟真实打字(有延迟)

// 组合快捷键
await page.keyboard.press('Control+a'); // 全选
await page.keyboard.press('Control+c'); // 复制
await page.keyboard.press('Control+v'); // 粘贴

// 按键序列
await page.keyboard.insertText('自动化测试'); // 输入文本(不受键盘布局影响)
await page.press('#editor', 'Tab');  // 在特定元素上按Tab键
表单操作详解

表单是Web应用中最常见的交互元素,Playwright提供了专门的API:

// 文本输入
await page.fill('#username', 'admin');  // 填充输入框
await page.type('#search', '搜索内容', { delay: 50 }); // 模拟按键输入
await page.fill('textarea', ''); // 清空输入

// 选择下拉选项
await page.selectOption('select#country', 'China'); // 通过值选择
await page.selectOption('#language', { label: '中文' }); // 通过标签选择
await page.selectOption('#colors', ['red', 'green', 'blue']); // 多选
await page.selectOption('#level', { index: 2 }); // 通过索引选择

// 复选框和单选按钮
await page.check('#agree');          // 勾选复选框
await page.uncheck('#newsletter');   // 取消勾选
await page.setChecked('#terms', true); // 设置选中状态
await page.check('input[type="radio"]#option2'); // 选择单选按钮

// 文件上传
await page.setInputFiles('input[type="file"]', 'path/to/file.jpg');
// 多文件上传
await page.setInputFiles('input[type="file"]', [
  'path/to/file1.jpg', 
  'path/to/file2.pdf'
]);
// 使用Buffer上传(无需实际文件)
await page.setInputFiles('input[type="file"]', {
  name: 'file.txt',
  mimeType: 'text/plain',
  buffer: Buffer.from('测试内容')
});

// 日期选择
await page.fill('input[type="date"]', '2023-06-30');
获取元素信息

Playwright提供多种方法获取页面元素信息:

// 使用Locator API(推荐)
const submitButton = page.locator('button.submit');
const isVisible = await submitButton.isVisible();
const text = await submitButton.textContent();

// 获取文本内容
const message = await page.textContent('.message');
const innerText = await page.innerText('.container'); // 只获取可见文本
const innerHTML = await page.innerHTML('.content'); // 获取HTML内容

// 获取属性值
const href = await page.getAttribute('a.link', 'href');
const src = await page.getAttribute('img', 'src');
const classes = await page.getAttribute('div', 'class');

// 获取表单值
const inputValue = await page.inputValue('#email');
const isChecked = await page.isChecked('input[type="checkbox"]');
const selectedValue = await page.evaluate(() => 
  document.querySelector('select').value
);

// 元素状态检查
const isDisabled = await page.isDisabled('button.submit');
const isEditable = await page.isEditable('input#name');
const isEnabled = await page.isEnabled('button.action');
const isChecked = await page.isChecked('input[type="checkbox"]');
const isHidden = await page.isHidden('.notification');
const isVisible = await page.isVisible('.message');

// 获取元素数量
const count = await page.locator('.item').count();

// 获取元素边界框
const boundingBox = await page.locator('.logo').boundingBox();
console.log(boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height);
执行JavaScript

Playwright允许在页面上下文中执行JavaScript代码:

// 基本执行
const dimensions = await page.evaluate(() => {
  return {
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight,
    devicePixelRatio: window.devicePixelRatio
  };
});

// 传递参数
const result = await page.evaluate(
  (name, age) => {
    return `${name} is ${age} years old`;
  },
  'John', 
  30
);

// 使用元素作为参数
const text = await page.evaluate(
  element => element.textContent,
  await page.$('.title')
);

// evaluateHandle 返回JSHandle(用于引用DOM对象等)
const bodyHandle = await page.evaluateHandle(() => document.body);
const html = await page.evaluate(body => body.outerHTML, bodyHandle);
await bodyHandle.dispose(); // 释放资源

// 公开函数到页面上下文
await page.exposeFunction('md5', text => crypto.createHash('md5').update(text).digest('hex'));
await page.evaluate(async () => {
  // 现在可以在浏览器中调用md5函数
  const hash = await window.md5('测试文本');
  console.log(hash);
});
处理对话框(alert、confirm、prompt)

Playwright可以处理各种浏览器对话框:

// 设置对话框处理程序
page.on('dialog', async dialog => {
  console.log(`对话框信息: ${dialog.message()}`);
  console.log(`对话框类型: ${dialog.type()}`); // alert, confirm, prompt, beforeunload
  
  // 按需提供输入(针对prompt)
  if (dialog.type() === 'prompt')
    await dialog.accept('用户输入');
  else
    await dialog.accept(); // 确定
  
  // 或者取消对话框
  // await dialog.dismiss();
});

// 触发对话框
await page.click('#alert-button');

// 也可以通过等待对话框事件处理
const dialogPromise = page.waitForEvent('dialog');
await page.click('#confirm-button');
const dialog = await dialogPromise;
await dialog.accept();
截图和PDF

Playwright提供了强大的截图和PDF生成功能:

// 页面截图
await page.screenshot({ path: 'screenshot.png' });

// 截图选项
await page.screenshot({
  path: 'full-page.png',
  fullPage: true,           // 完整页面截图(包括滚动部分)
  clip: { x: 0, y: 0, width: 500, height: 500 }, // 剪裁区域
  omitBackground: true,     // 透明背景
  quality: 90               // JPEG质量(仅限JPEG格式)
});

// 元素截图
await page.locator('.card').screenshot({ path: 'element.png' });

// 生成PDF(仅支持Chromium无头模式)
await page.pdf({
  path: 'document.pdf',
  format: 'A4',             // A4, Letter等
  landscape: false,         // 横向/纵向
  margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' },
  printBackground: true,    // 打印背景
  scale: 0.8,               // 缩放比例 (0.1-2)
  pageRanges: '1-5',        // 指定页面范围
});
Cookies管理

Playwright提供了完整的Cookies操作API:

// 获取所有cookies
const cookies = await context.cookies();

// 获取特定URL的cookies
const siteCookies = await context.cookies(['https://example.com']);

// 设置cookies
await context.addCookies([{
  name: 'session_id',
  value: '12345',
  domain: '.example.com',
  path: '/',
  expires: Date.now() / 1000 + 3600, // 1小时后过期
  httpOnly: true,
  secure: true,
  sameSite: 'Lax' // 'Strict', 'Lax' 或 'None'
}]);

// 清除cookies
await context.clearCookies();
模拟地理位置和设备

Playwright可以模拟不同的设备和地理位置:

// 导入预定义设备
const { devices } = require('@playwright/test');
const iPhone = devices['iPhone 13'];

// 使用预定义设备
const context = await browser.newContext({
  ...iPhone
});

// 自定义设备属性
const context = await browser.newContext({
  viewport: { width: 375, height: 812 },
  deviceScaleFactor: 3,
  isMobile: true,
  hasTouch: true,
  userAgent: '...'
});

// 设置地理位置
await context.setGeolocation({
  latitude: 31.2304,
  longitude: 121.4737
});

// 授予地理位置权限
await context.grantPermissions(['geolocation']);

这些基础API和操作覆盖了Playwright自动化测试的核心功能,掌握这些操作后,您就能应对大多数Web自动化测试场景。下一部分将介绍如何更精确地定位页面元素。

3. 元素定位与选择器

Playwright提供了强大而灵活的元素定位机制,让您能够精确地找到页面上的任何元素。良好的元素定位策略是编写稳健测试的基础,本节将详细介绍Playwright的各种选择器及其最佳实践。

Locator API

Playwright推荐使用Locator API进行元素定位,它提供了更可靠、更易读的元素定位方式:

// 创建定位器
const loginButton = page.locator('button:has-text("登录")');

// 使用定位器执行操作
await loginButton.click();
await loginButton.isVisible();

// 链式定位 - 在父元素中查找子元素
const form = page.locator('form#login');
const usernameInput = form.locator('input[name="username"]');
const submitButton = form.locator('button[type="submit"]');

Locator的优点:

  • 自动等待元素可用、可见或可操作
  • 支持自动重试机制,提高测试稳定性
  • 提供丰富的定位和断言API
  • 可组合性强,支持链式定位
选择器类型详解

Playwright支持多种类型的选择器,每种选择器都有其适用场景:

1. CSS选择器

CSS选择器是最基本、最常用的定位方式:

// 基本CSS选择器
page.locator('div.user-panel');        // 类选择器
page.locator('#login-button');         // ID选择器
page.locator('button.primary');        // 元素+类选择器
page.locator('div > p');               // 子元素选择器
page.locator('div + p');               // 相邻兄弟选择器
page.locator('div ~ p');               // 一般兄弟选择器

// 属性选择器
page.locator('[data-test-id=submit]'); // 自定义数据属性(推荐)
page.locator('input[name="username"]'); // 属性等于
page.locator('[placeholder^="Enter"]'); // 属性开头匹配
page.locator('[placeholder$="word"]');  // 属性结尾匹配
page.locator('[placeholder*="pass"]');  // 属性包含匹配

// 伪类选择器
page.locator('button:hover');          // 悬停状态
page.locator('input:focus');           // 聚焦状态
page.locator('input:checked');         // 选中状态
page.locator('li:nth-child(3)');       // 第n个子元素
page.locator('div:nth-of-type(2)');    // 同类型的第n个元素
page.locator('button:disabled');       // 禁用状态
page.locator('button:enabled');        // 启用状态

// 组合CSS选择器
page.locator('form.login button[type="submit"]:not(.disabled)');
2. 文本内容选择器

Playwright特有的文本选择器,非常适合定位按钮、链接等有文本的元素:

// 精确文本匹配
page.locator('text=登录');       // 完全匹配"登录"文本的元素
page.locator('text="确认提交"'); // 用引号处理包含空格的文本

// 部分文本匹配
page.locator('text=确认');       // 包含"确认"的元素

// 组合CSS与文本
page.locator('button:has-text("登录")'); // CSS + 文本内容
page.locator('.nav-item:has-text("首页")');

// 文本正则表达式
page.locator('text=/^注册.*/');  // 以"注册"开头的文本
page.locator('text=/提交.*表单$/'); // 以"表单"结尾的文本
page.locator('text=/密码:\d+/'); // 包含数字的文本
3. XPath选择器

XPath功能强大但复杂度较高,适用于特定场景:

// 基本XPath
page.locator('xpath=//button');      // 所有按钮
page.locator('xpath=//button[1]');   // 第一个按钮
page.locator('xpath=//div/button');  // div下的直接子按钮

// 属性XPath
page.locator('xpath=//input[@name="username"]'); // 属性等于
page.locator('xpath=//div[@data-test="login"]'); // 自定义属性
page.locator('xpath=//*[@placeholder="密码"]');  // 任何有placeholder属性的元素

// 文本XPath
page.locator('xpath=//button[text()="登录"]');         // 精确文本匹配
page.locator('xpath=//div[contains(text(), "错误")]'); // 包含文本
page.locator('xpath=//span[starts-with(text(), "欢迎")]'); // 以特定文本开头

// 组合条件
page.locator('xpath=//button[@type="submit" and contains(text(), "登录")]');
page.locator('xpath=//button[@disabled or contains(@class, "inactive")]');

// 父元素、祖先、兄弟元素
page.locator('xpath=//input[@id="password"]/..'); // 父元素
page.locator('xpath=//input[@id="password"]/ancestor::form'); // 祖先表单元素
page.locator('xpath=//label[text()="用户名"]/following-sibling::input'); // 后续兄弟元素
4. 其他高级选择器
// ID选择器
page.locator('id=login-button');

// 角色选择器(ARIA角色)
page.locator('role=button');         // 所有按钮角色
page.locator('role=button[name="提交"]'); // 带有特定名称的按钮
page.locator('role=heading[level=2]'); // 二级标题
page.locator('role=checkbox[checked]'); // 选中的复选框

// 测试ID选择器(推荐用于测试)
page.locator('data-testid=submit-button');
选择器最佳实践

选择好的选择器能大幅提高测试的稳定性和可维护性:

选择器优先级(从高到低)
  1. 测试ID选择器: data-testid, data-test-id, data-qa 等专为测试准备的属性

    page.locator('[data-testid="submit-button"]');
    
  2. ARIA属性和角色: 对可访问性有益,也提供稳定的选择器

    page.locator('role=button[name="登录"]');
    
  3. CSS类选择器: 使用专门为测试设计的CSS类

    page.locator('.login-submit-btn');
    
  4. 语义化HTML元素属性: 如name, id, type等

    page.locator('input[name="username"]');
    
  5. 文本内容选择器: 当上述方法不适用时使用,但注意文本可能变化

    page.locator('text=登录');
    
避免使用的选择器
  1. 基于视觉样式的选择器: 如[style="color: red"],这些容易变化
  2. 复杂的XPath: 容易被细小的DOM变化破坏
  3. 索引选择器: 如div >> nth=3,脆弱且难以维护
  4. 生成的ID: 如自动生成的临时ID,这些可能在每次页面加载时变化
实际应用示例

下面是一些实际应用中的选择器示例:

登录表单
const loginForm = page.locator('form#login-form');
const username = loginForm.locator('[data-testid="username-input"]');
const password = loginForm.locator('[data-testid="password-input"]');
const submitBtn = loginForm.locator('button[type="submit"]');

await username.fill('[email protected]');
await password.fill('password123');
await submitBtn.click();
表格操作
// 定位特定单元格
const cell = page.locator('table[data-testid="data-table"] tr:nth-child(3) td:nth-child(2)');

// 表格行遍历
const rows = page.locator('table tr');
const count = await rows.count();
for (let i = 0; i < count; i++) {
  const row = rows.nth(i);
  const text = await row.textContent();
  if (text.includes('目标数据')) {
    await row.locator('button.edit').click();
    break;
  }
}
动态列表
// 查找包含特定文本的列表项
const targetItem = page.locator('ul.list li:has-text("需要编辑的项目")');
await targetItem.locator('button.edit').click();

// 等待列表加载特定数量的项目
await page.locator('ul.items li').waitFor({ state: 'attached', timeout: 5000 });
const itemCount = await page.locator('ul.items li').count();
expect(itemCount).toBeGreaterThan(5);
定位多个元素

Playwright允许定位和操作多个匹配的元素:

// 获取所有匹配元素
const buttons = page.locator('button.action');

// 获取元素数量
const count = await buttons.count();
console.log(`找到 ${count} 个按钮`);

// 遍历所有元素
for (let i = 0; i < count; i++) {
  const text = await buttons.nth(i).textContent();
  console.log(`按钮 ${i+1} 文本: ${text}`);
}

// 通过索引获取特定元素
const firstButton = buttons.first(); // 第一个
const lastButton = buttons.last();   // 最后一个
const thirdButton = buttons.nth(2);  // 第三个 (索引从0开始)

// 筛选元素
const visibleButtons = buttons.filter({ hasText: '确认' });
过滤器和修饰符

Playwright提供了强大的过滤机制来精确定位元素:

// 基于文本过滤
const button = page.locator('button').filter({ hasText: '登录' });

// 基于属性过滤
const input = page.locator('input').filter({ has: page.locator('[placeholder="用户名"]') });

// 可见性过滤
const visibleError = page.locator('.error').filter({ visible: true });

// 组合过滤器
const item = page.locator('li').filter({
  hasText: '产品',
  has: page.locator('.badge.new')
});
等待和断言

Playwright提供了强大的等待和断言机制,确保元素处于预期状态:

// 等待元素可见
await page.locator('.notification').waitFor({ state: 'visible' });

// 等待元素隐藏
await page.locator('.spinner').waitFor({ state: 'hidden' });

// 等待元素附加到DOM
await page.locator('.dynamic-content').waitFor({ state: 'attached' });

// 等待元素从DOM中分离
await page.locator('.removed-item').waitFor({ state: 'detached' });

// 断言元素包含特定文本
await expect(page.locator('.message')).toContainText('成功');

// 断言元素可见性
await expect(page.locator('.error')).toBeVisible();
await expect(page.locator('.loading')).toBeHidden();

// 断言元素属性
await expect(page.locator('input')).toHaveAttribute('disabled', '');
高级定位技巧
阴影DOM(Shadow DOM)

Playwright可以无缝穿透阴影DOM边界:

// 直接定位阴影DOM中的元素
const shadowButton = page.locator('my-custom-element button');
await shadowButton.click();
iframe定位

Playwright可以轻松操作iframe中的元素:

// 定位iframe
const frame = page.frameLocator('#my-iframe');

// 在iframe中定位元素
const buttonInFrame = frame.locator('button.action');
await buttonInFrame.click();

// 多层iframe
const nestedButton = page
  .frameLocator('#parent-iframe')
  .frameLocator('.child-iframe')
  .locator('button');
内容菜单和悬浮层
// 打开上下文菜单(右键点击)
await page.locator('.item').click({ button: 'right' });

// 悬停以显示菜单
await page.locator('.dropdown-trigger').hover();

// 操作悬浮菜单中的元素
await page.locator('.dropdown-menu .item').click();

通过掌握这些元素定位技巧,您可以精确地操作任何Web应用中的任何元素,编写出稳定、可靠的自动化测试。在下一节中,我们将详细介绍如何处理异步操作和等待机制。

4. 处理等待与超时

在Web自动化测试中,正确处理等待和超时是确保测试稳定性的关键。现代Web应用通常包含异步操作、AJAX请求、动态内容加载等,需要适当的等待策略来确保元素在操作前处于期望状态。Playwright提供了强大的自动等待和显式等待机制,帮助您编写可靠的测试。

自动等待机制

Playwright的一大特点是内置自动等待机制,大多数操作都会自动等待元素准备好。这显著降低了处理等待的复杂性:

// Playwright会自动等待按钮可见、可用且稳定后再点击
await page.click('button#submit');

// 自动等待表单字段可用后再填写
await page.fill('input#username', 'admin');

// 自动等待导航完成
await page.goto('https://example.com');

自动等待背后的原理:

  1. 可见性检查:确保元素在视口中可见
  2. 可操作性检查:确保元素未被禁用、遮挡或被动画覆盖
  3. 稳定性检查:确保元素已停止移动或动画
操作的自动等待

Playwright的每个操作都包含特定的等待逻辑:

操作 自动等待逻辑
click() 等待元素可见、可点击且稳定
fill() 等待输入框可见、可编辑且稳定
check(), uncheck() 等待复选框可见、可点击且稳定
selectOption() 等待选择框和选项可见、可选择
goto() 等待导航完成,页面加载事件触发
screenshot() 等待页面视觉稳定,无动画
导航和加载状态

Playwright提供了精细控制页面加载状态的方法:

// 基本导航(默认等待'load'事件)
await page.goto('https://example.com');

// 指定等待条件
await page.goto('https://example.com', {
  waitUntil: 'domcontentloaded' // 更快,只等待DOM解析完成
});

// 更安全的等待方式
await page.goto('https://example.com', {
  waitUntil: 'networkidle' // 等待网络活动停止(最可靠但最慢)
});

// 手动等待特定加载状态
await page.waitForLoadState('domcontentloaded');
await page.waitForLoadState('load');
await page.waitForLoadState('networkidle');

不同加载状态的比较:

  • 'domcontentloaded': DOM解析完成,最快但可能资源尚未加载完成
  • 'load': 页面加载事件触发,较快但可能有后续AJAX请求
  • 'networkidle': 网络活动停止500ms,最可靠但最慢
显式等待策略

尽管自动等待很强大,有时我们需要更精确的控制:

等待元素
const { test, expect } = require('@playwright/test');

test('等待示例', async ({ page }) => {
  await page.goto('https://example.com');
  
  // 等待元素出现(附加到DOM)
  await page.waitForSelector('.notification', { state: 'attached' });

  // 等待元素可见
  await page.waitForSelector('.message', { state: 'visible' });

  // 等待元素消失
  await page.waitForSelector('.loading', { state: 'hidden' });

  // 等待元素从DOM中移除
  await page.waitForSelector('.temp-item', { state: 'detached' });

  // 使用Locator API(推荐)
  await page.locator('.notification').waitFor({ state: 'visible' });

  // 等待特定数量的元素
  const listItems = page.locator('ul.results li');
  await listItems.waitFor({ state: 'attached' });
  expect(await listItems.count()).toBe(10);
});
等待网络请求
// 等待特定URL的请求发出
await page.waitForRequest('**/api/users');

// 等待匹配正则表达式的请求
await page.waitForRequest(/api\/items\/\d+/);

// 等待特定URL的响应
const response = await page.waitForResponse('**/api/data');
expect(response.status()).toBe(200);

// 等待响应并获取数据
const response = await page.waitForResponse('**/api/products');
const data = await response.json();
expect(data.items.length).toBeGreaterThan(0);

// 等待所有匹配请求完成
const [response1, response2] = await Promise.all([
  page.waitForResponse('**/api/user'),
  page.waitForResponse('**/api/settings'),
  page.click('button#refresh') // 触发请求的操作
]);
等待导航和URL变化
// 等待导航完成
await Promise.all([
  page.waitForNavigation(),
  page.click('a.link') // 触发导航的操作
]);

// 等待URL变化 (推荐方式)
await Promise.all([
  page.waitForURL('**/dashboard'),
  page.click('button#login')
]);

// 等待特定URL模式
await page.waitForURL(/.*\/profile\/\d+/);

// 精确URL匹配
await page.waitForURL('https://example.com/success', {
  waitUntil: 'networkidle' // 确保页面完全加载
});

注意: 在新版本的Playwright中,page.waitForNavigation()正在逐渐被page.waitForURL()替代,后者提供了更精确的URL匹配能力。

等待事件
// 等待页面控制台消息
const msgPromise = page.waitForEvent('console');
await page.evaluate(() => console.log('测试消息'));
const msg = await msgPromise;
console.log(msg.text());

// 等待对话框(alert/confirm/prompt)
const dialogPromise = page.waitForEvent('dialog');
await page.click('#alert-button');
const dialog = await dialogPromise;
console.log(dialog.message());
await dialog.accept();

// 等待文件下载
const downloadPromise = page.waitForEvent('download');
await page.click('button#download');
const download = await downloadPromise;
const path = await download.path(); // 临时文件路径
等待特定条件
// 等待JavaScript条件满足
await page.waitForFunction(() => {
  return document.querySelectorAll('.item').length > 5;
});

// 带参数的条件等待
const expectedText = '加载完成';
await page.waitForFunction(
  text => document.querySelector('.status').textContent.includes(text),
  expectedText
);

// 带超时的条件等待
await page.waitForFunction(
  () => window.serverData !== undefined,
  null, // 无参数
  { timeout: 10000 } // 10秒超时
);
超时设置

Playwright提供了多级别的超时设置,可根据需要调整:

全局超时设置

在playwright.config.js中设置全局超时:

// playwright.config.js
const { defineConfig } = require('@playwright/test');

module.exports = defineConfig({
  // 全局测试超时时间(毫秒)
  timeout: 30000,
  
  // 全局期望超时
  expect: {
    timeout: 5000  // 断言的超时时间
  },
  
  // 全局操作超时
  use: {
    actionTimeout: 10000,    // 点击、填充等操作的超时
    navigationTimeout: 30000 // 导航操作的超时
  }
});
特定操作的超时设置

针对单个操作设置超时:

// 导航操作的超时
await page.goto('https://slow-site.com', { timeout: 60000 });

// 点击操作的超时
await page.click('button.slow-loading', { timeout: 30000 });

// 元素等待的超时
await page.waitForSelector('.dynamic-content', { timeout: 20000 });

// 条件等待的超时
await page.waitForFunction(
  () => document.querySelectorAll('.item').length > 20,
  null,
  { timeout: 15000 }
);
暂时禁用超时

某些特殊情况可能需要禁用超时:

// 禁用超时(谨慎使用)
await page.waitForSelector('.may-not-appear', { timeout: 0 });
高级等待模式
轮询间隔
// 自定义轮询间隔(默认为100ms)
await page.waitForFunction(
  () => window.status === 'ready',
  null,
  { timeout: 30000, polling: 1000 } // 每秒轮询一次
);

// 持续轮询(适用于实时更新)
await page.waitForFunction(
  () => document.querySelector('.count').textContent > '10',
  null,
  { polling: 'raf' } // 使用requestAnimationFrame频率轮询
);
优雅处理超时错误
try {
  await page.waitForSelector('.may-not-appear', { timeout: 5000 });
  console.log('元素出现了');
} catch (error) {
  if (error.name === 'TimeoutError') {
    console.log('元素未出现,继续执行...');
    // 执行替代逻辑
  } else {
    throw error; // 重新抛出其他类型的错误
  }
}
条件等待和分支逻辑
// 根据元素是否存在执行不同逻辑
const exists = await page.locator('.popup').isVisible();
if (exists) {
  await page.click('.popup .close');
} else {
  console.log('没有弹窗,继续执行');
}

// 等待多个可能出现的元素之一
const elementPromises = [
  page.waitForSelector('.success', { state: 'visible', timeout: 10000 }),
  page.waitForSelector('.error', { state: 'visible', timeout: 10000 })
];

const result = await Promise.race(elementPromises)
  .then(async (element) => {
    const text = await element.textContent();
    return { text, element };
  })
  .catch(() => {
    return null; // 都没有出现
  });

if (result) {
  if (await result.element.matches('.success')) {
    console.log('成功状态');
  } else {
    console.log('错误状态:', result.text);
  }
}
真实场景等待示例
等待动态加载的内容
// 等待无限滚动加载
async function loadAllItems() {
  let previousCount = 0;
  let currentCount = await page.locator('.item').count();
  
  while (previousCount !== currentCount) {
    // 滚动到底部触发加载
    await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
    
    // 等待新元素加载
    await page.waitForFunction(
      count => document.querySelectorAll('.item').length > count,
      currentCount,
      { timeout: 10000 }
    ).catch(() => {
      // 如果10秒内没有新元素,认为已加载完成
      console.log('No more items loaded, probably reached the end');
    });
    
    previousCount = currentCount;
    currentCount = await page.locator('.item').count();
    console.log(`Loaded ${currentCount} items`);
  }
  
  return currentCount;
}
处理AJAX加载的表格
// 等待表格加载完成
async function waitForTableLoad() {
  // 首先等待表格结构出现
  await page.locator('table.data-grid').waitFor({ state: 'attached' });
  
  // 等待加载指示器消失
  await page.locator('.table-loading').waitFor({ state: 'hidden', timeout: 30000 });
  
  // 确认表格有数据
  await page.waitForFunction(
    () => document.querySelectorAll('table.data-grid tbody tr').length > 0,
    null,
    { timeout: 10000 }
  );
  
  // 返回行数
  return page.locator('table.data-grid tbody tr').count();
}
等待文件上传完成
async function uploadFileAndWait(filePath) {
  // 设置文件输入
  await page.setInputFiles('input[type="file"]', filePath);
  
  // 等待上传开始(进度条出现)
  await page.locator('.upload-progress').waitFor({ state: 'visible' });
  
  // 等待上传完成(进度条消失)
  await page.locator('.upload-progress').waitFor({ state: 'hidden', timeout: 60000 });
  
  // 等待成功消息
  await page.locator('.upload-success').waitFor({ state: 'visible' });
  
  // 获取上传后的文件信息
  return page.locator('.file-info').textContent();
}

正确处理等待和超时是编写稳定测试的关键。Playwright的自动等待机制大大简化了这一过程,但在复杂场景中,了解和使用各种显式等待策略仍然至关重要。接下来我们将探讨如何使用Playwright的自动录制功能快速生成测试代码。

5. 自动录制与代码生成

Playwright提供了强大的自动录制和代码生成功能,通过记录用户在浏览器中的真实操作,自动生成相应的测试代码。这一功能大大提高了测试脚本开发效率,尤其适合初学者和快速原型开发。本节将详细介绍如何使用Playwright的代码生成工具录制和生成自动化测试脚本。

Playwright Codegen 工具介绍

Playwright Codegen 是一个强大的录制工具,它可以:

  • 记录用户在浏览器中的实际操作
  • 实时生成对应的Playwright代码
  • 支持多种编程语言(JavaScript, TypeScript, Python, Java, C#)
  • 提供精细控制和编辑功能
  • 帮助识别最佳选择器
启动代码生成器

可以通过多种方式启动Playwright的代码生成器:

# 基本启动方式
npx playwright codegen

# 指定目标网站
npx playwright codegen https://example.com

# 指定浏览器
npx playwright codegen --browser firefox

# 指定输出语言
npx playwright codegen --target python

# 指定视口大小
npx playwright codegen --viewport-size=1280,720

# 指定设备模拟
npx playwright codegen --device="iPhone 13"

# 组合使用多个选项
npx playwright codegen https://example.com --browser webkit --target java --viewport-size=1280,720

启动参数详解:

参数 说明 示例
--target 输出语言 --target=python,可选值: javascript, typescript, python, java, csharp
--browser 使用的浏览器 --browser=firefox,可选值: chromium, firefox, webkit
--viewport-size 视口大小 --viewport-size=1920,1080
--device 模拟设备 --device="iPhone 13"
--output 输出文件 --output=test.js
--save-storage 保存状态 --save-storage=auth.json
--load-storage 加载状态 --load-storage=auth.json
Codegen 界面详解

启动Codegen后,会打开两个窗口:

  1. 浏览器窗口:用于执行操作的浏览器
  2. Codegen窗口:显示生成的代码和录制控制界面

Codegen窗口包含以下主要部分:

  • 代码面板:显示生成的代码
  • 录制控制:开始/暂停/恢复录制
  • 语言选择器:切换输出语言
  • 操作记录器:记录执行的每一步操作
  • 选择器编辑器:允许修改生成的选择器
  • 复制/导出按钮:复制或保存生成的代码
基本录制流程
  1. 启动录制器

    npx playwright codegen https://example.com
    
  2. 在浏览器中执行操作

    • 点击链接和按钮
    • 填写表单
    • 选择选项
    • 导航到不同页面
  3. 观察生成的代码
    在Codegen窗口中可以实时看到生成的代码,每执行一步操作,对应的代码会立即生成。

  4. 导出或复制代码
    完成录制后,可以将生成的代码复制到剪贴板或保存到文件,用于测试脚本开发。

高级录制技巧
1. 结合已有认证状态

在已登录状态下开始录制:

# 首先保存认证状态
npx playwright open https://example.com
# 手动登录后
npx playwright codegen --save-storage=auth.json

# 使用保存的认证状态开始录制
npx playwright codegen --load-storage=auth.json https://example.com/dashboard

这样可以跳过每次录制都需要重复的登录步骤。

2. 录制移动设备视图

模拟移动设备录制操作:

# 使用iPhone 13模拟器
npx playwright codegen --device="iPhone 13" https://example.com

# 自定义移动设备尺寸
npx playwright codegen --viewport-size=375,812 --user-agent="自定义UA" https://example.com
3. 暂停和恢复录制

在录制过程中,可以通过Codegen窗口控制录制过程:

  • 点击"暂停"按钮暂停录制,执行一些不希望记录的操作
  • 点击"恢复"按钮继续录制
4. 修改生成的选择器

Codegen允许实时编辑生成的选择器:

  1. 点击操作步骤旁的选择器
  2. 在选择器编辑器中修改选择器
  3. 点击"更新"应用修改

这对于生成更稳定、更可维护的选择器非常有用。

实例:录制完整测试流程

下面是一个完整的电子商务网站测试流程录制示例:

示例:录制登录和购物流程
  1. 启动代码生成器:

    npx playwright codegen https://example.com
    
  2. 在浏览器中执行以下操作:

    • 点击"登录"按钮
    • 填写用户名和密码
    • 点击"登录"提交表单
    • 搜索产品"手机"
    • 点击第一个搜索结果
    • 选择颜色和容量选项
    • 点击"加入购物车"
    • 点击"结算"
    • 填写配送信息
    • 完成订单流程
  3. 生成的代码示例:

    const { chromium } = require('playwright');
    
    (async () => {
      const browser = await chromium.launch({
        headless: false
      });
      const context = await browser.newContext();
      const page = await context.newPage();
    
      // 导航到网站
      await page.goto('https://example.com/');
      
      // 登录流程
      await page.click('text=登录');
      await page.fill('#username', '[email protected]');
      await page.fill('#password', 'password123');
      await page.click('button:has-text("登录")');
      await page.waitForURL('https://example.com/account');
      
      // 搜索产品
      await page.fill('[placeholder="搜索商品"]', '手机');
      await page.press('[placeholder="搜索商品"]', 'Enter');
      await page.waitForURL('https://example.com/search?q=%E6%89%8B%E6%9C%BA');
      
      // 选择产品
      await page.click('.product-card:nth-child(1)');
      await page.waitForURL(/\/product\/\d+/);
      
      // 选择选项
      await page.click('.color-option:has-text("黑色")');
      await page.selectOption('.capacity-select', '128GB');
      
      // 加入购物车
      await page.click('button:has-text("加入购物车")');
      await page.waitForSelector('.cart-notification:visible');
      
      // 结算
      await page.click('.cart-icon');
      await page.click('button:has-text("结算")');
      
      // 填写配送信息
      await page.fill('#shipping-name', '张三');
      await page.fill('#shipping-phone', '13800138000');
      await page.fill('#shipping-address', '北京市海淀区中关村大街1号');
      await page.selectOption('#province', '北京市');
      await page.selectOption('#city', '海淀区');
      
      // 选择支付方式
      await page.check('#payment-option-2');
      
      // 提交订单
      await page.click('button:has-text("提交订单")');
      await page.waitForURL('https://example.com/order/confirmation');
      
      // 验证订单成功
      await page.waitForSelector('text=订单提交成功');
    
      // ---------------------
      await context.close();
      await browser.close();
    })();
    
  4. 保存和优化代码:

    • 复制生成的代码到项目中
    • 根据需要调整和优化代码
    • 添加断言和错误处理
Codegen生成的代码特点

Codegen生成的代码具有以下特点:

  1. 完整性:包含从启动浏览器到关闭的完整流程
  2. 等待处理:自动包含适当的等待语句
  3. 选择器质量:尝试生成最佳的选择器,但可能需要手动优化
  4. 动作序列:准确反映用户操作顺序
  5. 代码格式:格式良好,易于阅读和修改
从录制到稳定测试的优化步骤

录制的代码通常需要进一步优化才能成为健壮的测试脚本:

  1. 改进选择器

    • 将自动生成的复杂选择器替换为更稳定的选择器
    • 优先使用数据测试属性(如data-testid
    // 替换这个
    await page.click('.product-list > div:nth-child(3) > .card > .btn');
    // 使用这个
    await page.click('[data-testid="product-card-3"] button.buy-now');
    
  2. 添加断言

    • 在关键步骤添加验证点
    // 添加断言
    await expect(page.locator('.cart-count')).toHaveText('1');
    await expect(page.locator('.total-price')).toContainText('¥1299');
    
  3. 添加错误处理

    try {
      await page.click('button.checkout');
      await page.waitForURL('/payment', { timeout: 10000 });
    } catch (error) {
      console.error('结算过程失败:', error);
      // 记录错误状态,截图等
      await page.screenshot({ path: 'error-checkout.png' });
      throw error;
    }
    
  4. 重构为更可维护的结构

    • 将录制的代码组织为测试框架格式
    const { test, expect } = require('@playwright/test');
    
    test('完整购物流程', async ({ page }) => {
      // 登录
      await login(page, '[email protected]', 'password123');
      
      // 搜索产品
      await searchProduct(page, '手机');
      
      // 添加到购物车
      await addToCart(page, 1);
      
      // 验证购物车
      await expect(page.locator('.cart-count')).toHaveText('1');
      
      // 完成结算
      await checkout(page);
      
      // 验证订单成功
      await expect(page.locator('.order-confirmation')).toBeVisible();
      await expect(page.locator('.order-number')).toBeVisible();
    });
    
    async function login(page, username, password) {
      // 登录实现
    }
    
    async function searchProduct(page, keyword) {
      // 搜索实现
    }
    
    // 其他帮助函数...
    
Playwright Inspector

除了Codegen,Playwright还提供了Inspector工具,用于调试和改进现有测试:

# 在调试模式下运行测试
PWDEBUG=1 npx playwright test login.spec.js

# Windows PowerShell
$env:PWDEBUG=1; npx playwright test login.spec.js

# Windows CMD
set PWDEBUG=1
npx playwright test login.spec.js

Inspector 提供以下功能:

  • 逐步执行测试
  • 检查页面状态
  • 探索和评估选择器
  • 实时修改和尝试不同的选择器
  • 查看页面DOM结构
  • 记录额外的操作
VSCode 插件集成

如果您使用VSCode,Playwright官方插件提供了更好的集成体验:

  1. 安装 Playwright VSCode 插件
  2. 可直接在编辑器中录制操作
  3. 直接从编辑器运行和调试测试
  4. 内置选择器生成工具
自动录制的最佳实践
  1. 录制核心路径

    • 先录制关键功能的主要路径
    • 手动添加变体和边界情况
  2. 分步录制

    • 将复杂流程拆分为几个小段分别录制
    • 后期组合并优化代码
  3. 维护录制环境

    • 使用稳定的测试环境录制
    • 考虑使用模拟数据和隔离环境
  4. 记录关键信息

    • 录制过程中记录关键数据(如生成的订单号)
    • 将动态值参数化
  5. 选择器策略

    • 录制前在应用程序中添加测试ID效果最佳
    • 录制后检查并优化选择器
实际案例:多语言生成

Playwright可以生成多种编程语言的代码:

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

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://playwright.dev/');
  await page.click('text=Get Started');
  // ...
  await browser.close();
})();
Python:
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto('https://playwright.dev/')
    page.click('text=Get Started')
    # ...
    browser.close()
Java:
import com.microsoft.playwright.*;

public class Example {
  public static void main(String[] args) {
    try (Playwright playwright = Playwright.create()) {
      Browser browser = playwright.chromium().launch();
      Page page = browser.newPage();
      page.navigate("https://playwright.dev/");
      page.click("text=Get Started");
      // ...
      browser.close();
    }
  }
}
C#:
using Microsoft.Playwright;
using System.Threading.Tasks;

class Program
{
    public static async Task Main()
    {
        using var playwright = await Playwright.CreateAsync();
        await using var browser = await playwright.Chromium.LaunchAsync();
        var page = await browser.NewPageAsync();
        await page.GotoAsync("https://playwright.dev/");
        await page.ClickAsync("text=Get Started");
        // ...
    }
}

Playwright的代码生成功能极大地简化了测试脚本开发过程,尤其适合快速上手和原型开发。通过录制实际操作,您可以快速生成自动化测试脚本,然后根据需要优化和扩展,构建可靠的测试套件。

6. 移动端测试与响应式设计测试

随着移动设备使用率的不断提高,移动端测试变得越来越重要。Playwright提供了强大的移动设备模拟能力,允许在桌面浏览器中模拟移动设备环境,测试网站在不同设备上的表现。本节将详细介绍如何使用Playwright进行移动端测试。

移动设备模拟基础

Playwright提供了两种主要方式来模拟移动设备:

  1. 使用预定义设备描述符
  2. 自定义设备参数
const { test, expect, devices } = require('@playwright/test');

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

// 或在测试中指定
test('在iPhone上测试', async ({ page }) => {
  // 测试代码
});

// 在配置文件中定义设备项目
// playwright.config.js
module.exports = defineConfig({
  projects: [
    {
      name: 'Mobile Chrome',
      use: {
        ...devices['Pixel 5'],
      },
    },
    {
      name: 'Mobile Safari',
      use: {
        ...devices['iPhone 13'],
      },
    },
  ],
});
预定义设备列表

Playwright内置了大量预定义设备配置,覆盖了主流的手机和平板电脑:

// 打印所有可用设备
const { devices } = require('@playwright/test');
console.log(Object.keys(devices));

// 常用设备示例
const iPhone13 = devices['iPhone 13'];
const pixel5 = devices['Pixel 5'];
const iPadAir = devices['iPad Air'];
const galaxyS8 = devices['Galaxy S8'];

预定义设备配置包含了以下关键参数:

  • 视口尺寸
  • 设备像素比(DPR)
  • 用户代理字符串
  • 触摸屏支持
  • 移动标志
自定义设备配置

如果预定义设备无法满足需求,可以创建自定义设备配置:

// 自定义设备
const myDevice = {
  viewport: { width: 375, height: 667 },
  deviceScaleFactor: 2,
  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',
};

// 在上下文中使用
const context = await browser.newContext({
  ...myDevice,
});

// 或在测试配置中使用
test.use({
  viewport: { width: 375, height: 667 },
  deviceScaleFactor: 2,
  isMobile: true,
  hasTouch: true,
  userAgent: 'Custom User Agent String',
});
模拟触摸事件

在移动设备模拟中,Playwright会自动将鼠标事件转换为相应的触摸事件:

// 这些操作在移动模式下会生成触摸事件
await page.tap('button.login');  // 点击/轻触
await page.dblclick('.item');    // 双击/双触
await page.hover('.menu');       // 触摸开始但不结束

// 特殊的触摸手势
// 滑动手势(从一点滑动到另一点)
await page.touchscreen.tap(100, 200); // 轻触特定坐标
await page.mouse.move(100, 200);
await page.mouse.down();
await page.mouse.move(100, 400); // 垂直滑动200像素
await page.mouse.up();
实现高级手势

对于更复杂的手势,如捏合缩放或多指操作,可以使用页面评估函数模拟:

// 模拟捏合缩放手势(放大)
await page.evaluate(() => {
  const touchStartEvent = new TouchEvent('touchstart', {
    touches: [
      new Touch({identifier: 0, target: document.body, pageX: 100, pageY: 100}),
      new Touch({identifier: 1, target: document.body, pageX: 200, pageY: 200})
    ],
    targetTouches: [],
    changedTouches: []
  });
  document.dispatchEvent(touchStartEvent);
  
  // 模拟手指移动(分开 - 放大)
  const touchMoveEvent = new TouchEvent('touchmove', {
    touches: [
      new Touch({identifier: 0, target: document.body, pageX: 50, pageY: 50}),
      new Touch({identifier: 1, target: document.body, pageX: 250, pageY: 250})
    ],
    targetTouches: [],
    changedTouches: []
  });
  document.dispatchEvent(touchMoveEvent);
  
  // 结束触摸
  const touchEndEvent = new TouchEvent('touchend', {
    touches: [],
    targetTouches: [],
    changedTouches: []
  });
  document.dispatchEvent(touchEndEvent);
});
测试响应式设计

响应式设计测试是移动测试的重要部分,Playwright可以帮助测试网站在不同屏幕尺寸下的表现:

// 定义多种屏幕尺寸
const screenSizes = [
  { width: 1920, height: 1080, name: 'Desktop' },
  { width: 1024, height: 768, name: 'Tablet Landscape' },
  { width: 768, height: 1024, name: 'Tablet Portrait' },
  { width: 375, height: 812, name: 'Mobile' }
];

// 测试响应式行为
for (const size of screenSizes) {
  test(`测试在${size.name}屏幕上的响应式布局`, async ({ browser }) => {
    const context = await browser.newContext({
      viewport: { width: size.width, height: size.height }
    });
    const page = await context.newPage();
    await page.goto('https://example.com');
    
    // 检查响应式元素是否正确显示
    if (size.width <= 768) {
      // 移动视图应该显示汉堡菜单
      await expect(page.locator('.mobile-menu-icon')).toBeVisible();
      await expect(page.locator('nav.desktop-menu')).toBeHidden();
    } else {
      // 桌面视图应该显示常规导航
      await expect(page.locator('.mobile-menu-icon')).toBeHidden();
      await expect(page.locator('nav.desktop-menu')).toBeVisible();
    }
    
    // 其他响应式测试...
    
    // 截图以便视觉检查
    await page.screenshot({ path: `responsive-${size.name}.png` });
    
    await context.close();
  });
}
移动特定功能测试
测试地理位置功能
// 模拟北京的地理位置
const beijingLocation = {
  latitude: 39.9042,
  longitude: 116.4074
};

test('测试基于位置的功能', async ({ browser }) => {
  const context = await browser.newContext({
    ...devices['iPhone 13'],
    geolocation: beijingLocation,
    permissions: ['geolocation']
  });
  
  const page = await context.newPage();
  await page.goto('https://maps-example.com');
  
  // 验证位置是否正确显示
  await expect(page.locator('.current-location')).toContainText('北京');
  
  // 更改位置并检查更新
  await context.setGeolocation({
    latitude: 31.2304,
    longitude: 121.4737 // 上海位置
  });
  
  // 注意:位置更改后可能需要触发位置重新获取
  await page.reload();
  await expect(page.locator('.current-location')).toContainText('上海');
});
测试设备方向
// 测试横屏/竖屏切换
test('测试屏幕方向响应', async ({ browser }) => {
  // 开始以竖屏模式
  const context = await browser.newContext({
    ...devices['iPhone 13'],
  });
  const page = await context.newPage();
  await page.goto('https://example.com');
  await expect(page.locator('.portrait-content')).toBeVisible();
  
  // 切换到横屏模式
  await context.setViewportSize({ 
    width: 844,   // iPhone 13 高度
    height: 390   // iPhone 13 宽度
  });
  
  // 模拟orientation change事件
  await page.evaluate(() => {
    window.dispatchEvent(new Event('orientationchange'));
    // 有些应用监听resize事件而非orientationchange
    window.dispatchEvent(new Event('resize'));
  });
  
  await expect(page.locator('.landscape-content')).toBeVisible();
});
测试App下载横幅

很多网站在移动访问时会显示应用下载横幅,也需要测试:

// 使用iPhone设备模拟
test.use({ ...devices['iPhone 13'] });

test('测试App下载横幅', async ({ page }) => {
  await page.goto('https://example.com');
  
  // 检查是否显示App下载横幅
  const appBanner = page.locator('.app-install-banner');
  await expect(appBanner).toBeVisible();
  
  // 测试关闭按钮
  await page.click('.banner-close-button');
  await expect(appBanner).toBeHidden();
  
  // 测试"安装应用"按钮
  await page.reload(); // 重新加载以显示横幅
  await page.click('.install-app-button');
  // 验证相应行为...
});
常见移动端测试场景
表单填写体验
// 使用iPhone设备模拟
test.use({ ...devices['iPhone 13'] });

test('移动端表单填写体验', async ({ page }) => {
  await page.goto('https://example.com/form');
  
  // 测试输入框聚焦是否正确(例如虚拟键盘显示时页面是否正确滚动)
  await page.tap('input#email');
  
  // 等待可能的视图调整动画完成
  await page.waitForTimeout(300);
  
  // 验证输入框在可视区域内
  const inputBox = await page.locator('input#email').boundingBox();
  const viewportSize = page.viewportSize();
  expect(inputBox.y).toBeLessThan(viewportSize.height);
  
  // 填写表单
  await page.fill('input#email', '[email protected]');
  await page.tap('input#password');
  await page.fill('input#password', 'password123');
  
  // 测试表单提交
  await page.tap('button[type="submit"]');
  await expect(page.locator('.success-message')).toBeVisible();
});
下拉刷新
// 使用iPhone设备模拟
test.use({ ...devices['iPhone 13'] });

test('测试下拉刷新功能', async ({ page }) => {
  await page.goto('https://example.com/feed');
  
  // 记录当前内容
  const initialContent = await page.locator('.feed-item').count();
  
  // 模拟下拉刷新手势
  const middleX = page.viewportSize().width / 2;
  await page.mouse.move(middleX, 100);
  await page.mouse.down();
  await page.mouse.move(middleX, 300); // 向下拖动
  await page.waitForTimeout(500); // 等待刷新触发
  await page.mouse.up();
  
  // 等待刷新完成(例如,刷新指示器消失)
  await page.waitForSelector('.refresh-indicator', { state: 'hidden' });
  
  // 验证内容是否更新
  const newContentCount = await page.locator('.feed-item').count();
  expect(newContentCount).not.toBe(initialContent);
});
移动端测试最佳实践
  1. 始终在多个设备配置上测试

    • 至少测试iOS和Android的代表性设备
    • 同时测试手机和平板设备
  2. 测试真实的触摸交互

    • 使用tap()而非click()
    • 模拟复杂手势如滑动、捏合
  3. 注意视口差异

    • 移动端视口与CSS像素不同
    • 考虑设备像素比(DPR)的影响
  4. 测试特定移动功能

    • 地理位置服务
    • 设备方向
    • 移动专用UI元素(App下载横幅等)
  5. 避免依赖悬停状态

    • 移动设备没有真正的悬停状态
    • 设计替代测试方法验证桌面端的悬停效果
  6. 网络条件模拟

    • 模拟低速网络连接
    // 模拟3G网络
    await page.route('**/*', route => {
      route.continue({
        throttle: {
          downloadSpeed: 1.5 * 1024 * 1024 / 8, // 1.5Mbps
          uploadSpeed: 750 * 1024 / 8, // 750Kbps
          latency: 150 // 150ms延迟
        }
      });
    });
    
  7. 无障碍测试

    • 确保足够的触摸目标尺寸(至少44×44像素)
    • 验证无障碍标签在移动设备上的正常工作
  8. 虚拟键盘测试

    // 使用iPhone设备模拟
    test.use({ ...devices['iPhone 13'] });
    
    // 测试虚拟键盘行为
    test('测试虚拟键盘不遮挡表单字段', async ({ page }) => {
      await page.goto('https://example.com/form');
      
      // 轻触底部的输入字段
      const inputField = page.locator('input#message');
      await inputField.tap();
      
      // 检查字段是否在视口中可见(页面应该滚动)
      const inputBox = await inputField.boundingBox();
      const viewportSize = page.viewportSize();
      
      // 假设虚拟键盘大约占屏幕的40%
      const estimatedKeyboardHeight = viewportSize.height * 0.4;
      const visibleViewportHeight = viewportSize.height - estimatedKeyboardHeight;
      
      expect(inputBox.y + inputBox.height).toBeLessThan(visibleViewportHeight);
    });
    
  9. 系统UI考量

    • iOS的底部手势条/底部功能区
    • 确保关键UI元素不被系统UI遮挡
    • 针对全面屏设备进行特别测试
  10. 设备方向测试

    • 测试横屏和竖屏模式
    • 验证响应式设计在方向变化时正确工作
移动端测试技术限制

使用Playwright进行移动设备模拟有一些技术限制需要注意:

  1. 这是模拟而非真机测试

    • 模拟的移动环境与真实设备有差异
    • 无法测试特定于移动操作系统的功能
  2. 浏览器差异

    • 模拟的移动Safari是基于WebKit的,但与iOS上的Safari有一定差异
    • 同样,模拟的Chrome移动版与Android设备上的有差异
  3. 无法测试的功能

    • 设备传感器(加速度计、陀螺仪等)
    • 原生应用集成
    • 真实触摸屏特性(如不同的压力级别)
  4. 何时需要真机测试

    • 当需要验证特定设备的实际外观和感觉
    • 当需要测试系统集成功能
    • 在生产发布前的最终验证
结合真机测试的策略

虽然Playwright的设备模拟非常强大,但它不能完全替代真机测试。以下是一种结合策略:

  1. 使用Playwright模拟测试进行广泛覆盖(多设备、多屏幕尺寸)
  2. 使用真实设备进行关键路径的验证
  3. 考虑为特定移动功能使用专门的移动测试工具(如Appium)

通过这种结合方法,可以利用Playwright的速度和便利性进行大部分测试,同时确保在真实设备上的兼容性。

总结

Playwright是一个功能丰富的自动化测试工具,提供了跨浏览器支持和强大的API。本文详细介绍了从安装配置到基础操作、元素定位、等待处理、自动录制以及移动端测试等核心功能,希望能帮助您快速上手Playwright自动化测试。

Playwright的关键优势包括:

  1. 跨浏览器支持:一套代码同时支持Chromium、Firefox和WebKit引擎
  2. 强大的自动等待机制:减少脆弱的显式等待代码
  3. 先进的定位机制:提供多种元素定位策略,增强测试稳定性
  4. 内置录制功能:加速测试脚本开发
  5. 现代Web特性支持:包括Shadow DOM、iframe、移动视图等
  6. 模拟设备能力:对移动设备的全面模拟支持

随着Web应用的复杂性不断提高,Playwright这样的现代化测试工具能够帮助开发者和测试人员更高效地进行自动化测试,提高软件质量。通过深入学习和实践Playwright,您将能够构建更稳定、更可维护的自动化测试套件,为您的Web应用保驾护航。

你可能感兴趣的:(测试工程师知识体系,playwright)