Playwright 超时报错分层排查:到底是哪一层的 timeout 触发的
超时是这套东西最容易被改错的地方。用例挂了,第一反应是把 timeout 调大,调大之后要么没变化,要么变成跑得更久然后照样挂。原因是 Playwright Test 里并不只有一个超时,仓库文档 docs/src/test-timeouts-js.md 开篇就写明它「有多个可配置的超时」,而这些超时之间的从属关系并不直观。
先说清楚一件事:test-timeouts 这一页在仓库里只有 -js 后缀的版本,也就是说它描述的是 JavaScript/TypeScript 的 test runner。其它语言绑定的超时行为要去各自的 API 文档里看,本文最后会专门提一处绑定之间的差异。
一、现象:三段报错分别来自三层
文档里给了三段错误文本,长得完全不一样,这正是分层判定的第一手依据。
用例层超时:
example.spec.ts:3:1 › basic test ===========================
Timeout of 30000ms exceeded.
断言层超时:
example.spec.ts:3:1 › basic test ===========================
Error: expect(received).toHaveText(expected)
Expected string: "my text"
Received string: ""
Call log:
- expect.toHaveText with timeout 5000ms
- waiting for "locator('button')"
整轮跑批超时:
Running 1000 tests using 10 workers
514 skipped
486 passed
Timed out waiting 3600s for the entire test run
以上三段均原样抄自 docs/src/test-timeouts-js.md,里面的数值是文档示例里的值,不是你环境里一定会出现的数字。
二、怎么确认是哪一层:四个可执行的判定动作
判定一,先看报错里有没有 Call log。 有 Call log,并且里面出现 expect.<matcher> with timeout ...ms 这样一行,说明触发的是断言超时。文档在 docs/src/test-assertions-js.md 的自定义消息一节里也给了同样结构的输出(expect.toBeVisible with timeout 5000ms),可见这一行是断言层的固定标记。
判定二,报错只有一句 Timeout of ...ms exceeded.、没有具体的匹配器信息,那是用例层。这里有个容易漏的点:文档写明用例超时把「测试函数本身、fixture 的 setup、beforeEach 钩子」的耗时算在一起。所以看到用例层超时,不要只盯着测试函数看。
判定三,报错是 Timed out waiting ...s for the entire test run,并且前面带着 skipped 的统计,那是全局超时。这一层砍的是整轮,改单个用例的配置没有任何用。
判定四,用 --debug 把用例层排除掉。 docs/src/test-cli-js.md 的参数表写明 --debug 是 PWDEBUG=1 环境变量的快捷方式,并且等价于 --timeout=0 --max-failures=1 --headed --workers=1;docs/src/debug.md 也重复说明了这一点,并列出「浏览器以 headed 方式启动」「默认超时被设为 0(即不超时)」两项。文档里给的命令是:
npx playwright test example.spec.ts:10 --debug
用例层超时被关掉之后,如果画面仍然停在同一个动作上不动,那问题就不是「时间不够」,而是这个动作在等一个永远不会满足的条件。
Windows 上,JS 侧就是上面这条 npx playwright test --debug,写法没有差别。其它语言绑定靠 PWDEBUG 环境变量,文档在 Python 页签里同时给了 PWDEBUG=1 pytest -s 和 Windows 批处理的 set PWDEBUG=1 两种写法——Windows 的 shell 不吃前缀式的环境变量赋值,这一点文档是分开列的,别照抄成一行。
要注意的是,文档在 --debug 这一处只写了上面几项默认改动,并没有说断言层的超时会被一并关掉。这一页里我们没有找到关于二者关系的进一步说明,所以不要默认 --debug 能把断言超时也一起消掉。
三、五层各自的配置项与覆盖方式
判定完之后再去改,改的位置如下。这张表只列配置项名与覆盖入口,都出自 docs/src/test-timeouts-js.md 与 docs/src/test-api/ 下对应的类文档。
| 层 | 配置文件里的字段 | 单点覆盖入口 |
|---|---|---|
| 全局(整轮) | globalTimeout | 无单测入口;命令行 --global-timeout <timeout> |
| 用例 | timeout | test.setTimeout()、test.slow()、testInfo.setTimeout();命令行 --timeout <timeout> |
| 断言 | expect: { timeout } | expect(locator).toBeVisible({ timeout })、expect.configure({ timeout }) |
| 动作 | use: { actionTimeout } | locator.click({ timeout }) |
| 导航 | use: { navigationTimeout } | page.goto('/', { timeout }) |
几条必须记住的语义:
用例层的时间是分段发放的。 文档写明:测试函数跑完之后,fixture 的 teardown 与 afterEach 共享「另一份数值相同的独立超时」;beforeAll / afterAll 用的也是同一个数值,但它们不与任何用例共享时间。也就是说这个数字不是「一个用例总共只能花这么久」。
断言层与用例层无关。 文档原话是断言超时与测试超时不相关。把 timeout 调到很大,断言层该五秒挂还是五秒挂——很多人第一次踩的就是这个坑。
动作层与导航层在 JS 侧默认是不设限的。 docs/src/test-timeouts-js.md 把它们放在「Advanced: low level timeouts」一节,表里标的是 no timeout,并且文档自述这些是 test runner 预先配好的、「你应该不需要改」,还补了一句:如果你是因为用例 flaky 才翻到这一节,多半解法在别处。这句自述值得当成一条判据用——如果你没配过 actionTimeout / navigationTimeout,也没在调用处传 timeout,那么按文档给出的默认值,这两层不该是报错的来源,排查时可以先往后放。默认值本身随版本可能变动,真要确认还得回头看你所用版本的文档。
Library 侧的优先级链是写死在 API 文档里的。 docs/src/api/class-page.md 的 Page.setDefaultNavigationTimeout 下有一条 note:它优先于 Page.setDefaultTimeout、BrowserContext.setDefaultTimeout 和 BrowserContext.setDefaultNavigationTimeout;docs/src/api/class-browsercontext.md 里反过来又写了一遍,前三者优先于 BrowserContext.setDefaultTimeout。同一处文档还列明了导航默认超时具体作用于哪些方法:Page.goBack、Page.goForward、Page.goto、Page.reload、Page.setContent、Page.waitForNavigation——class-page.md 里同一份名单还多一项 Page.waitForURL,两页的列举并不完全一致,以你所用版本的仓库内容为准。不在这份名单里的方法,改导航超时不会影响它。
配置文件的写法直接抄文档:
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
actionTimeout: 10 * 1000,
navigationTimeout: 30 * 1000,
},
});
以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。
四、改完怎么验证
只改一层,并且优先用命令行覆盖而不是直接改配置文件。 --timeout 与 --global-timeout 都在 CLI 参数表里,用它们做二分能保证配置文件干净。判据是:如果把用例层放开之后报错换了一种文本,说明你上一步的判定是对的,真正卡住的是新报出来的那一层。
npx playwright test example.spec.ts:10 --timeout 0
以上为按仓库文档中的参数语义组合的示例,未经实测,以仓库最新内容与 --help 的实际输出为准。
断言层看 Call log 里的数字。 那一行 expect.<matcher> with timeout ...ms 会跟着你配的值走,数字变了就是配置生效了,没变说明你改的位置不对——比如误把 timeout 写在了顶层而不是 expect 下面。
fixture 慢就单独给它配,别去顶大用例超时。 docs/src/test-fixtures-js.md 的 Fixture timeout 一节写明:fixture 被视为用例的一部分,它的 setup 与 teardown 耗时计入用例超时,因此慢 fixture 会把用例拖挂;对这类 fixture 可以单独设一个更大的超时,同时把整体用例超时保持在小值。写法是在 fixture 定义的第二个参数里给 { timeout: ... }。
这里有一处两份文档口径不完全一致,值得自己核一下:test-timeouts-js.md 的低层超时表把 Fixture timeout 标成 no timeout,而 test-fixtures-js.md 正文写的是 fixture 默认与用例共享超时,且每个 worker-scoped fixture 有一份等于用例超时的独立超时。两处放在一起读,正文的描述更细;具体以你所用版本的仓库内容为准。
五、什么情况说明不是这五层的问题
- 报错发生在用例开始之前,指向服务起不来。
TestConfig.webServer有自己的timeout,文档说明它是「等待进程启动并可用的时长」,属于跑批前置阶段,不归上面任何一层管。 - 报错里压根没有 timeout 字样。 断言值不匹配是立即失败,定位器写错也是立即报错,这类问题调超时只会让失败来得更慢。
- 把用例超时调大之后,耗时正好顶到新上限又挂。 这说明在等的条件永远不会满足,属于页面状态或定位器的问题,继续加时间没有意义。
--debug之下超时已经是 0,画面仍然停在同一个动作。 同上,时间已经不是约束条件了。toPass挂了。docs/src/test-assertions-js.md明确写着:toPass默认超时为 0,并且不遵循自定义的 expect 超时。所以调expect: { timeout }对它无效,要在toPass({ timeout })里单独给。这条相当反直觉,遇到过一次就不会忘。- 同一份用例在别的语言绑定里表现不同。
docs/src/api/params.md里,动作类timeout选项对 js 写的是默认 0(不超时),对 python、java、csharp 写的是默认 30000 毫秒;导航类同理。这不是配置漂移,是绑定之间本来就不同的默认值。
上面提到的默认值均为仓库当前文档里写明的默认值,随版本可能变动。该项目持续更新,涉及的配置项名、参数语义与默认值请以仓库最新内容为准。
本文依据 github.com/microsoft/playwright 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的文档与源码。我们没有对文中涉及的功能做过实测,
因此不涉及运行速度、稳定性与实际表现的任何描述。
该项目迭代频繁,文中涉及的 API 签名、配置项与默认值随版本变动,请以仓库最新内容为准。
本文不涉及浏览器版本矩阵与版本清单,相关信息请以官方发布说明为准。