Playwright 测试编写实战:从第一个测试到断言、隔离与 Hooks 的完整指南
本文基于 Playwright 官方文档 writing-tests(JavaScript 版),系统讲解如何编写 Playwright 测试:编写第一个测试、执行页面动作(导航与交互)、使用异步断言、利用 BrowserContext 实现测试隔离,以及用 test.describe / test.beforeEach 等 hooks 组织测试。读完本文,你将掌握 Playwright 测试的核心编写模式,并能结合仓库源码理解这些 API 背后的自动等待与轮询机制。
测试编写总览:执行动作并断言状态
Playwright 测试的核心范式非常简单:执行动作(perform actions),然后**断言状态(assert the state)**是否符合预期。
它有两个关键设计,决定了测试编写的整体风格:
- 动作前的自动等待:Playwright 在执行每个动作前,会自动等待 actionability(可操作性) 检查通过,你不需要手写任何
waitForTimeout,也不用担心竞态条件。 - 面向“终将成立”的断言:Playwright 的断言(expectations)被设计为描述“最终会被满足”的预期条件,通过自动重试轮询消除不稳定的超时和竞态检查。
官方文档给出的学习目标(也是本文的章节脉络):
- 如何编写第一个测试
- 如何执行动作(Actions)
- 如何使用断言(Assertions)
- 测试如何以隔离方式运行(Test Isolation)
- 如何使用测试 hooks
第一个测试
先看一个完整示例,覆盖“导航 + 标题断言 + 点击 + 可见性断言”的最小闭环。文件保存为 tests/example.spec.ts:
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Click the get started link.
await page.getByRole('link', { name: 'Get started' }).click();
// Expects page to have a heading with the name of Installation.
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
要点说明:
test与expect都从@playwright/test包导入,这是测试入口(对应仓库中的 packages/playwright)。- 测试函数签名是
async ({ page }) =>,page是 Playwright 内置的 fixture(测试夹具),由框架自动创建并注入,无需手动browser.newPage()。 toHaveTitle(/Playwright/)接受正则,做的是“标题包含子串”的匹配,而非精确匹配。
提示(来自原文档):在 VS Code 中使用 JavaScript(而非 TypeScript)编写测试时,在每个测试文件开头添加
// @ts-check,即可获得自动类型检查。
动作(Actions):导航与交互
导航(Navigation)
大多数测试从导航到某个 URL 开始,之后再与页面元素交互:
await page.goto('https://playwright.dev/');
Python 版本对应写法(跨语言参考):
page.goto("https://playwright.dev/")
page.goto 会等待页面达到 load 状态后才继续,避免“页面还没加载就开始操作”的问题。完整参数(如 waitUntil、timeout 等)可查阅 Page.goto API。
交互(Interactions):基于 Locator 定位元素
执行动作的第一步是定位元素,Playwright 使用 Locators API 来完成。Locator 代表“在任意时刻都能找到页面元素”的一种惰性查询——它不绑定某个具体 DOM 节点,而是绑定一段查找逻辑,因此天然适配动态更新的页面。定位器的种类(getByRole、getByText、getByLabel 等)见 Locators 指南。
Playwright 在执行动作前会等待元素变为 actionable(可操作)(可见、稳定、可接收事件、未被其他元素遮挡等),所以不需要自己等待元素“出现”:
// Create a locator.
const getStarted = page.getByRole('link', { name: 'Get started' });
// Click it.
await getStarted.click();
大多数场景会直接写成一行链式调用:
await page.getByRole('link', { name: 'Get started' }).click();
常用基础动作一览
以下是 Playwright 中最常用的动作,完整列表见 Locator API:
| 动作 | 说明 |
|---|---|
Locator.check |
勾选复选框 |
Locator.click |
点击元素 |
Locator.uncheck |
取消勾选复选框 |
Locator.hover |
鼠标悬停在元素上 |
Locator.fill |
填充表单字段 / 输入文本 |
Locator.focus |
聚焦元素 |
Locator.press |
按下单个按键 |
Locator.setInputFiles |
选择文件上传 |
Locator.selectOption |
在下拉框中选择选项 |
断言(Assertions):异步等待型断言消除不稳定
Playwright 内置的测试断言以 expect 函数的形式提供:调用 expect(value) 后选择一个匹配器(matcher)来表达预期。其实现入口在 matchers/expect.ts,由 packages/playwright/src/index.ts 统一导出。
异步匹配器:等待直到条件成立
Playwright 包含异步匹配器,它们会持续重试,直到预期条件被满足(或超时)。使用这些匹配器是测试不 flaky、具备韧性的关键。例如,下面这行代码会等待直到页面标题包含 "Playwright":
await expect(page).toHaveTitle(/Playwright/);
以下是最常用的异步断言,完整列表见 断言指南:
| 断言 | 说明 |
|---|---|
LocatorAssertions.toBeChecked |
复选框已被勾选 |
LocatorAssertions.toBeEnabled |
控件处于启用状态 |
LocatorAssertions.toBeVisible |
元素可见 |
LocatorAssertions.toContainText |
元素包含指定文本 |
LocatorAssertions.toHaveAttribute |
元素具有指定属性 |
LocatorAssertions.toHaveCount |
元素列表具有指定长度 |
LocatorAssertions.toHaveText |
元素匹配指定文本 |
LocatorAssertions.toHaveValue |
输入元素具有指定值 |
PageAssertions.toHaveTitle |
页面具有指定标题 |
PageAssertions.toHaveURL |
页面处于指定 URL |
从源码看异步断言的实现
在仓库中,这些异步匹配器都定义在 packages/playwright/src/matchers/matchers.ts。以 toBeVisible 和 toBeEnabled 为例(第 185 行、第 152 行附近):
// packages/playwright/src/matchers/matchers.ts
export function toBeVisible(
this: ExpectMatcherStateInternal,
locator: LocatorEx,
options?: { visible?: boolean, timeout?: number, signal?: AbortSignal },
) {
const visible = !options || options.visible === undefined || options.visible;
const expected = visible ? 'visible' : 'hidden';
// ...
return toBeTruthy.call(this, 'toBeVisible', locator, 'Locator', expected, arg, async (isNot, timeout, signal) => {
return await locator._expect(visible ? 'to.be.visible' : 'to.be.hidden', { isNot, timeout, signal, title: this.title });
}, options);
}
从源码结构看,每个 Locator 断言最终都通过 locator._expect(...) 委托给注入到页面的 utility script 在浏览器端执行检查——这正是断言能够“反复轮询直到条件成立”的底层机制。另外值得注意的是:
toBeVisible/toBeEnabled/toBeChecked都支持反向参数(如{ visible: false }、{ enabled: false }、{ checked: false }),一个匹配器即可覆盖正反两种预期;- 多数匹配器支持
timeout和AbortSignal参数,用于覆盖默认超时或响应取消。
断言超时的默认值与优先级
在 packages/playwright/src/matchers/expect.ts 中可以确认异步断言的默认超时与解析优先级:
const defaultExpectTimeout = 5000;
// ...
const timeout = info.timeout ?? expectConfig().timeout ?? defaultExpectTimeout;
即:匹配器自身的 timeout 参数 > expect.configure({ timeout }) 全局配置 > 默认值 5000ms。这意味着若页面条件在 5 秒内无法成立,断言才会失败;你可以按项目需要调整其中任何一级。
通用匹配器:同步检查已就绪的值
Playwright 还包含 toEqual、toContain、toBeTruthy 等通用匹配器,用于断言任意条件。与异步匹配器不同,它们执行的是对已就绪值的立即同步检查,因此**不需要 await:
expect(success).toBeTruthy();
适用边界:通用匹配器适合对同步计算结果(如函数返回值、配置值)做即时检查;对页面状态(元素是否存在、文本是否出现)则应优先使用带 await 的异步匹配器。
测试隔离(Test Isolation):每个测试都是全新环境
Playwright Test 建立在 test fixtures(测试夹具) 的概念之上,例如传入测试的内置 page fixture。关键在于:页面通过 BrowserContext 在测试之间相互隔离——一个 BrowserContext 相当于一个全新的浏览器配置(干净的 Cookie、localStorage、缓存),因此即使多个测试运行在同一个浏览器进程内,每个测试拿到的也是一个全新环境。
import { test } from '@playwright/test';
test('example test', async ({ page }) => {
// "page" belongs to an isolated BrowserContext, created for this specific test.
});
test('another test', async ({ page }) => {
// "page" in this second test is completely isolated from the first test.
});
实践含义:
- 测试之间不共享状态,顺序无关,可安全并行;
- 登录等一次性操作不适合写在每个测试里反复执行,应通过 fixtures 或 storage state 复用,避免重复成本(详见 BrowserContexts 指南);
- 隔离是以 BrowserContext 为粒度实现的,而非为每个测试启动新浏览器,因此在性能上开销可控。
使用测试 Hooks:组织与复用
Playwright 提供了多种 test hooks:
| Hook | 执行时机 |
|---|---|
test.describe |
声明一组测试(测试分组) |
test.beforeEach |
每个测试执行之前 |
test.afterEach |
每个测试执行之后 |
test.beforeAll |
每个 worker 中所有测试执行之前(仅一次) |
test.afterAll |
每个 worker 中所有测试执行之后(仅一次) |
注意 beforeAll / afterAll 是每 worker 执行一次,而非整个测试文件只执行一次——在并行执行下,同一文件的不同测试可能被拆分到不同 worker。
import { test, expect } from '@playwright/test';
test.describe('navigation', () => {
test.beforeEach(async ({ page }) => {
// Go to the starting url before each test.
await page.goto('https://playwright.dev/');
});
test('main navigation', async ({ page }) => {
// Assertions use the expect API.
await expect(page).toHaveURL('https://playwright.dev/');
});
});
这种“beforeEach 准备环境 + describe 按功能分组”的组织方式是官方示例项目的一贯写法。仓库中的 examples/todomvc/tests 即按 adding-todos、completing-todos、deleting-todos、editing-todos、filtering-todos、todo-creation 等目录分模块组织,并配套公共 fixtures.ts 来复用测试前置逻辑,可作为大型测试套件的参考结构。
后续学习路径
按照原文档给出的 What's Next,编写完测试后可以继续:
- 运行单个测试、多个测试、headed 模式
- 使用 Codegen 生成测试
- 查看测试的 trace 追踪
- 探索 UI Mode
- 在 CI 中使用 GitHub Actions 运行测试
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00