我们的立场很明确:我们是 Geonode,主要销售代理服务,而 Playwright 经常通过这些代理进行数据抓取和地理位置测试。 需要坦诚说明的是,绝大多数 Playwright 超时问题与代理无关。 选择器匹配不到任何内容、元素被 Cookie 提示框遮挡、动画始终无法停止——无论您的流量是直接传输还是经过六个中间节点,这些情况都会引发相同的错误。 文末提到了一个真正的与代理相关的情况:家庭网络连接会增加实际延迟,因此针对本地测试调优的默认设置会导致虚假失败。但请先检查选择器。如果未配置代理时测试也以完全相同的方式失败,那么问题就不在于代理。
六种超时及其默认值
首先需要理解的是,这些是各自独立的机制,具有不同的默认值;了解是哪一种机制触发了超时,就能知道该从哪里排查问题。
| 超时类型 | 默认值 | 设置方式 |
|---|---|---|
| 测试 | 30,000 毫秒 | testConfig.timeout, test.setTimeout() |
| 预期 | 5,000 毫秒 | testConfig.expect.timeout, 每个断言的选项 |
| 操作 | 无超时 | testOptions.actionTimeout, 每次调用的选项 |
| 导航 | 无超时 | testOptions.navigationTimeout, 每次调用的选项 |
| beforeAll / afterAll 钩子 | 30,000 毫秒 | 钩子内部的 test.setTimeout() |
| 全局 | 无 | testConfig.globalTimeout |
数据来源:Playwright 超时文档。
其中有两项常让人感到意外。
Action 和 navigation 默认没有超时限制。 它们仅受测试超时限制。因此,未加限定条件的 page.click() 操作将等待至剩余测试时间用尽,此时出现的错误是 测试 超时,而非点击操作超时。这就是为什么即使没人配置 30 秒的点击时长,错误信息中仍显示 30000ms 的原因。
全局超时根本没有默认值。 文档中描述其目的是防止“当一切出错时资源被过度消耗”——在 CI 中值得设置此选项,这样卡住的测试套件会直接失败,而不是无限期地占用运行器资源。
“超时 30000 毫秒”究竟是什么意思
这条具体提示信息指的是测试超时,而测试超时是一个时间限制,而非故障诊断。说明内部某个环节耗时过长,该提示仅指明了时间限制,而非具体原因。
按其作为真正原因的频率排序:
1. 定位器未匹配到任何内容。 选择器有误,或者该元素尚未出现,又或者它位于你未考虑到的 iframe 或阴影根中。Playwright 正耐心地等待着一个永远不会存在的东西。
2. 元素存在但无法交互。 被覆盖层、Cookie 提示栏或固定头部遮挡;处于禁用状态;或仍在动画中。Playwright 等待其变为可点击状态,但该状态永远不会出现。
3. 导航过程未完成。 网络请求卡住、重定向循环,或是waitUntil条件(特别是networkidle)——对于使用持久连接的页面,该条件永远无法满足。
4. 断言从未为真。 expect在轮询应用程序永远不会触发的条件。
5. 测试确实做得太多了。 这种情况确实存在,但最为罕见。
顺序很重要,因为解决方法截然不同。只有第5种情况可以通过延长超时时间来解决。在其他四种情况下,延长超时时间只会导致等待更久,最终仍会遇到同样的失败。
可操作性正是点击延迟的原因
理解这一点就能消除大部分困惑,因为它解释了在这三十秒内 Playwright 究竟在做什么。
可操作性文档 指出,Playwright“在执行操作前会对元素进行一系列可操作性检查,以确保这些操作能按预期运行”,并且它“会自动等待所有相关检查通过,之后才执行请求的操作”。 当检查未能及时通过时,“操作将因TimeoutError而失败。”
所需的检查因操作而异,这种差异具有诊断意义:
| 操作 | 所需检查 |
|---|---|
click, dblclick, check, uncheck, tap, setChecked | 可见、稳定、接收事件、已启用 |
hover, dragTo | 可见、稳定、接收事件 |
fill, clear | 可见、已启用 |
selectOption | 可见、已启用 |
screenshot, selectText | 可见 |
scrollIntoViewIfNeeded | 稳定 |
blur, focus, press, pressSequentially, dispatchEvent, setInputFiles | 无 |
从这张表格中可以立即得出两点结论。
当对同一元素执行 fill 操作时,click 却超时,这表明该元素处于“稳定”状态或“正在接收事件”——即元素正在移动,或者有其他元素覆盖在其上方。动画和叠加层通常是导致此现象的常见原因。
**未进行检查的操作既是“后门”,也是“警告信号”。**如果 locator.click() 超时,但 dispatchEvent('click') 却能正常运行,说明你并没有真正解决问题——你只是绕过了原本能告诉你“真实用户也无法点击该元素”的检查机制。有时这种情况可以接受,但通常这意味着存在一个真实的覆盖层问题,而你的测试只是停止了对其的检测。
在正确的位置修改每个超时设置
配置分为多个层次,若将其放置在错误的层次上,会导致结果混乱。
全局配置,位于 playwright.config.ts
:
export default defineConfig({
timeout: 60_000,
globalTimeout: 60 * 60 * 1000,
expect: { timeout: 10_000 },
use: {
actionTimeout: 15_000,
navigationTimeout: 30_000,
},
});
请注意各配置的所在位置。timeout
和 globalTimeout
属于顶级配置;expect.timeout
位于 expect
之下;actionTimeout
和 navigationTimeout
位于 use
之下,因为它们是测试 选项,而非运行器配置。 若将它们放置在错误的层级,系统会静默忽略。
按测试:
test('slow one', async ({ page }) => {
test.setTimeout(120_000);
// ...
});
**test.slow()
** 将默认超时时间增加三倍——对于已知确实耗时较长的测试,这是一个不错的默认值,无需随意设定数值。
针对每个断言:
await expect(page.getByRole('status')).toHaveText('Done', { timeout: 30_000 });
针对每个操作:
await page.getByRole('button', { name: 'Export' }).click({ timeout: 15_000 });
**在 beforeAll
和 afterAll
** 中(它们各自拥有 30 秒的执行配额),请在钩子内部调用 test.setTimeout()
。
对于耗时的测试 fixture,请在 test.extend()
中为其单独设置超时,而不是让每个使用它的测试都受到影响:
export const test = base.extend<{ seeded: void }>({
seeded: [async ({}, use) => {
await seedDatabase();
await use();
}, { timeout: 60_000 }],
});
一般原则:设定能解决问题且范围最小的超时。为了适应一个耗时的测试而提高全局测试超时,会导致其他所有测试失败得更慢,这会在持续集成(CI)中消耗实际时间。
哪些因素会计入测试超时时间
这一点常被误解,它解释了为何有些测试在“还没做任何事情”时就超时了。
文档中明确指出:“测试函数、测试环境设置以及beforeEach钩子所花费的时间都计入测试超时时间。”
因此,一个执行登录、数据初始化和页面导航的 beforeEach 测试,会消耗与测试主体相同的 30 秒时间。一个看似在第一行就超时的测试,可能在准备阶段已经耗费了 28 秒。
默认情况下,测试 fixture 会共享测试超时时间,这其实是同一陷阱的另一种表现形式——一个耗时的 fixture 会消耗所有依赖它的测试的超时配额。应为耗时的 fixture 设置独立的超时时间,而不是导致所有测试都触发超时。
清理操作是分离的:测试函数完成后,测试环境清理和 afterEach 钩子会获得各自的时限配额,因此缓慢的清理操作不会消耗测试的时间。
这对调试的实际意义在于:当测试超时,应检查整个链条——包括测试环境、beforeEach 以及测试主体——而不仅仅是错误指出的那一行代码。
升级前先排查
一套能在几分钟内解决大多数超时问题的流程。
配合跟踪查看器运行。 这是最有价值的工具,但往往被低估:
npx playwright test --trace on
npx playwright show-trace trace.zip
跟踪记录会显示每个操作、其持续时间、操作前后的 DOM 快照以及网络活动。 如果某个定位器未匹配到任何元素,会立即显而易见;同样,覆盖在按钮上方的 Cookie 横幅也会一目了然。
若想观察具体发生过程,请以“带头部信息”且“放慢速度”的方式运行:
npx playwright test --headed --debug
检查定位器是否能成功解析:
console.log(await page.getByRole('button', { name: 'Save' }).count());
结果为零表示选择器存在问题,仅靠调整超时值无法解决。
检查是否为稳定性问题:使用不包含检查操作的动作作为诊断手段(而非修复方案)。如果 dispatchEvent('click')
能正常工作,而 click()
却超时,说明有其他元素遮挡或移动了该元素。
**检查是否存在 networkidle
等待。** 包含分析信标、WebSocket 或轮询的页面可能永远无法进入网络空闲状态。建议等待您真正关心的事件发生:
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
请仔细阅读完整的错误信息。 Playwright 的超时错误信息包含定位器、已解析元素的数量,以及正在等待的“可操作性”检查。最后这一细节通常能直接指出问题所在。
延长超时时间通常会加剧不稳定现象
这一点虽然违反直觉,但非常重要。
不稳定的测试是指其结果取决于时间因素的测试。延长超时时间会扩大测试通过的时间窗口,因此不稳定现象会变得更少见——相应地,更难重现、更难诊断,而且一旦失败,处理起来也会更慢。
与此同时,每次失败都会带来代价。一套包含 200 个测试的测试套件,如果超时设置为 30 秒,完全失败最多需要 100 分钟;如果设置为 120 秒,则需要 400 分钟。在持续集成(CI)环境中,这意味着真金白银的损失和漫长的等待。
真正能解决不稳定性的方法是:
等待状态,而非等待时间。 waitForTimeout 几乎总是错误的。断言你关心的条件,并让 Playwright 进行轮询。
使用“Web 优先”的断言。 expect(locator).toBeVisible() 会自动重试。expect(await locator.isVisible()).toBe(true) 仅检查一次,并在首次未通过时即判定失败——这是导致不稳定性的一个微妙但非常常见的根源。
确定性地处理覆盖层。 在测试 fixture 中直接关闭 Cookie 提示框,而不是指望它们已经消失。
在配置中尽可能禁用动画,而不是等待动画结束。
等待你所依赖的特定网络响应,而不是等待网络恢复静默状态。
**稳定数据。**依赖共享可变状态的测试会出现不稳定现象,而超时机制无法解决这一问题。
**何时应真正抛出超时异常:**当操作确实很慢且无法绕过时——例如大文件上传、生成报告需要一分钟,或是故意限速的网络配置。在这些情况下,应仅针对该测试或该断言抛出超时异常,并保持默认设置不变。
通过代理运行时的超时问题
何时会触发合法的异常,以及相应的配置。
Playwright 在 网络配置 中接受代理设置:
export default defineConfig({
use: {
proxy: {
server: 'http://proxy.example.com:9000',
username: 'user',
password: 'pass',
},
},
});
或者按上下文设置,当不同测试需要不同的出口位置时,建议采用后者:
const context = await browser.newContext({
proxy: { server: 'http://proxy.example.com:9000' },
});
这会带来三个实际影响。
住宅代理会增加真实的延迟。 流量通过真实的用户连接传出,因此每个请求多出几百毫秒是正常现象,而非故障。 一个页面发出八十次请求,延迟就会累积八十倍。针对 localhost 调优的默认设置会导致测试失败,看起来像是代理故障,但实际上只是距离造成的。
正确的应对方式是测量而非猜测:通过代理运行测试套件,查看跟踪时间,并根据观察结果(留出充足余量)设置 navigationTimeout
和 actionTimeout
。
带宽才是真正的成本,而浏览器产生的带宽消耗极其巨大。 Playwright 会预加载每张图片、每种字体、每个脚本和每段视频。在按流量计费的家庭网络中($0.79/GB),这部分费用远超其他所有开销。屏蔽不必要的资源类型是最大的节省来源:
await page.route('**/*.{png,jpg,jpeg,webp,gif,woff,woff2,mp4}', r => r.abort());
这通常能大幅削减总流量,并作为附带效果加快测试速度。
**阻断不等于超时。**如果目标服务器返回验证页面,Playwright 会在等待该验证页面上不存在的元素时发生超时——这看起来完全像超时,但实际上并非如此。 在失败时截个图,看看实际渲染的内容:
use: { screenshot: 'only-on-failure', trace: 'retain-on-failure' }
这就是我们在 为什么测试代理很重要 中描述的“静默失败”模式:请求成功了,页面渲染出来了,但渲染出来的却是错误的页面。
大家还问
Playwright 的默认超时时间是多少?
测试以及 beforeAll/afterAll 钩子的默认超时时间为 30,000 毫秒,而 expect 断言的默认超时时间为 5,000 毫秒。操作和导航超时没有默认值,仅受测试超时时间的限制,这就是为什么点击速度较慢时会报告测试的 30 秒超时,而不是其自身的限制。
如何延长单个 Playwright 测试的超时时间?
在测试内部调用 test.setTimeout(120_000),或调用 test.slow() 将默认值增加三倍。建议采用这些方法,而非提高全局超时时间——后者会导致其他所有测试的失败速度变慢。
为什么当元素位于页面上时,我的 Playwright 测试仍会超时?
通常是因为该元素无法执行操作。click 要求元素可见、稳定、能接收事件且处于启用状态——因此,被横幅遮挡或仍在动画中的元素虽然会被找到,但永远无法被点击。错误消息会指明是哪项检查处于待处理状态。
测试超时和预期超时有什么区别?
测试超时是指测试函数、测试环境设置和 beforeEach 钩子组合的总时间限制,默认值为 30 秒。预期超时是指单个 Web-first 断言的轮询时长,默认值为 5 秒。断言在 5 秒后失败属于预期超时,而非测试超时。
我应该在 Playwright 中使用 waitForTimeout 吗?
几乎不应该。固定的等待时间要么太短,导致测试结果不稳定;要么太长,导致测试套件运行缓慢——通常在不同的机器上这两种情况都会发生。建议改用基于 Web 的断言来等待条件满足,该断言会在超时前自动重试。
为什么 networkidle 始终无法解析?
因为页面一直在发送请求——分析信标、WebSocket、轮询、长连接等。networkidle 需要网络静默状态,而许多现代应用程序永远不会进入静默状态。建议改为等待您关心的特定元素或响应。
延长超时时间能修复不稳定的测试吗?
这只是掩盖了问题。测试不稳定现象会变得更少见、更难复现且失败速度更慢,而测试套件中的每次真实失败现在却需要更长时间。请解决根本原因——等待状态变化而非时间,使用重试断言,确定性地关闭覆盖层,并禁用动画。
使用代理时是否需要更长的超时时间?
通常是的,尤其是在导航和操作方面,因为住宅代理会为每次请求增加实际延迟,而一个页面发出的众多请求会使这种延迟累积。请在启用追踪功能的情况下通过代理测量延迟,并根据观察结果设置超时值,而不是预先将所有超时值都调高。
总结
错误信息显示测试耗时超过了30秒,但这只是一个时间限制,而非问题原因。测试内部有某个部分在等待一个从未触发的条件,而在五分之四的情况下,该条件要么是匹配不到任何元素的选择器,要么是某个从未进入可操作状态的元素。
因此,节省时间的处理顺序是:先阅读完整的错误信息(其中指明了待处理的可操作性检查);接着打开跟踪记录(它会显示失败瞬间的 DOM 状态);确认定位器能否解析成功;最后才考虑时间数值。 抛出超时异常仅对一种原因才是正确的修复方案——即操作确实耗时超过预算——而对于其余四种情况,这却是错误的修复方案,因为它只会换来更慢的失败。
当你确实需要触发超时时,请严格限定范围:按每个测试、每个断言、每个测试 fixture 分别设置。为了容纳单次缓慢的上传而放宽全局默认值,会导致测试套件中的每次失败都更加耗时,而持续集成(CI)时间是唯一无法挽回的资源。
