API 教程与代码示例
掌握 Playwright Test 最常用的 TypeScript API,配合可运行的代码示例快速学习。
元素定位
Playwright 推荐优先使用语义化定位方法(getByRole 等),它们更贴近用户视角。
locators.spec.ts — 推荐方式
// ===== 推荐:语义化定位 ===== // 按 ARIA 角色定位 — 最推荐 page.getByRole('button', { name: '提交' }); page.getByRole('heading', { name: '欢迎', level: 1 }); page.getByRole('link', { name: '首页' }); page.getByRole('textbox', { name: '用户名' }); page.getByRole('checkbox', { name: '记住我' }); // 按文本 page.getByText('立即注册'); page.getByText('注册', { exact: true }); // 按 label / placeholder / alt / test-id page.getByLabel('邮箱地址'); page.getByPlaceholder('请输入搜索内容'); page.getByAltText('公司 Logo'); page.getByTestId('submit-button');
locators.spec.ts — CSS / 链式过滤
// CSS 选择器 page.locator('#login-form'); page.locator('.btn.btn-primary'); page.locator('input[type="email"]'); // 链式过滤 page.locator('.product-card') .filter({ hasText: 'TypeScript' }) .first(); // 包含特定子元素 page.locator('.card').filter({ has: page.getByRole('button', { name: '购买' }), }); // 第 N 个 / 计数 page.locator('li.item').nth(2); page.locator('li.item').first(); const count = await page.locator('li').count();
页面操作
actions.spec.ts
// ===== 点击 ===== await page.getByRole('button', { name: '提交' }).click(); await page.locator('#item').dblclick(); await page.locator('#menu').click({ button: 'right' }); await page.locator('#link').click({ modifiers: ['Shift'] }); // ===== 输入 ===== await page.getByLabel('用户名').fill('admin'); // 清空再填 await page.getByLabel('搜索').pressSequentially('Playwright', { delay: 80, // 逐字输入 }); await page.keyboard.press('Enter'); // ===== 选择 ===== await page.locator('#city').selectOption('shanghai'); await page.locator('#city').selectOption({ label: '上海' }); // ===== 复选框 ===== await page.getByRole('checkbox', { name: '同意' }).check(); await page.getByRole('checkbox', { name: '同意' }).uncheck(); await page.getByRole('checkbox', { name: '同意' }).setChecked(true); // ===== 文件上传 ===== await page.locator('input[type="file"]').setInputFiles('report.pdf'); await page.locator('input[type="file"]').setInputFiles([]); // ===== 拖放 / 悬停 ===== await page.locator('#source').dragTo(page.locator('#target')); await page.locator('.dropdown').hover(); // ===== 截图 ===== await page.screenshot({ path: 'full.png', fullPage: true }); await page.locator('.chart').screenshot({ path: 'chart.png' });
断言(Assertions)
Playwright Test 提供自带自动重试的 expect 断言。
assertions.spec.ts
import { test, expect } from '@playwright/test'; test('assertions', async ({ page }) => { // ===== 页面断言 ===== await expect(page).toHaveTitle(/Dashboard/); await expect(page).toHaveURL('**/dashboard'); // ===== 元素断言(自动重试) ===== const msg = page.locator('.message'); await expect(msg).toBeVisible(); await expect(msg).toHaveText('操作成功'); await expect(msg).toContainText('成功'); await expect(msg).toHaveClass(/success/); await expect(msg).toHaveAttribute('role', 'alert'); await expect(msg).toBeEnabled(); // 否定 await expect(msg).not.toBeVisible(); // 列表 const items = page.locator('ul > li'); await expect(items).toHaveCount(5); await expect(items).toHaveText(['A', 'B', 'C', 'D', 'E']); // ===== 截图对比(Visual) ===== await expect(page).toHaveScreenshot('home.png'); await expect(page.locator('.hero')).toHaveScreenshot('hero.png', { maxDiffPixelRatio: 0.01, }); // ===== 软断言(不立即失败) ===== await expect.soft(msg).toHaveText('OK'); await expect.soft(page).toHaveTitle('Home'); // 所有软断言结果在测试结束时汇总 });
网络处理
network.spec.ts
import { test, expect } from '@playwright/test'; test('network interception', async ({ page }) => { // ===== 拦截并修改 ===== await page.route('**/*.{png,jpg,gif}', route => route.abort()); // ===== Mock API ===== await page.route('**/api/users', async route => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ users: [{ name: 'Alice' }, { name: 'Bob' }], }), }); }); // ===== 修改请求头 ===== await page.route('**/api/**', async route => { const headers = route.request().headers(); await route.continue_({ headers: { ...headers, 'X-Custom': 'value' }, }); }); // ===== 等待特定响应 ===== const responsePromise = page.waitForResponse('**/api/submit'); await page.getByRole('button', { name: '提交' }).click(); const response = await responsePromise; expect(response.status()).toBe(200); }); // ===== API 测试(使用 request fixture) ===== test('API testing', async ({ request }) => { const res = await request.post('/api/login', { data: { username: 'admin', password: 'secret' }, }); expect(res.ok()).toBeTruthy(); const body = await res.json(); expect(body.token).toBeDefined(); });
Test Fixtures
Playwright Test 的核心特性——依赖注入系统。
fixtures.ts — 自定义 Fixture
import { test as base, expect, Page } from '@playwright/test'; // 定义自定义 fixture 类型 type MyFixtures = { adminPage: Page; todoPage: TodoPage; }; // Page Object class TodoPage { constructor(private page: Page) {} async goto() { await this.page.goto('/todos'); } async addTodo(text: string) { await this.page.getByPlaceholder('What needs to be done?').fill(text); await this.page.keyboard.press('Enter'); } get items() { return this.page.getByTestId('todo-item'); } } // 扩展 test export const test = base.extend<MyFixtures>({ // 每个测试获得已登录的管理员页面 adminPage: async ({ browser }, use) => { const ctx = await browser.newContext({ storageState: 'admin-auth.json', }); const page = await ctx.newPage(); await use(page); await ctx.close(); }, // Page Object fixture todoPage: async ({ page }, use) => { const todoPage = new TodoPage(page); await todoPage.goto(); await use(todoPage); }, });
todo.spec.ts — 使用自定义 Fixture
import { test } from './fixtures'; import { expect } from '@playwright/test'; test('can add todos', async ({ todoPage }) => { await todoPage.addTodo('Learn Playwright'); await todoPage.addTodo('Write tests'); await expect(todoPage.items).toHaveCount(2); }); test('admin can manage', async ({ adminPage }) => { await adminPage.goto('/admin'); await expect(adminPage.getByText('Admin Panel')).toBeVisible(); });
高级功能
auth-setup.ts
import { test as setup } from '@playwright/test'; // 全局 setup:登录并保存状态 setup('authenticate', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill('[email protected]'); await page.getByLabel('Password').fill('password'); await page.getByRole('button', { name: 'Sign in' }).click(); await page.waitForURL('/dashboard'); // 保存状态供后续测试使用 await page.context().storageState({ path: '.auth/user.json', }); });
multi-tab.spec.ts
test('multi tab', async ({ page, context }) => { // 新标签页 const [newPage] = await Promise.all([ context.waitForEvent('page'), page.getByText('Open tab').click(), ]); await expect(newPage).toHaveURL(/new-tab/); // 弹窗 page.on('dialog', d => d.accept()); // 下载 const [download] = await Promise.all([ page.waitForEvent('download'), page.getByText('Download').click(), ]); await download.saveAs('report.xlsx'); });
device.spec.ts
import { devices } from '@playwright/test'; // 在 config 中配置设备 // projects: [ // { name: 'Mobile', use: devices['iPhone 14'] } // ] // 或在测试中动态创建 test('mobile view', async ({ browser }) => { const ctx = await browser.newContext({ ...devices['iPhone 14'], locale: 'zh-CN', geolocation: { latitude: 31.23, longitude: 121.47 }, permissions: ['geolocation'], colorScheme: 'dark', }); const page = await ctx.newPage(); // ... });
trace.spec.ts
// 在 config 中启用 trace // use: { trace: 'on-first-retry' } // 或手动控制 test('with trace', async ({ page, context }) => { await context.tracing.start({ screenshots: true, snapshots: true, sources: true, }); await page.goto('/'); // ... test steps ... await context.tracing.stop({ path: 'trace.zip', }); // npx playwright show-trace trace.zip });