<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Testing Best Practices · 仁才德</title>
    <link>https://jared.lynskey.co.nz/zh-cn/tags/testing-best-practices/</link>
    <description>关于软件工程与研发团队领导力的短文——绩效评估、招聘与团队扩张、CI/CD、DevOps 监控、智能体 AI。写于首尔。</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <copyright>© 2026 仁才德</copyright>
    <lastBuildDate>Thu, 22 Oct 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/zh-cn/tags/testing-best-practices/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Playwright 报「Test timeout of 30000ms exceeded.」：先找到永远等不完的那一步</title>
      <link>https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-10-22-playwright-timeout-30000ms/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-10-22-playwright-timeout-30000ms/</guid>
      <pubDate>Thu, 22 Oct 2026 00:00:00 +1300</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>Playwright</category>
      <category>UI Testing</category>
      <category>Testing Best Practices</category>
      <category>Debugging</category>
      <description>Playwright 的「Test timeout of 30000ms exceeded.」到底在计哪段时间、按我排查的顺序常见原因有哪些、怎么找到卡住的那一步，以及什么时候调大超时反而是错的。附上我自己测试套件里的一次真实失败：五个测试，一个改了名的测试 ID。</description>
      <content:encoded><![CDATA[<p><code>Test timeout of 30000ms exceeded.</code> 的意思是：某个 Playwright 测试用完了它的 30 秒时限。30 秒是 Playwright Test 的默认值，这段时间要同时覆盖测试主体、<code>beforeEach</code> 钩子和 fixture 的准备。报错点名的是测试而不是某一步，是因为点击、输入和页面跳转默认都没有自己的超时。它们会一直等下去，测试超时只是最先响的那个闹钟。</p>
<p>所以这个错误很少真的是「太慢」。多半是某一步在等一件永远不会发生的事。按我排查的顺序，原因和对应的修法如下：</p>
<ol>
<li>定位器（locator）什么都没匹配到，于是 <code>click()</code> 或 <code>fill()</code> 一直等到测试时间用完。常见原因是测试 ID 改了、文字改了，或者有个对话框挡在前面。改定位器。</li>
<li>页面跳转或 <code>waitForURL()</code> 在等一个永远不会出现的页面，因为重定向、登录页或首次启动页把应用带去了别处。改你等待的条件。</li>
<li>准备工作确实慢：第一次请求才开始编译的开发服务器、从缩容到零状态被唤醒的后端、每个 <code>beforeEach</code> 里都走一遍 UI 登录。改环境，或者只准备一次。</li>
<li>测试本来就长。这时，也只有这时，才用 <code>test.slow()</code> 或 <code>test.setTimeout()</code> 给这个测试加时间。</li>
</ol>
<p>不管是哪种，第一件事都是找到时间用完那一刻还在执行的那一步。第二件事是设置 <code>actionTimeout</code>，让下一次失败直接告诉你是哪一步。</p>

<h2 class="relative group">这 30 秒里都有什么
    <div id="这-30-秒里都有什么" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%bf%99-30-%e7%a7%92%e9%87%8c%e9%83%bd%e6%9c%89%e4%bb%80%e4%b9%88" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>测试超时是一份时限，由 fixture 准备、<code>beforeEach</code> 钩子和测试函数共用。测试结束后，<code>afterEach</code> 钩子和 fixture 清理（teardown）另外再拿一份同样长的时限。<code>beforeAll</code> 和 <code>afterAll</code> 也拿同样的值，但不和任何测试共用，失败时也有自己的报错。</p>
<p>在这份时限里，只有断言有自己的计时。<code>await expect(locator).toBeVisible()</code> 默认 5 秒后放弃，并准确告诉你它在找什么。动作和页面跳转则不然：<code>actionTimeout</code> 和 <code>navigationTimeout</code> 默认不设上限。这个错误之所以这么含糊，原因全在这里。去点一个永远不会出现的元素，并不会失败，它只会老老实实地等，直到别的东西把测试结束掉。</p>
<p>下面是所有能让一次运行结束的超时，以及各自打印的内容。这些报错原文取自 Playwright 1.58 和 1.63，也就是我项目里最旧和最新的两个版本。</p>
<table>
	<thead>
			<tr>
					<th>超时</th>
					<th>默认值</th>
					<th>在哪里设置</th>
					<th>打印的内容</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>测试（主体、<code>beforeEach</code>、fixture 准备）</td>
					<td>30,000 ms</td>
					<td>配置里的 <code>timeout</code>、<code>--timeout</code>、<code>test.setTimeout()</code>、<code>test.slow()</code></td>
					<td><code>Test timeout of 30000ms exceeded.</code></td>
			</tr>
			<tr>
					<td><code>beforeAll</code> / <code>afterAll</code></td>
					<td>与测试超时相同</td>
					<td>在钩子里调用 <code>test.setTimeout()</code></td>
					<td><code>&quot;beforeAll&quot; hook timeout of 30000ms exceeded.</code></td>
			</tr>
			<tr>
					<td>断言（<code>expect</code>）</td>
					<td>5,000 ms</td>
					<td>配置里的 <code>expect: { timeout }</code>，或单个断言上的 <code>{ timeout }</code></td>
					<td><code>expect(locator).toBeVisible() failed</code> … <code>Timeout: 5000ms</code></td>
			</tr>
			<tr>
					<td>动作（<code>click</code>、<code>fill</code>）</td>
					<td>无</td>
					<td><code>use: { actionTimeout }</code>，或单个动作上的 <code>{ timeout }</code></td>
					<td><code>TimeoutError: locator.click: Timeout 10000ms exceeded.</code></td>
			</tr>
			<tr>
					<td>页面跳转（<code>goto</code>、<code>waitForURL</code>）</td>
					<td>无</td>
					<td><code>use: { navigationTimeout }</code>，或单次调用上的 <code>{ timeout }</code></td>
					<td><code>TimeoutError: page.waitForURL: Timeout 15000ms exceeded.</code></td>
			</tr>
			<tr>
					<td>整次运行</td>
					<td>无</td>
					<td>配置里的 <code>globalTimeout</code></td>
					<td><code>Timed out waiting 3600s for the test suite to run</code></td>
			</tr>
			<tr>
					<td><code>webServer</code> 启动</td>
					<td>60,000 ms</td>
					<td><code>webServer: { timeout }</code></td>
					<td><code>Timed out waiting 60000ms from config.webServer.</code></td>
			</tr>
	</tbody>
</table>
<p>动作、页面跳转和整次运行这三行没有默认值可打印，所以里面的数字就是你自己设的值。</p>

<h2 class="relative group">五个测试和一个改了名的测试 ID
    <div id="五个测试和一个改了名的测试-id" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%ba%94%e4%b8%aa%e6%b5%8b%e8%af%95%e5%92%8c%e4%b8%80%e4%b8%aa%e6%94%b9%e4%ba%86%e5%90%8d%e7%9a%84%e6%b5%8b%e8%af%95-id" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>今年 2 月，我做的估价应用的测试套件在一次运行里有五个测试失败，而且失败方式一模一样。这套测试用 Playwright 驱动应用的 Web 版（由开发服务器提供），只开一个 worker，连的是刚灌好种子数据的本地后端。下面是五个里的第一个，稍作删减：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">  1) [default] › tests/templates.spec.ts:50:7 › Templates › should edit a template ───
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    Test timeout of 30000ms exceeded.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    Error: locator.click: Target page, context or browser has been closed
</span></span><span class="line"><span class="cl">    Call log:
</span></span><span class="line"><span class="cl">      - waiting for locator(&#39;[data-testid=&#34;template-item&#34;]&#39;).filter({ hasText: &#39;E2E Edit Template&#39; }).locator(&#39;[data-testid=&#34;edit-template&#34;]&#39;)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      57 |       .locator(&#39;[data-testid=&#34;template-item&#34;]&#39;)
</span></span><span class="line"><span class="cl">      58 |       .filter({ hasText: &#34;E2E Edit Template&#34; });
</span></span><span class="line"><span class="cl">    &gt; 59 |     await item.locator(&#39;[data-testid=&#34;edit-template&#34;]&#39;).click();
</span></span><span class="line"><span class="cl">         |                                                         ^
</span></span><span class="line"><span class="cl">      60 |     await page.waitForTimeout(500);</span></span></code></pre></div></div>
<p>单看 <code>Target page, context or browser has been closed</code> 这一行，像是浏览器崩了。其实不是。这是页面在脚下被关掉时，动作给出的报错：30 秒到了，Playwright 开始收拾这个测试，那个还在等待的点击就报告说它的页面没了。如果用的是内置的 <code>page</code> fixture，这一行会再说一遍超时，写成 <code>Error: locator.click: Test timeout of 30000ms exceeded.</code>。这些测试是在 <code>beforeAll</code> 里自己开的浏览器上下文，所以点击拿到的是通用版本的报错。</p>
<p>真正有用的是调用日志（call log）。点击在等 <code>[data-testid=&quot;template-item&quot;]</code>。十个小时前，我把列表行组件的测试 ID 从 <code>template-item</code> 改成了 <code>template-item-${name}</code>，好让原生应用的端到端测试能按名字挑出某一行。用 <code>=</code> 的属性选择器要求整个值完全相等，于是 Web 测试要找的元素在页面上已经不存在了；又没有动作超时，这个点击注定要一直等到测试超时。五个失败各 30 秒，就是两分半钟，而整次运行一共才 3.8 分钟。</p>
<p>我第一时间加的是重试。不到半小时，配置里就多了 <code>retries: process.env.CI ? 2 : 1</code>，旁边的注释至今还写着 &ldquo;for flaky browser crashes&rdquo;，意思是用来应付偶发的浏览器崩溃。下一次运行，同样那五个里有四个连同重试一起又挂了，耗时从 3.8 分钟涨到 6.4 分钟。这几个 describe 块跑在串行（serial）模式下：一个失败，组里剩下的全部跳过；重试则从组里第一个测试重新开始。所以每个坏掉的点击都把 30 秒等了两遍。每次都以同样方式失败的测试，重试救不回来，只会让你付两遍代价。</p>
<p>修法是改成前缀匹配：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">item</span> <span class="o">=</span> <span class="nx">page</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">&#39;[data-testid^=&#34;template-item-&#34;]&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">filter</span><span class="p">({</span> <span class="nx">hasText</span><span class="o">:</span> <span class="s2">&#34;E2E Edit Template&#34;</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">item</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">&#39;[data-testid=&#34;edit-template&#34;]&#39;</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span></span></span></code></pre></div></div>
<p>往大里说，测试 ID 也是一种契约。我有两套测试都依赖这一个 ID，而彼此都不知道对方的存在。Playwright 的最佳实践指南建议优先用 <code>getByRole()</code> 这类基于用户可见属性的定位器，而不是我这种 CSS 选择器，为的就是让测试扛得住那些用户根本察觉不到的 DOM 改动。</p>

<h2 class="relative group">找到卡住的那一步
    <div id="找到卡住的那一步" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%89%be%e5%88%b0%e5%8d%a1%e4%bd%8f%e7%9a%84%e9%82%a3%e4%b8%80%e6%ad%a5" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>先看调用日志。它属于时间用完时还在执行的那一步；如果 <code>waiting for locator(...)</code> 后面什么都没有，说明定位器从头到尾都没匹配上任何东西。</p>
<p>还不够的话，就打开 trace。我的两个应用测试套件，一个是估价应用的，一个是我做的一个集市兼社区应用的，用的都是 <code>trace: &quot;on-first-retry&quot;</code>。它只在第一次重试时录制，所以 <code>retries</code> 至少要设成 1。不开重试的话用 <code>&quot;retain-on-failure&quot;</code>；临时跑一次就加 <code>--trace on</code>。然后直接打开 trace，或者从 HTML 报告里打开：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">npx playwright show-trace test-results/&lt;test-folder&gt;/trace.zip
</span></span><span class="line"><span class="cl">npx playwright show-report</span></span></code></pre></div></div>
<p>没执行完的那个动作，就是列表末尾附近带着报错的那一项。选中它看快照：那就是 Playwright 等待时页面的样子，你可以在里面找你期待的那个元素。换成我这个例子，那一行就好好地在那儿，只是带着一个测试没在找的 ID。</p>
<p>较新的版本还会在失败截图旁边写一个 <code>error-context.md</code>，里面是失败那一刻页面的文本快照。元素找不到的情况，往往光看它就够了。</p>
<p>有个坑：<code>--debug</code> 打开 Inspector 的同时，还会设置 <code>--timeout=0</code>，外加 <code>--max-failures=1</code>、<code>--headed</code> 和 <code>--workers=1</code>。在 <code>--debug</code> 下能通过的测试，对这 30 秒什么也说明不了。</p>

<h2 class="relative group">给每个动作设个上限
    <div id="给每个动作设个上限" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%bb%99%e6%af%8f%e4%b8%aa%e5%8a%a8%e4%bd%9c%e8%ae%be%e4%b8%aa%e4%b8%8a%e9%99%90" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>我的配置文件里没有一个设了动作超时，所以一个找不到的元素才能五次各耗掉 30 秒。在 <code>use</code> 里加两行就能改过来：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@playwright/test&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">timeout</span>: <span class="kt">30_000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="o">:</span> <span class="p">{</span> <span class="nx">timeout</span>: <span class="kt">5_000</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nx">use</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">actionTimeout</span>: <span class="kt">10_000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">navigationTimeout</span>: <span class="kt">15_000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">trace</span><span class="o">:</span> <span class="s2">&#34;retain-on-failure&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p>同样那个坏掉的定位器，就会在测试还剩时间的时候这样失败：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">    TimeoutError: locator.click: Timeout 10000ms exceeded.
</span></span><span class="line"><span class="cl">    Call log:
</span></span><span class="line"><span class="cl">      - waiting for locator(&#39;[data-testid=&#34;template-item&#34;]&#39;).filter({ hasText: &#39;E2E Edit Template&#39; }).locator(&#39;[data-testid=&#34;edit-template&#34;]&#39;)</span></span></code></pre></div></div>
<p>哪一步、哪个动作、等了多久，全写在上面。我觉得这两个值都应该明显小于测试超时，这样某一步卡住时，不会把整份时限耗光，再躲到那条含糊的报错后面。2 月那次，问题从来不在 30 秒。那个点击身上只有这一个计时器，所以响的就是它。</p>

<h2 class="relative group">只给长测试加时间，别给卡住的测试加
    <div id="只给长测试加时间别给卡住的测试加" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8f%aa%e7%bb%99%e9%95%bf%e6%b5%8b%e8%af%95%e5%8a%a0%e6%97%b6%e9%97%b4%e5%88%ab%e7%bb%99%e5%8d%a1%e4%bd%8f%e7%9a%84%e6%b5%8b%e8%af%95%e5%8a%a0" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>测试确实在做耗时的工作时，加超时是对的；某一步在等一件不会发生的事时，加超时就是错的，数字再大也只是让失败来得更晚。到底是哪种，通常看一眼 trace 就清楚了：动作一个接一个都完成了，只是数量多，那是长测试；有一个动作直到最后都停在 <code>waiting for</code>，那是卡住的测试。</p>
<p>对长测试，时间要加在真正长的地方，而不是整个套件。集市应用的套件里有一个冒烟测试，会一边截图一边把聊天界面走一遍，我给了它三分钟：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="nx">test</span><span class="p">(</span><span class="s2">&#34;chat: open, send a message, go back&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">test</span><span class="p">.</span><span class="nx">setTimeout</span><span class="p">(</span><span class="mi">180</span><span class="nx">_000</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p>在 <code>describe</code> 里调用 <code>test.setTimeout()</code>，会作用于组内所有测试，估价应用套件里的成本计算（job-cost）测试就是这样拿到一分钟的。<code>test.slow()</code> 会把当前的超时乘以三，不管它原来是多少，也可以带条件。在 <code>beforeEach</code> 里用 <code>testInfo.setTimeout()</code>，会延长所有经过这个钩子的测试：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="nx">test</span><span class="p">(</span><span class="s2">&#34;slow in WebKit&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span><span class="p">,</span> <span class="nx">browserName</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">test</span><span class="p">.</span><span class="nx">slow</span><span class="p">(</span><span class="nx">browserName</span> <span class="o">===</span> <span class="s2">&#34;webkit&#34;</span><span class="p">,</span> <span class="s2">&#34;This page is slow in WebKit&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">test</span><span class="p">.</span><span class="nx">beforeEach</span><span class="p">(</span><span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">},</span> <span class="nx">testInfo</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">testInfo</span><span class="p">.</span><span class="nx">setTimeout</span><span class="p">(</span><span class="nx">testInfo</span><span class="p">.</span><span class="nx">timeout</span> <span class="o">+</span> <span class="mi">30</span><span class="nx">_000</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p>还有一样东西每次都在悄悄吃掉时限：固定等待。估价应用的套件里，几乎每个 spec 文件都用到了 <code>page.waitForTimeout()</code>，加起来一百多处。上面那个编辑测试在走到点击之前先睡了 3 秒，可睡多久都救不了它。Playwright 的 API 文档在这一点上说得很直白：&ldquo;Never wait for timeout in production. Tests that wait for time are inherently flaky.&rdquo; 也就是说，正式环境里永远不要按时间等待，靠时间等待的测试天生就不稳定。Web 优先断言（web-first assertion）本来就会等到需要的时候为止，一点不多等：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="c1">// before
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">&#39;[data-testid=&#34;save-template&#34;]&#39;</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForTimeout</span><span class="p">(</span><span class="mi">1500</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByText</span><span class="p">(</span><span class="s2">&#34;E2E Edited Template&#34;</span><span class="p">)).</span><span class="nx">toBeVisible</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// after
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">&#39;[data-testid=&#34;save-template&#34;]&#39;</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByText</span><span class="p">(</span><span class="s2">&#34;E2E Edited Template&#34;</span><span class="p">)).</span><span class="nx">toBeVisible</span><span class="p">();</span></span></span></code></pre></div></div>
<p>断言超时也要同样小心。两个应用套件都把 <code>expect.timeout</code> 调到了 15 秒，而且都是对着应用的开发服务器跑，而不是生产构建。这样是多了余量，但连着两个慢断言就可能把 30 秒用光，于是测试超时先触发，你又回到了那条含糊的报错。</p>

<h2 class="relative group">当计时的不是测试本身
    <div id="当计时的不是测试本身" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%bd%93%e8%ae%a1%e6%97%b6%e7%9a%84%e4%b8%8d%e6%98%af%e6%b5%8b%e8%af%95%e6%9c%ac%e8%ba%ab" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>如果报错里出现 <code>config.webServer</code>，说明还一个测试都没跑。Playwright 会先启动你的 <code>webServer</code> 命令，等它的 <code>url</code> 有响应了才开始跑测试。这段等待有单独的超时，默认 60 秒。两个应用套件都给开发服务器留了两分钟：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="nx">webServer</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">command</span><span class="o">:</span> <span class="s2">&#34;npm run web -- --port 8081&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">url</span><span class="o">:</span> <span class="s2">&#34;http://localhost:8081&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">reuseExistingServer</span>: <span class="kt">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">timeout</span>: <span class="kt">120_000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">},</span></span></span></code></pre></div></div>
<p>如果测试对着的是部署好的环境而不是本地服务器，先搞清楚它的第一个请求要多久。我终于在自己用的托管平台上量了一次，一台从缩容到零状态启动的机器，要 25 到 33 秒才能响应任何请求，还没点第一下，测试的时限就用掉了大半甚至全部。那段经历写在<a href="https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-09-09-scale-to-zero-504s/" >缩容到零，于是每次重启都是一次 30 秒的故障</a>里。</p>

<h2 class="relative group">如果报的是「No tests found」
    <div id="如果报的是no-tests-found" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%a6%82%e6%9e%9c%e6%8a%a5%e7%9a%84%e6%98%afno-tests-found" aria-label="锚点">#</a>
    </span>
    
</h2>
<p><code>No tests found</code> 听起来相关，但它不是超时：Playwright 找了一圈，没有可跑的东西。默认情况下，它会在 <code>testDir</code> 下收集匹配 <code>**/*.@(spec|test).?(c|m)[jt]s?(x)</code> 的文件，而 <code>testDir</code> 默认就是配置文件所在的目录。我的配置都指向配置文件旁边的 <code>testDir: &quot;./tests&quot;</code>，所以放在别处的 spec 根本看不见。命令行参数是拿来匹配文件路径的正则表达式，所以报错里才会附上一句 &ldquo;Make sure that arguments are regular expressions matching test files.&quot;。想在不运行任何东西的情况下，看看 Playwright 收集到了什么：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">npx playwright <span class="nb">test</span> --list</span></span></code></pre></div></div>
<p>如果你的 spec 不在这个列表里，它压根就没机会超时。另外，如果你是第一次搭 Playwright，我在<a href="https://jared.lynskey.co.nz/zh-cn/posts/2024/2024-06-09-playwright-tests/" >小团队更该做视觉测试</a>里讲过为什么建议从截图对比开始。</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-10-22-playwright-timeout-30000ms/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
