基本测试结构
import { test, expect } from '@playwright/test'
test.describe('测试套件名称', () => {
// 每个测试前的准备工作
test.beforeEach(async ({ page }) => {
await page.goto('/path')
// 其他准备工作...
})
// 单个测试用例
test('测试用例名称', async ({ page }) => {
// 测试步骤...
})
})
常用钩子
test.beforeAll(): 所有测试前执行一次test.beforeEach(): 每个测试前执行test.afterEach(): 每个测试后执行test.afterAll(): 所有测试后执行一次
常用 API 说明
页面导航
// 页面跳转
await page.goto('/path')
// 等待页面加载完成
await page.waitForLoadState('networkidle')
// 页面刷新
await page.reload()
// 前进/后退
await page.goForward()
await page.goBack()
// 获取当前 URL
const url = page.url()
// 获取页面标题
const title = await page.title()
元素定位
通过角色定位
// 1. 基础角色
await page.getByRole('button') // 按钮
await page.getByRole('checkbox') // 复选框
await page.getByRole('radio') // 单选框
await page.getByRole('textbox') // 文本输入框
await page.getByRole('combobox') // 下拉框
await page.getByRole('listbox') // 列表框
await page.getByRole('option') // 选项
await page.getByRole('link') // 链接
await page.getByRole('img') // 图片
await page.getByRole('heading') // 标题
await page.getByRole('paragraph') // 段落
await page.getByRole('list') // 列表
await page.getByRole('listitem') // 列表项
await page.getByRole('table') // 表格
await page.getByRole('row') // 表格行
await page.getByRole('cell') // 表格单元格
await page.getByRole('columnheader') // 表格列头
await page.getByRole('rowheader') // 表格行头
await page.getByRole('grid') // 网格
await page.getByRole('gridcell') // 网格单元格
await page.getByRole('tab') // 标签页
await page.getByRole('tablist') // 标签页列表
await page.getByRole('tabpanel') // 标签面板
await page.getByRole('dialog') // 对话框
await page.getByRole('alertdialog') // 警告对话框
await page.getByRole('alert') // 警告提示
await page.getByRole('status') // 状态信息
await page.getByRole('progressbar') // 进度条
await page.getByRole('slider') // 滑块
await page.getByRole('spinbutton') // 数字输入框
await page.getByRole('switch') // 开关
await page.getByRole('searchbox') // 搜索框
await page.getByRole('scrollbar') // 滚动条
await page.getByRole('separator') // 分隔符
await page.getByRole('toolbar') // 工具栏
await page.getByRole('tooltip') // 工具提示
await page.getByRole('tree') // 树形结构
await page.getByRole('treeitem') // 树形项
await page.getByRole('menubar') // 菜单栏
await page.getByRole('menu') // 菜单
await page.getByRole('menuitem') // 菜单项
await page.getByRole('menuitemcheckbox') // 菜单复选框项
await page.getByRole('menuitemradio') // 菜单单选框项
await page.getByRole('navigation') // 导航
await page.getByRole('main') // 主要内容
await page.getByRole('article') // 文章
await page.getByRole('aside') // 侧边栏
await page.getByRole('banner') // 横幅
await page.getByRole('complementary') // 补充内容
await page.getByRole('contentinfo') // 内容信息
await page.getByRole('form') // 表单
await page.getByRole('search') // 搜索区域
await page.getByRole('region') // 区域
await page.getByRole('figure') // 图片区域
await page.getByRole('group') // 分组
await page.getByRole('radiogroup') // 单选按钮组
await page.getByRole('spinbutton') // 数字输入框
await page.getByRole('timer') // 计时器
await page.getByRole('log') // 日志
await page.getByRole('marquee') // 滚动文本
await page.getByRole('math') // 数学公式
await page.getByRole('note') // 注释
await page.getByRole('presentation') // 展示
await page.getByRole('definition') // 定义
await page.getByRole('directory') // 目录
await page.getByRole('document') // 文档
await page.getByRole('feed') // 信息流
await page.getByRole('figure') // 图片区域
await page.getByRole('generic') // 通用容器
await page.getByRole('img') // 图片
await page.getByRole('list') // 列表
await page.getByRole('listitem') // 列表项
await page.getByRole('meter') // 计量器
await page.getByRole('none') // 无角色
await page.getByRole('term') // 术语
await page.getByRole('time') // 时间
await page.getByRole('tooltip') // 工具提示
// 角色定位的选项
await page.getByRole('button', {
name: '按钮文本', // 按钮文本
pressed: true, // 按钮状态
expanded: true, // 展开状态
disabled: false, // 禁用状态
selected: true, // 选中状态
checked: true, // 勾选状态
level: 1, // 标题级别
value: { min: 0, max: 100 }, // 数值范围
current: true, // 当前项
description: '描述文本', // 描述文本
includeHidden: false, // 是否包含隐藏元素
exact: true, // 精确匹配
})
// 组合角色定位
await page.getByRole('listitem', { has: page.getByRole('button') })
await page.getByRole('row', {
has: page.getByRole('cell', { name: '单元格文本' }),
})
await page.getByRole('tab', { selected: true })
await page.getByRole('option', { selected: true })
await page.getByRole('checkbox', { checked: true })
await page.getByRole('radio', { checked: true })
await page.getByRole('combobox', { expanded: true })
await page.getByRole('button', { pressed: true })
await page.getByRole('slider', { value: { min: 0, max: 100 } })
await page.getByRole('spinbutton', { value: { min: 0, max: 100 } })
await page.getByRole('meter', { value: { min: 0, max: 100 } })
await page.getByRole('progressbar', { value: { min: 0, max: 100 } })
await page.getByRole('scrollbar', { orientation: 'vertical' })
await page.getByRole('separator', { orientation: 'horizontal' })
await page.getByRole('tablist', { orientation: 'horizontal' })
await page.getByRole('toolbar', { orientation: 'horizontal' })
await page.getByRole('tree', { orientation: 'vertical' })
await page.getByRole('menubar', { orientation: 'horizontal' })
await page.getByRole('menu', { orientation: 'vertical' })
await page.getByRole('grid', { orientation: 'horizontal' })
await page.getByRole('listbox', { orientation: 'vertical' })
await page.getByRole('radiogroup', { orientation: 'horizontal' })
await page.getByRole('spinbutton', { orientation: 'vertical' })
await page.getByRole('slider', { orientation: 'horizontal' })
其他定位
// 通过标签定位
await page.getByLabel('标签文本')
// 通过文本定位
await page.getByText('文本内容')
await page.getByText('文本内容', { exact: true }) // 精确匹配
// 通过选择器定位
await page.waitForSelector('.class-name')
await page.$('.class-name')
await page.$$('.class-name')
// 通过测试 ID 定位
await page.getByTestId('test-id')
// 通过占位符定位
await page.getByPlaceholder('请输入...')
// 通过标题定位
await page.getByTitle('标题文本')
// 使用 locator 定位
// 1. CSS 选择器
const element = page.locator('.class-name')
const elements = page.locator('.class-name').all()
// 2. 文本内容
const textElement = page.locator('text=Hello')
const textElements = page.locator('text=Hello').all()
// 3. 组合选择器
const complexElement = page.locator('.class-name >> text=Hello')
const parentChild = page.locator('.parent >> .child')
// 4. 链式定位
const element = page
.locator('.container')
.locator('.item')
.locator('text=Click me')
// 5. 过滤定位
const visibleElement = page.locator('.item').filter({ hasText: 'Hello' })
const nthElement = page.locator('.item').nth(1)
// 6. 条件定位
const element = page.locator('.item', { hasText: 'Hello' })
const element = page.locator('.item', { has: page.locator('.child') })
// 7. 相对定位
const element = page.locator('.item').locator('..') // 父元素
const element = page.locator('.item').locator('> span') // 直接子元素
// 8. 属性定位
const element = page.locator('[data-testid="submit"]')
const element = page.locator('input[type="text"]')
// 9. 组合定位
const element = page.locator('div').filter({ hasText: 'Hello' }).first()
const element = page.locator('div').filter({ has: page.getByRole('button') })
// 10. 动态定位
const element = page.locator(`.item-${dynamicId}`)
const element = page.locator(`[data-id="${dynamicId}"]`)
// locator 的常用方法
const element = page.locator('.item')
// 等待元素可见
await element.waitFor({ state: 'visible' })
// 检查元素是否存在
const isVisible = await element.isVisible()
const isEnabled = await element.isEnabled()
// 获取元素属性
const text = await element.textContent()
const value = await element.inputValue()
const attribute = await element.getAttribute('data-testid')
// 获取元素位置和大小
const box = await element.boundingBox()
const { x, y, width, height } = box
// 获取元素数量
const count = await element.count()
// 获取所有匹配元素
const elements = await element.all()
// 获取第一个/最后一个元素
const first = await element.first()
const last = await element.last()
// 获取第 n 个元素
const nth = await element.nth(1)
// 检查元素状态
const isChecked = await element.isChecked()
const isDisabled = await element.isDisabled()
const isEditable = await element.isEditable()
元素交互
// 点击
await page.getByRole('button').click()
await page.getByRole('button').click({ force: true }) // 强制点击
await page.getByRole('button').click({ position: { x: 10, y: 10 } }) // 指定点击位置
// 双击
await page.getByRole('button').dblclick()
// 右键点击
await page.getByRole('button').click({ button: 'right' })
// 输入文本
await page.getByLabel('输入框').fill('文本内容')
await page.getByLabel('输入框').type('文本内容') // 模拟键盘输入
// 清空输入
await page.getByLabel('输入框').clear()
// 选择选项
await page.getByRole('combobox').selectOption('选项值')
await page.getByRole('combobox').selectOption({ label: '选项标签' })
// 勾选/取消勾选
await page.getByRole('checkbox').check()
await page.getByRole('checkbox').uncheck()
// 悬停
await page.getByRole('button').hover()
// 拖拽
await page.getByRole('draggable').dragTo(page.getByRole('droppable'))
等待机制
// 等待元素出现
await page.waitForSelector('.class-name')
// 等待元素消失
await page.waitForSelector('.class-name', { state: 'hidden' })
// 等待加载状态
await page.waitForSelector('.el-loading-mask', { state: 'hidden' })
// 等待函数条件
await page.waitForFunction(() => {
// 返回 true 时继续执行
return document.querySelectorAll('.class-name').length > 0
})
// 等待网络请求
await page.waitForResponse(response => response.url().includes('/api'))
// 等待多个条件
await Promise.all([
page.waitForSelector('.class-name'),
page.waitForLoadState('networkidle'),
])
// 设置超时时间
await page.waitForSelector('.class-name', { timeout: 10000 })
元素验证
// 检查元素可见性
await expect(page.getByText('文本')).toBeVisible()
await expect(page.getByText('文本')).toBeHidden()
// 检查元素包含文本
await expect(page.getByRole('row')).toContainText('文本')
// 检查元素不存在
await expect(page.getByText('文本')).not.toBeVisible()
// 检查元素数量
const elements = await page.$$('.class-name')
expect(elements.length).toBeGreaterThan(0)
// 检查元素属性
await expect(page.getByRole('button')).toHaveAttribute('disabled')
await expect(page.getByRole('input')).toHaveValue('输入值')
// 检查元素样式
await expect(page.getByRole('button')).toHaveCSS(
'background-color',
'rgb(0, 0, 0)'
)
// 检查元素类名
await expect(page.getByRole('button')).toHaveClass('active')
对话框处理
// 等待对话框显示
await page.waitForSelector('.el-dialog')
// 等待对话框关闭
await page.waitForSelector('.el-dialog', { state: 'hidden' })
// 点击对话框按钮
await page.getByRole('button', { name: '确定' }).click()
// 处理确认对话框
page.on('dialog', async dialog => {
await dialog.accept() // 确认
// await dialog.dismiss(); // 取消
})
// 处理文件选择对话框
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
page.getByRole('button', { name: '选择文件' }).click(),
])
await fileChooser.setFiles('path/to/file.pdf')
表格操作
// 等待表格加载
await page.waitForSelector('.el-table__body-wrapper')
// 获取表格行
const rows = await page.$$('.el-table__body-wrapper tr')
// 查找特定行
const row = page.getByRole('row').filter({ hasText: '行文本' })
// 操作行内按钮
await row.getByRole('button', { name: '按钮文本' }).click()
// 获取单元格内容
const cellText = await row.getByRole('cell').nth(0).textContent()
// 排序
await page.getByRole('columnheader', { name: '列名' }).click()
// 分页
await page.getByRole('button', { name: '下一页' }).click()
网络请求
// 监听请求
await page.route('**/api/*', route => {
// 修改请求
route.continue({
headers: { ...route.request().headers(), Authorization: 'Bearer token' },
})
})
// 模拟响应
await page.route('**/api/*', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ data: [] }),
})
})
// 等待请求完成
const response = await page.waitForResponse('**/api/*')
const responseBody = await response.json()
文件操作
// 上传文件
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
page.getByRole('button', { name: '上传' }).click(),
])
await fileChooser.setFiles('path/to/file.pdf')
// 下载文件
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: '下载' }).click(),
])
await download.saveAs('path/to/save.pdf')
键盘和鼠标操作
// 键盘操作
await page.keyboard.press('Enter')
await page.keyboard.type('Hello')
await page.keyboard.down('Shift')
await page.keyboard.up('Shift')
// 鼠标操作
await page.mouse.move(100, 100)
await page.mouse.down()
await page.mouse.move(200, 200)
await page.mouse.up()
断言
元素状态断言
// 可见性断言
await expect(page.locator('.element')).toBeVisible();
await expect(page.locator('.element')).toBeHidden();
await expect(page.locator('.element')).toHaveCount(3);
// 启用状态断言
await expect(page.locator('button')).toBeEnabled();
await expect(page.locator('button')).toBeDisabled();
// 选中状态断言
await expect(page.locator('checkbox')).toBeChecked();
await expect(page.locator('radio')).not.toBeChecked();
元素属性断言
// 文本内容断言
await expect(page.locator('.element')).toHaveText('预期文本');
await expect(page.locator('.element')).toContainText('部分文本');
await expect(page.locator('.element')).toHaveValue('输入值');
// 属性断言
await expect(page.locator('a')).toHaveAttribute('href', 'https://example.com');
await expect(page.locator('img')).toHaveAttribute('src', /.png$/);
// CSS 断言
await expect(page.locator('.element')).toHaveCSS('color', 'rgb(255, 0, 0)');
await expect(page.locator('.element')).toHaveClass('active');
页面状态断言
// URL 断言
await expect(page).toHaveURL('https://example.com');
await expect(page).toHaveURL(/.*/dashboard/);
// 标题断言
await expect(page).toHaveTitle('页面标题');
await expect(page).toHaveTitle(/.*Dashboard/);
// 响应断言
await expect(response).toBeOK();
await expect(response).toHaveStatus(200);
最佳实践
1. 使用数据属性
<button data-testid="submit-button">提交</button>
await page.locator('[data-testid="submit-button"]').click();
2. 等待策略
// 等待元素可见
await page.locator('.element').waitFor({ state: 'visible' });
// 等待网络请求
await page.waitForResponse(response => response.url().includes('/api/data'));
// 等待导航
await page.waitForURL('**/dashboard');
3. 错误处理
try {
await page.locator('.element').click();
} catch (error) {
console.log('元素点击失败:', error);
}
4. 测试隔离
test.beforeEach(async ({ page }) => {
// 清理测试数据
await page.evaluate(() => localStorage.clear());
});