超时、重试与显式等待策略
本教程共 59 篇 · 第 23 篇 · 更新于 2026-08-04 · 约 11 分钟阅读
本节目标:分清 Playwright 各级超时的作用范围,会在自动等待兜不住时挑对显式等待的方法。
「测试跑起来时好时坏」——这句话背后,九成是等待没处理对。
Playwright 已经把大部分等待自动化了。但总有它猜不到的场景,需要你手动补。这章先把超时体系理清楚,再讲什么时候该显式等待、用哪一个。
超时分几级
Playwright Test 有一整套独立的超时,互不干扰。先看总表:
| 超时类型 | 默认值 | 管什么 |
|---|---|---|
| 测试超时 | 30 秒 | 单个测试函数整体 |
| 断言超时 | 5 秒 | 单个自动重试断言 |
| 操作超时 | 无 | 单次 click / fill 等 |
| 导航超时 | 无 | 单次 goto / 跳转 |
beforeAll / afterAll 超时 | 30 秒 | 单个钩子 |
| 夹具超时 | 无 | 单个夹具(Fixture) |
| 全局超时 | 无 | 整轮测试运行 |
「无」表示不单独限时,最终受测试超时约束。
新手最容易混的是前两个。测试超时 30 秒,断言超时 5 秒,两者完全独立。 一个测试里有 10 个断言,每个最多等 5 秒,但整体不能超过 30 秒。
测试超时
超时报错长这样:
example.spec.ts:3:1 › basic test ===========================
Timeout of 30000ms exceeded.
计入测试超时的部分:测试函数本体、夹具的初始化、beforeEach 钩子。
夹具的清理和 afterEach 另算一份同样长度的时间,不和测试抢。
配置方式有三种:
// 1. 全局,playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
timeout: 120_000,
});
// 2. 单个测试
test('很慢的测试', async ({ page }) => {
test.setTimeout(120_000);
// ...
});
test('稍微慢一点', async ({ page }) => {
test.slow(); // 一句话把默认超时乘以 3
// ...
});
// 3. 在 beforeEach 里给这一组都加时间
test.beforeEach(async ({ page }, testInfo) => {
testInfo.setTimeout(testInfo.timeout + 30_000);
});
test.slow() 挺好用。不用算具体数字,标一下就行。
断言超时
超时报错会把 Call log 一起打出来:
Error: expect(received).toHaveText(expected)
Expected string: "我的文字"
Received string: ""
Call log:
- expect.toHaveText with timeout 5000ms
- waiting for "locator('button')"
调整方式:
// 全局,playwright.config.ts
export default defineConfig({
expect: { timeout: 10_000 },
});
// 单条断言
await expect(locator).toHaveText('hello', { timeout: 10_000 });
操作与导航超时
这两个默认不限时,靠测试超时兜底。
想单独限制,在配置的 use 里设:
// playwright.config.ts
export default defineConfig({
use: {
actionTimeout: 10_000,
navigationTimeout: 30_000,
},
});
单次覆盖:
await page.goto('https://example.com', { timeout: 30_000 });
await page.getByText('开始').click({ timeout: 10_000 });
Tip官方的态度是:这些底层超时一般不用动。如果你在翻这一节是因为测试不稳,问题多半在别处——定位器不精确、缺显式等待、或者应用本身有竞态。
全局超时
限制整轮测试的总时长,防止 CI 上卡死烧机器:
// playwright.config.ts
export default defineConfig({
globalTimeout: 3_600_000, // 1 小时
});
超时输出:
Running 1000 tests using 10 workers
514 skipped
486 passed
Timed out waiting 3600s for the entire test run
夹具超时
慢的 Fixture(夹具,比如要启动一个服务)可以单独给时间,不占测试额度:
import { test as base } from '@playwright/test';
export const test = base.extend<{ slowFixture: string }>({
slowFixture: [async ({}, use) => {
// 一些很慢的准备工作
await use('hello');
}, { timeout: 60_000 }],
});
什么时候需要显式等待
先说结论:大多数时候不需要。
Playwright 的自动等待覆盖了两大块:
- 操作前的可操作性检查(第 17 章)——等元素可见、稳定、能收事件、启用。
- 断言的自动重试(第 22 章)——等条件成立。
这两块加起来,日常 80% 的等待需求就没了。
剩下 20% 是这些情况:
- 等某个不参与断言的中间状态(比如加载动画消失)。
- 等 URL 跳到某个地址。
- 等一个网络请求或响应。
- 等页面里某个 JS 变量变成特定值。
- 等一个事件(弹窗、下载、新标签页)。
locator.waitFor:等元素到某个状态
// 等元素可见(默认)
await page.getByTestId('result').waitFor();
// 等元素消失
await page.getByTestId('loading-spinner').waitFor({ state: 'hidden' });
// 等元素挂到 DOM(不要求可见)
await page.getByTestId('lazy-block').waitFor({ state: 'attached' });
// 等元素从 DOM 移除
await page.getByTestId('toast').waitFor({ state: 'detached' });
// 自定义超时
await page.getByTestId('result').waitFor({ timeout: 15_000 });
四种状态记一下:visible、hidden、attached、detached。
最常见的用法是等遮罩层消失:
await page.getByRole('button', { name: '查询' }).click();
await page.getByTestId('loading-mask').waitFor({ state: 'hidden' });
await expect(page.getByRole('table')).toBeVisible();
Note能用断言表达的,优先用断言。
await expect(locator).toBeVisible()和await locator.waitFor()效果接近,但断言失败时的报错信息更完整,也会计入报告。waitFor更适合「只是要等一下,不算一条检查项」的场合。
page.waitForURL:等地址变化
// 等跳到指定地址
await page.waitForURL('https://example.com/dashboard');
// 支持通配符
await page.waitForURL('**/dashboard');
// 支持正则
await page.waitForURL(/\/orders\/\d+/);
// 等到网络空闲再算数
await page.waitForURL('**/dashboard', { waitUntil: 'networkidle' });
登录后跳转是典型场景:
await page.getByRole('button', { name: '登录' }).click();
await page.waitForURL('**/dashboard');
await expect(page.getByText('欢迎回来')).toBeVisible();
page.waitForLoadState:等加载阶段
await page.waitForLoadState(); // 默认 'load'
await page.waitForLoadState('domcontentloaded'); // DOM 解析完
await page.waitForLoadState('networkidle'); // 网络安静下来
三个阶段的含义:
domcontentloaded:HTML 解析完,图片样式可能还在下。load:load事件触发,所有资源加载完。networkidle:至少 500 毫秒内没有新的网络连接。
page.goto() 默认已经等到 load,所以多数时候不用手动调。
Warning
networkidle看着很美,实则不建议常用。带轮询、长连接、埋点上报的页面永远「不安静」,这一等就是超时。官方也明确不推荐把它当默认等待手段。
page.waitForFunction:等 JS 条件成立
在页面里反复执行一个函数,直到返回真值:
// 等某个全局变量就绪。window 上的自定义属性 TS 不认识,断言一下
await page.waitForFunction(() => (window as any).appReady === true);
// 等元素数量达到预期
await page.waitForFunction(() => document.querySelectorAll('.item').length >= 10);
// 传参进去
const target = 20;
await page.waitForFunction(
expected => document.querySelectorAll('.item').length >= expected,
target,
);
它默认按 requestAnimationFrame 的节奏轮询,也可以改成固定间隔:
await page.waitForFunction(() => (window as any).dataLoaded, null, { polling: 1000 });
这是个万能兜底,但也意味着测试开始依赖内部实现。能用可见状态表达的,就别掏这个。
等事件:waitForEvent 家族
第 20、21 章已经用过这个模式:
// 等新标签页
const pagePromise = context.waitForEvent('page');
await page.getByText('新窗口打开').click();
const newPage = await pagePromise;
// 等下载
const downloadPromise = page.waitForEvent('download');
await page.getByText('下载').click();
const download = await downloadPromise;
// 等弹出窗口
const popupPromise = page.waitForEvent('popup');
await page.getByText('打开弹窗').click();
const popup = await popupPromise;
网络方向有两个专用方法:
// 等某个请求发出
const requestPromise = page.waitForRequest('**/api/search*');
await page.getByRole('button', { name: '搜索' }).click();
const request = await requestPromise;
// 等某个响应回来
const responsePromise = page.waitForResponse(
res => res.url().includes('/api/search') && res.status() === 200,
);
await page.getByRole('button', { name: '搜索' }).click();
const response = await responsePromise;
console.log(await response.json());
再强调一次那个顺序规则:
Warning先挂等待(不加
await),再触发动作,最后awaitPromise。反过来写——先点击再等事件——会漏掉在这中间已经发生的事件,运气不好就永久卡住。
waitForTimeout:能不用就不用
死等固定毫秒数:
await page.waitForTimeout(2000); // 干等 2 秒
它的问题很直白:
- 快的时候浪费时间,一百个测试就是几分钟。
- 慢的时候还是不够,照样失败。
- 掩盖了真实问题,让人以为「加个等待就好了」。
官方在这个 API 的文档里写得很直白:生产环境的测试不要用它,靠时间等待的测试天生就是脆的。
那什么时候能用?我自己只在两种场合用过:
- 本地临时调试,想看清页面某一刻的样子。
- 确实要测「停留 N 秒后自动触发」的逻辑——但这种场景更好的选择是时钟模拟(第 29 章)。
一张决策表
遇到「这里要等一下」,照这个顺序想:
- 能不能靠自动等待?——直接写操作或断言,先跑一次看看。
- 是等元素状态?——
expect(locator).toBeVisible()或locator.waitFor()。 - 是等页面跳转?——
page.waitForURL()。 - 是等接口?——
page.waitForResponse()。 - 是等事件?——
page.waitForEvent()。 - 是等复杂条件?——
expect.poll()或expect.toPass()。 - 是等 JS 内部状态?——
page.waitForFunction()。 - 以上都不行——才考虑
waitForTimeout(),并留个注释说明原因。
小结
- 超时分七级,最常打交道的是测试超时(30 秒)和断言超时(5 秒),两者独立。
- 单测试放宽用
test.slow()或test.setTimeout(),全局在配置里改。 - 操作和导航默认不限时,靠测试超时兜底,一般不用单独配。
- 自动等待能覆盖大部分场景,显式等待是补充不是主力。
locator.waitFor等元素状态,waitForURL等跳转,waitForResponse等接口,waitForEvent等事件。networkidle在有轮询的页面上会卡死,慎用。- 事件等待永远是「先挂 Promise 再触发动作」。
waitForTimeout是最后手段,用了就等于埋了个雷。
下一章预告:浏览器/上下文/页面三层级模型——
page到底从哪来,测试隔离又是怎么做到的。