<?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/ja/tags/testing-best-practices/</link>
    <description>ソフトウェアエンジニアリングと開発組織のリーダーシップについて。評価、採用とチームの拡大、CI/CD、DevOps モニタリング、エージェンティック AI。ソウルより。</description>
    <generator>Hugo</generator>
    <language>ja</language>
    <copyright>© 2026 仁才徳</copyright>
    <lastBuildDate>Thu, 22 Oct 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/ja/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/ja/posts/2026/2026-10-22-playwright-timeout-30000ms/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ja/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.」が実際に何を測っているのか、私が確認する順番で見たよくある原因、止まったステップの見つけ方、そしてタイムアウトを延ばすのが間違いになる場合をまとめました。自分のテストスイートで実際に起きた失敗、5つのテストと名前の変わったテストID 1つの話も添えています。</description>
      <content:encoded><![CDATA[<p><code>Test timeout of 30000ms exceeded.</code> は、Playwrightのテストが30秒の持ち時間を超えたという意味です。30秒はPlaywright Testのデフォルトで、この持ち時間にはテスト本体、<code>beforeEach</code> フック、フィクスチャのセットアップがまとめて含まれます。メッセージがステップではなくテストを名指しするのは、クリックや入力、ページ遷移にはデフォルトで専用のタイムアウトがないからです。どれだけかかっても待ち続け、テストタイムアウトはたまたま最初に切れる時計にすぎません。</p>
<p>つまり、このエラーが本当に速度の問題であることはまれです。たいていは、どこかのステップが決して起こらないことを待っています。私が確認する順に、原因と対処を並べるとこうなります。</p>
<ol>
<li>ロケーターが何にも一致せず、<code>click()</code> や <code>fill()</code> がテストの持ち時間が尽きるまで待ち続けている。よくある理由は、テストIDの変更、テキストの変更、手前に被さったダイアログです。ロケーターを直します。</li>
<li>ページ遷移や <code>waitForURL()</code> が、いつまでも来ないページを待っている。リダイレクトやログイン画面、初回起動の画面のせいで、アプリが別の場所へ行ってしまったケースです。待つ対象を直します。</li>
<li>セットアップが本当に遅い。初回リクエストでようやくコンパイルを始める開発サーバー、ゼロスケールから起き上がるバックエンド、<code>beforeEach</code> のたびにUIで行うログインなどです。環境を直すか、セットアップを1回で済ませます。</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="#30%e7%a7%92%e3%81%ab%e5%90%ab%e3%81%be%e3%82%8c%e3%82%8b%e3%82%82%e3%81%ae" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>テストタイムアウトは、フィクスチャのセットアップ、<code>beforeEach</code> フック、テスト関数が共有する1つの持ち時間です。テストが終わると、<code>afterEach</code> フックとフィクスチャのティアダウンには、同じ長さの持ち時間が別に与えられます。<code>beforeAll</code> と <code>afterAll</code> も同じ値を受け取りますが、どのテストとも時間を共有せず、失敗したときも専用のメッセージを出します。</p>
<p>この持ち時間の中で、自分の時計を持っているのはアサーションだけです。<code>await expect(locator).toBeVisible()</code> はデフォルトで5秒たつと諦め、何を探していたのかを正確に教えてくれます。アクションとページ遷移はそうではありません。<code>actionTimeout</code> と <code>navigationTimeout</code> のデフォルトは無制限です。このエラーがこれほど曖昧な理由は、すべてここにあります。決して現れない要素へのクリックは失敗しません。ほかの何かがテストを終わらせるまで、行儀よく待ち続けるだけです。</p>
<p>実行を終わらせうるタイムアウトをすべて、それぞれが出すメッセージと一緒に並べました。メッセージは、私のプロジェクトに入っている中で最も古い1.58と最も新しい1.63からそのまま写しています。</p>
<table>
	<thead>
			<tr>
					<th>タイムアウト</th>
					<th>デフォルト</th>
					<th>設定する場所</th>
					<th>出力されるメッセージ</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>テスト（本体、<code>beforeEach</code>、フィクスチャのセットアップ）</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">5つのテストと、名前の変わったテストID
    <div id="5つのテストと名前の変わったテスト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="#5%e3%81%a4%e3%81%ae%e3%83%86%e3%82%b9%e3%83%88%e3%81%a8%e5%90%8d%e5%89%8d%e3%81%ae%e5%a4%89%e3%82%8f%e3%81%a3%e3%81%9f%e3%83%86%e3%82%b9%e3%83%88id" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>2月、私が作っている見積もりアプリのテストスイートで、5つのテストが1回の実行の中でまったく同じように失敗しました。このスイートは、開発サーバーが配信するアプリのWeb版を、ワーカー1つで、シードデータを入れ直したローカルのバックエンドに対して動かしています。5つのうち最初の1つを、少し削って載せます。</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> フィクスチャを使っていれば、この行は <code>Error: locator.click: Test timeout of 30000ms exceeded.</code> のようにタイムアウトをもう一度伝えていたはずです。このテストは <code>beforeAll</code> で自前のブラウザコンテキストを開いているので、クリックには汎用のメッセージが届きました。</p>
<p>役に立つのはコールログです。クリックは <code>[data-testid=&quot;template-item&quot;]</code> を待っていました。その10時間前、私は一覧の行コンポーネントのテストIDを <code>template-item</code> から <code>template-item-${name}</code> に変えていました。ネイティブアプリのE2Eテストが、名前で特定の行を選べるようにするためです。<code>=</code> を使った属性セレクターは値全体が一致しないとマッチしないので、Webのテストはページのどこにも存在しない要素を探していたことになります。アクションタイムアウトもないので、そのクリックはテストの持ち時間が尽きるまで待つしかありませんでした。30秒の失敗が5つで2分30秒。実行全体は3.8分でした。</p>
<p>私が最初に足したのはリトライでした。30分もしないうちに設定には <code>retries: process.env.CI ? 2 : 1</code> が入り、そこに付けたコメントには今も &ldquo;for flaky browser crashes&rdquo;（ときどき起きるブラウザのクラッシュ対策）と書いてあります。次の実行では、同じ5つのうち4つがリトライも含めてまた失敗し、実行時間は3.8分から6.4分に延びました。これらのdescribeブロックはシリアルモードで動きます。1つが失敗するとグループの残りはスキップされ、リトライはグループを最初からやり直します。そのせいで、壊れたクリックはどれも30秒を2回ずつ待ちました。毎回同じ理由で失敗するテストは、リトライでは救えません。代償を2回払うだけです。</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 1つに頼るテストスイートが2つあり、どちらもお互いの存在を知りませんでした。Playwrightのベストプラクティスのガイドが、私が使ったようなCSSセレクターよりも <code>getByRole()</code> のようなユーザーから見える属性に基づくロケーターを勧めているのも、ユーザーが気づきもしない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%ad%a2%e3%81%be%e3%81%a3%e3%81%9f%e3%82%b9%e3%83%86%e3%83%83%e3%83%97%e3%82%92%e8%a6%8b%e3%81%a4%e3%81%91%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>まずコールログを読みます。これは時間切れの瞬間にまだ動いていたステップのもので、<code>waiting for locator(...)</code> の後に何も続いていなければ、ロケーターが最後まで何にも一致しなかったということです。</p>
<p>それで足りなければトレースを開きます。私のアプリのスイート2つ、見積もりアプリのものと、私が作っているマーケットプレイス兼コミュニティアプリのものは、どちらも <code>trace: &quot;on-first-retry&quot;</code> を使っています。最初のリトライのときだけ記録する設定なので、<code>retries</code> が1以上でないと記録されません。リトライを使わないなら <code>&quot;retain-on-failure&quot;</code>、1回だけ試すなら <code>--trace on</code> です。あとはトレースを直接開くか、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>落とし穴が1つあります。<code>--debug</code> はInspectorを開くと同時に、<code>--max-failures=1</code>、<code>--headed</code>、<code>--workers=1</code> と一緒に <code>--timeout=0</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="#%e3%81%99%e3%81%b9%e3%81%a6%e3%81%ae%e3%82%a2%e3%82%af%e3%82%b7%e3%83%a7%e3%83%b3%e3%81%ab%e4%b8%8a%e9%99%90%e3%82%92" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>私の設定ファイルには、アクションタイムアウトを設定しているものが1つもありません。だから見つからない要素1つのために、30秒を5回も使ったわけです。<code>use</code> に2行足せば変わります。</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つの値はテストタイムアウトより十分小さくしておくのがいいと思います。そうすれば、止まったステップ1つが持ち時間を使い切り、曖昧なメッセージの陰に隠れることはありません。2月の問題は30秒ではありませんでした。あのクリックにかかっていた時計がそれ1つだけだったので、それが鳴ったというだけです。</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="#%e3%82%bf%e3%82%a4%e3%83%a0%e3%82%a2%e3%82%a6%e3%83%88%e3%82%92%e5%bb%b6%e3%81%b0%e3%81%99%e3%81%ae%e3%81%af%e9%95%b7%e3%81%84%e3%83%86%e3%82%b9%e3%83%88%e3%81%a0%e3%81%91%e6%ad%a2%e3%81%be%e3%81%a3%e3%81%9f%e3%83%86%e3%82%b9%e3%83%88%e3%81%a7%e3%81%af%e3%81%aa%e3%81%8f" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>タイムアウトを延ばすのが正解なのは、テストが実際に時間のかかる仕事をしているときです。あるステップが起こらないことを待っているときには不正解です。数字を大きくしても、失敗が遅れて届くだけですから。どちらなのかは、たいていトレースを見ればはっきりします。アクションが1つずつ終わっていて単に数が多いなら長いテスト、1つのアクションが最後まで <code>waiting for</code> のまま止まっているなら止まったテストです。</p>
<p>長いテストなら、スイート全体ではなく、長さがある場所に時間を足します。マーケットプレイスアプリのスイートには、スクリーンショットを撮りながらチャット画面を一通り回るスモークテストが1つあり、このテストには3分を与えています。</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）のテストが1分をもらっているのは、この方法です。<code>test.slow()</code> は今のタイムアウトが何であれ3倍にし、条件を渡すこともできます。<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>持ち時間を毎回静かに削っていくものが、もう1つあります。固定のスリープです。見積もりアプリのスイートには、ほぼすべてのspecファイルにまたがって、<code>page.waitForTimeout()</code> の呼び出しが100個以上あります。上の編集テストはクリックにたどり着く前に3秒眠っていますが、どれだけ眠っても助からなかったでしょう。PlaywrightのAPIドキュメントも、ここははっきりしています。&ldquo;Never wait for timeout in production. Tests that wait for time are inherently flaky.&quot;（本番では決して時間で待たないこと。時間を待つテストは本質的に不安定です。）Webファーストアサーションは、もともと必要なだけ待ち、それ以上は待ちません。</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>アサーションのタイムアウトにも同じ注意が要ります。2つのアプリのスイートはどちらも <code>expect.timeout</code> を15秒に上げていて、どちらも本番ビルドではなくアプリの開発サーバーを相手に動いています。余裕はできますが、遅いアサーションが2つ続けば、その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="#%e3%83%86%e3%82%b9%e3%83%88%e3%81%ae%e6%99%82%e8%a8%88%e3%81%a7%e3%81%af%e3%81%aa%e3%81%84%e3%81%a8%e3%81%8d" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>メッセージに <code>config.webServer</code> が出てくるなら、まだテストは1つも動いていません。Playwrightは <code>webServer</code> のコマンドを起動し、その <code>url</code> が応答するまで待ってからテストを始めます。この待ち時間には別のタイムアウトがあり、デフォルトは60秒です。2つのアプリのスイートは、どちらも開発サーバーに2分を与えています。</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/ja/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="#no-tests-found%e3%81%a8%e5%87%ba%e3%81%9f%e3%82%89" 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.&rdquo; という一文が付きます。何も実行せずに、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/ja/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>
