この記事は「ドキュメントに書いてあること」と「実際に測った結果」の差を整理した検証ログです。Playwright でブラウザログインを自動化している人向け。
Playwright でログイン済みセッションを使い回す方法は2系統あります。
-
storageState: Cookie / LocalStorage を JSON に書き出して注入する -
launchPersistentContext: ブラウザプロファイルをディレクトリごと永続化する
公式 API リファレンスでは launchPersistentContext のオプションにも storageState があるので、「両方渡せば引っ越しできるはず」——と思って実装したら、ログイン画面にリダイレクトされました。
原因を測ったら、想像より単純な話でした。
検証環境: Windows 11 / Playwright 1.55 /
channel: 'chrome'(Chrome 140) / 対象サービス: Qiita
測り方: 同じ storageState を3通りで注入する
Qiita にログイン済みのプロファイルから context.storageState() で JSON を保存し、以下の3パターンで https://qiita.com/settings/notifications(ログイン必須ページ)を開きました。
| # | 方法 | 意図 |
|---|---|---|
| A | browser.newContext({ storageState }) |
普通の context に注入 |
| B | launchPersistentContext(新規dir, { storageState }) |
空のプロファイルに注入 |
| C | launchPersistentContext(ログイン時のdir) |
プロファイル丸ごと再利用 |
結果: B だけログインできない
| 方法 | 結果 |
|---|---|
A: newContext + storageState |
✅ 設定画面が開いた |
B: persistentContext(新規dir) + storageState |
❌ /login にリダイレクト |
C: persistentContext(元dir)
|
✅ 設定画面が開いた |
B が失敗するのは「Cookie の有効期限が切れた」系ではありません。保存した JSON の Cookie はすべて有効期限内で、A では同じファイルがちゃんと効いています。
原因: Cookie が1つも注入されていなかった
persistent context を起動して context.cookies() を見ると、こうなります。
const ctx = await chromium.launchPersistentContext('.fresh-profile', {
channel: 'chrome',
storageState: '.qiita-session.json', // Cookie 8件入り
});
console.log(await ctx.cookies('https://qiita.com'));
// → [] ← 空。storageState がサイレントに無視される
渡したはずの Cookie がゼロ件。 エラーも警告も出ず、ただ注入されません。
newContext 側は同じファイルで全件注入されます。つまり挙動はこうです。
| API | storageState |
|---|---|
browser.newContext({ storageState }) |
注入される |
launchPersistentContext(dir, { storageState }) |
注入されない |
型定義上は storageState を受け取れるので、書けてしまうのが罠です。channel: 'chrome'(実 Chrome)で再現を確認しています。bundled Chromium では挙動が違う可能性はあります。
正しい構成: プロファイル dir を「鍵」として運用する
persistent context を使うなら、storageState に頼らずログインしたディレクトリ自体を再利用するのが正解です。
// 初回: headed で起動して人間がログイン
const ctx = await chromium.launchPersistentContext('.session-qiita', {
channel: 'chrome',
headless: false,
});
// 2回目〜: 同じ dir を指定するだけでログイン済み
const ctx2 = await chromium.launchPersistentContext('.session-qiita', {
channel: 'chrome',
headless: true, // OK
});
サービス × アカウント単位でディレクトリを分けるのが運用のコツです。
.session-note-ryuhat/ ← note アカウントA
.session-qiita-ryuhat/ ← Qiita アカウントA
.session-twitter-ryuhat2/ ← X アカウントB
storageState が有効な場面
newContext 系にだけ使う、と割り切ると整理しやすいです。
- ✅
browser.newContext({ storageState })— テスト・スクレイピングの使い捨てセッション - ✅ CI で secrets から注入して認証済みテストを回す
- ❌
launchPersistentContextへの注入 — 効かない - ❌ persistent dir と storageState の併用 — storageState 側が無視される
まとめ
-
launchPersistentContextにstorageStateを渡しても Cookie は注入されない(1件も) - ログインが必要な persistent context では、ログインしたプロファイル dir をそのまま再利用する
- ドキュメントに載っている引数でも、対象 API では無視されることがある——疑ったら
context.cookies()で数えるのが一番早い
自動投稿・スクレイピング・E2E でセッション管理を組むときの参考になれば幸いです。

