E2E テストガイド (Playwright + Electron)
spec: e2e-automation-tests(Phase 0–4 完了)/ e2e-test-sdk(SDK 化)
このドキュメントは、tests/e2e/ 配下に実装した E2E テストの 設計判断 / 主要ヘルパー / よくあるハマり をまとめたもの。 日々の実行手順は tests/e2e/README.md を参照。
新規シナリオを書くとき は、まず
tests/e2e/sdk/README.mdを読む。 SDK のscenario()/controlPanel.tab().open()等を使えば、本ドキュメントの「ハマりどころ」に意識せず 5〜10 行でシナリオが書ける。 本ドキュメントは SDK が wrap している lower layer の理屈を説明する位置付け。 SDK 化の経緯・設計判断・before/after 比較は E2E Test SDK 実装概要 を参照。
1. 何を保証しているか
| シナリオ | カバー対象 | smoke 含 |
|---|---|---|
S1 起動スモーク (startup.e2e.ts) | Electron 起動 / Control 画面描画 / エラーダイアログ不在 | ✓ |
S2 ログ取込 (log-import.e2e.ts) | seed した試合が DB と DB 閲覧 UI の両方で見える | ✓ |
S3 DB ビューア (db-viewer.e2e.ts) | 試合詳細を開いて Pick / Ban が描画される | |
S4 プロファイル切替 (profile-switch.e2e.ts) | 複数プロファイル作成 → 切替で UI 反映 | |
S5 オーバーレイ同期 (overlay-sync.e2e.ts) | Control の syncConfig → Overlay の onConfigUpdated が 1.5 秒以内に届く |
smoke (PR ごと) は約 45 秒、full (nightly) は 5 シナリオで約 1 分 20 秒。
これらは「リリース前の手動回帰チェック」の自動化版という位置付け。 従来 docs/release-checklist などで人手で確認していた起動 / DB / プロファイル / 同期の経路を CI が見るようになった。
2. アーキテクチャ
tests/e2e/
├── sdk/ # ★ ユーザー向け fluent API(新規シナリオはここ経由で書く)
│ ├── index.ts # 単一 entry — `import { scenario } from '../sdk'`
│ ├── scenario.ts # scenario(name).withFixture().run/.smoke
│ ├── pageObjects/
│ │ ├── ControlPanel.ts # tab / detailButton / myUid / profile / expectVisible
│ │ ├── Overlay.ts # page() / waitForConfigUpdate()
│ │ └── constants.ts # TAB_NAMES / STORAGE_KEYS / SELECTORS
│ ├── fixtures/
│ │ ├── MatchFixtureBuilder.ts # f.match().player().pick().ban().done()
│ │ └── schema.ts # FIXTURE_DEFAULTS / Team / MatchType
│ ├── actions/
│ │ └── syncToOverlay.ts # control 側 syncConfig → overlay listener 待ち合わせ
│ ├── templates/
│ │ └── example.e2e.ts.template
│ └── README.md # SDK API 早見表 + 典型 4 サンプル
├── helpers/ # lower layer(SDK の内部実装。直接使うのはエスケープ時のみ)
│ ├── launchApp.ts # Electron を _electron.launch + control window pick
│ ├── withTempUserData.ts # Playwright fixture: テストごとに OS tmp dir を割り当て
│ ├── seedDatabase.ts # node:sqlite で matches.db を直接作る
│ ├── dbAccess.ts # node:sqlite で matches.db を read-only で開く
│ ├── dismissOnboardingDialogs.ts # 初回起動の onboarding / wishlist を強制 close
│ └── step.ts # demo モード用のナレーション / slowMo ラッパー
├── scenarios/
│ ├── startup.e2e.ts # S1
│ ├── log-import.e2e.ts # S2
│ ├── db-viewer.e2e.ts # S3 (SDK ベース、dogfood 済)
│ ├── profile-switch.e2e.ts # S4
│ └── overlay-sync.e2e.ts # S5
└── README.md # 実行手順production コードへの侵入:
electron/main.ts… 起動時にprocess.env.ELECTRON_USER_DATAがあればapp.setPath('userData', ...)で隔離 dir に切替(PathResolver より前)electron/windows/{Control,Overlay}WindowManager.ts…ELECTRON_USE_DIST=1のとき dev でもdist/の HTML を読む(後方互換でELECTRON_PERF_MEASURE=1も同等扱い)
CI ワークフロー:
.github/workflows/e2e.yml… PR で smoke を windows-latest 実行.github/workflows/e2e-nightly.yml… 夜間 cron で full を実行
3. 主要ヘルパー
3.1 launchApp(options)
「Electron を起動して control window の Page を返す」だけが責務。 内部で:
- Windows の
taskkill /F /IM electron.exe(前回テストの leftover を掃除) _electron.launch({ executablePath, args: ['.'], env })- URL に
control.htmlを含む Page を pick(devtools://やchrome-error://を弾く) dismissOnboardingDialogs(controlPage)で初回起動 dialog を閉じるcleanup()を返す(app.close()+ leftover kill)
env として既定で ELECTRON_USE_DIST=1 を投入。userDataDir を渡すと ELECTRON_USER_DATA も設定される。
3.2 withTempUserData fixture
Playwright の test.extend で userDataDir を提供。 os.tmpdir() 配下に wsm-e2e-XXXX を mkdtemp で作り、テスト終了時に rm -rf。 tmpdir() 外を削除しないガードと、Windows の EBUSY バックオフ retry 入り。
3.3 seedFixtureDatabase(userDataDir, matches?)
この PR の中核。「ログを開いて parse して save」の 3 段階を全部 skip し、 matches.db に固定行を直接 insert する。
実装の要点:
node:sqliteを使用(Node 22+ 標準)。better-sqlite3の native module は Electron バージョンに紐付くため Node test runner では rebuild が必要になり詰むので避けた。- production schema (
matches/players/bans/picks/log_files) を再現 - default fixture は 2v2 Rank 1 試合、自分 UID
1046101022が Win で MVP(MatchListTab.isReflectable2v2Matchの "2v2" 含む条件を満たす)
import { seedFixtureDatabase } from '../helpers/seedDatabase';
seedFixtureDatabase(userDataDir);開発時にも使える(scripts/dev/seed-fixture-db.ts):
npx tsx scripts/dev/seed-fixture-db.ts ./tmp/dev-userdata
$env:ELECTRON_USER_DATA = "$PWD/tmp/dev-userdata"; yarn dev:electron→ 毎回ログを取り込まずに固定 fixture が載った状態で起動できる。
3.4 step(page, label, fn)
CI 既定では何も挟まないが、demo モード(E2E_NARRATE=1 + E2E_SLOW_MS=1000)のときに:
- 画面上部に黄色いバナー
▶ <ラベル>を inject - stdout に
[step] <ラベル>を出力 fn()完了後にE2E_SLOW_MSだけ wait
ユーザーが「いま何が動いている?」を目で追える。詳細は §6。
3.5 dismissOnboardingDialogs(page)
初回起動の 2 つの dialog(onboarding guide / support wishlist)を強制 close する。
localStorageに「見た / 表示しない」フラグを書き込む<div id="root">がaria-hiddenになっている /MuiDialog-rootが残っている間 Esc を最大 30 回連打
これがないと dialog 配下で root が aria-hidden 化し、getByRole('tab') が拾えなくなる。
3.6 dbAccess.ts
openTestDB(userDataDir) / countMatches / getMatchById 等。これも node:sqlite ベース。 test runner と Electron で別 native binding を持たずに済むのが大きい。
4. テストの書き方
4.1 最小テンプレ(DB seed なし)
import { test, expect } from '../helpers/withTempUserData';
import { launchApp } from '../helpers/launchApp';
import { step } from '../helpers/step';
test('説明', async ({ userDataDir }) => {
const { controlPage, cleanup } = await launchApp({ userDataDir });
try {
await step(controlPage, '<何をするか>', async () => {
// ...
});
} finally {
await cleanup();
}
});4.2 DB seed が必要なテスト
import { seedFixtureDatabase } from '../helpers/seedDatabase';
test('...', async ({ userDataDir }) => {
seedFixtureDatabase(userDataDir); // 起動前に matches.db を作る
const { controlPage, cleanup } = await launchApp({ userDataDir });
// ...
});カスタム fixture を渡したい場合は seedFixtureDatabase(dir, [{...}, ...]) で配列を直接渡す。
4.3 click が intercept される時
OperationalChecklistPanel など複数の overlay が pointer event を奪うため、 通常の .click() は flake する。代わりに dispatchEvent('click') を使う:
await page.getByRole('tab').filter({ hasText: /^DB$/ }).first().dispatchEvent('click');副作用(mousedown → mouseup → click)が必要なケースは evaluate で MouseEvent を順次 dispatch する。
4.4 narrow viewport の tab label
ControlPanel は useMediaQuery('(max-width: 1100px)') で isNarrowScreen を判定し、 タブ label を 'DB閲覧' → 'DB'、'ログ解析' → 'ログ' 等に省略する。 Electron の既定 window 1200×800 は実描画 1100px 未満になるケースがあり narrow に倒れるので、 locator は filter({ hasText: /^DB$|DB.*閲覧/ }) のように両対応させる。
4.5 プロファイル選択
ProfileManager は localStorage[syncKey] を読んで初期選択する。 combobox を click → option を click のフローは intercept されやすいので、 localStorage に直接 id を書いて location.reload() する pattern を使う:
await page.evaluate((id) => {
localStorage.setItem('logAnalysis_selectedProfileId', String(id));
localStorage.setItem('logAnalysis_selectedProfileIds', JSON.stringify([id]));
location.reload();
}, profileId);5. 実行モード一覧
| script | 用途 | env |
|---|---|---|
yarn test:e2e | 全シナリオ通常実行 | – |
yarn test:e2e:smoke | smoke project(S1+S2)。CI/PR 既定 | – |
yarn test:e2e:headed | GUI 表示で実行 | – |
yarn test:e2e:demo | 目視確認モード: 黄色バナー + 1 秒間隔 | E2E_NARRATE=1 E2E_SLOW_MS=1000 --workers=1 |
yarn test:e2e:debug | Playwright Inspector で手動ステップ実行 | PWDEBUG=1 --workers=1 |
CI 設定(playwright.config.ts):
retries: isCI ? 2 : 0(CI で稀な flake を吸収、local では即検出)trace: on-first-retry(初回 retry 時のみ trace を取って artifact に出す)E2E_DEMO=1のときはtrace/video/screenshot全部をonにする(手元 demo 録画用)
6. demo モードの使いどころ
ユーザー(人間)が「ブラウザ自動化が単一ウィンドウで何をしているか目で追えない」となったときに:
yarn test:e2e:demo db-viewer # 1 シナリオだけ
yarn test:e2e:demo # 全シナリオを 1 秒間隔で挙動:
- Electron 起動(既に表示される)
- 各
step()の入口で画面上部に 黄色バナー を出す(▶ DB 閲覧タブへ移動等) - ステップ完了後に 1 秒 wait(人間が目で追える速度)
- stdout にも
[step] ...を併記
リリース前の最後の確認や、新シナリオを書いたときの「期待通りに動いてる?」の目視確認に使う。 CI と完全に同じ test code が走るため、demo で目視 OK = CI でも OK。
7. よくあるハマり(実装時の lesson)
7.1 better-sqlite3 native module mismatch
開発時に Electron 経由で実行した直後に test runner で開くと、片方が NODE_MODULE_VERSION mismatch で落ちる。 tests/e2e/helpers/{seedDatabase,dbAccess}.ts は node:sqlite を使うことで完全回避。 production の electron/database/*.ts は better-sqlite3 のままで OK(Electron でしか使われないため)。
7.2 deferred boot の dialog ブロック
initDatabase() が throw すると electron/main.ts の catch で dialog.showMessageBox が開き、 ユーザー操作なしには deferred boot が進まない(feature IPC が登録されないままロックする)。 yarn rebuild:db を忘れて Electron バージョンを上げると即この症状になる。 今は test 用に hang 検出経路はないので、Electron 再起動 / 再 build / rebuild:db を試す。
7.3 ELECTRON_USE_DIST を忘れると chrome-error://
dev で起動した Electron は既定で http://localhost:3000/control.html を取りに行き、Vite が無いと chrome-error 画面になる。 test では launchApp が ELECTRON_USE_DIST=1 を自動投入するため dist/control.html が読まれる。 新しく test runner 以外の経路で起動する場合は明示的に env を渡すこと。
7.4 「ログファイル読込」を本物で踏むと脆い
過去の実装では「dialog を mock して open-log-file IPC で読み込ませる」方式を採用していたが、
- production の
useLogAnalysisStateのanalysisMode既定値が'realtime'でStaticAnalysisPanelが render されない open-log-fileIPC が file content を返さず{ success, path }だった production bug- ログ format の細部 (UID 形式 /
Rank_2v2文字列 / 2v2 filter) に縛られる
これらに振り回されたため、fixture DB seed 方式に切替えた(spec Phase 4.1)。 production bug は同 PR で electron/ipc/FileSystemHandlers.ts の修正として残してある。
7.5 __dirname が ESM で未定義
package.json が "type": "module" のため、test ファイルで __dirname は使えない。 fileURLToPath(import.meta.url) 経由で resolve すること。
7.6 OverlayWindow の chrome-error 残骸
dev 起動時の Overlay は http://localhost:3000/overlay.html を取りに行って失敗する。 ControlWindowManager と同じ ELECTRON_USE_DIST 分岐が OverlayWindowManager にも必要(spec Phase 0.3 / 4.5 で対応済)。
7.7 タブクリックが intercept される
OperationalChecklistPanel の「すべて完了にする」ボタンや、.control-panel 自体が ヒットテストを奪うことがある。force: true よりも dispatchEvent('click') の方が安定。
7.8 narrow viewport で tab label 短縮
§4.4 参照。/^DB$|DB.*閲覧/ のような両対応 regex を最初から使う。
8. 関連ファイル
| File | Role |
|---|---|
playwright.config.ts | smoke / full project 定義、retries / trace / video |
package.json | test:e2e* scripts |
vitest.config.ts | tests/e2e/** を unit から exclude |
tsconfig.json | tests/ を typecheck 対象に追加 |
electron/main.ts | ELECTRON_USER_DATA override |
electron/windows/{Control,Overlay}WindowManager.ts | ELECTRON_USE_DIST 分岐 |
electron/ipc/FileSystemHandlers.ts | open-log-file の content 同梱(bug fix) |
.github/workflows/e2e.yml | PR smoke |
.github/workflows/e2e-nightly.yml | nightly full |
scripts/dev/seed-fixture-db.ts | dev 用 seed CLI |
.spec-workflow/specs/e2e-automation-tests/ | spec / tasks / Implementation Logs |
9. 拡張時のチェックリスト
新しいシナリオを追加する場合:
- [ ]
tests/e2e/scenarios/<name>.e2e.tsを作成(命名は*.e2e.ts) - [ ]
import { test, expect } from '../helpers/withTempUserData'でuserDataDirを受け取る - [ ]
launchApp({ userDataDir })で Electron を起動し、必ずtry / finallyでcleanup()を呼ぶ - [ ] DB を使うなら
seedFixtureDatabase(userDataDir)を起動前に呼ぶ - [ ] role / accessible name /
data-testidベースの locator のみ使う(class 名や生 DOM 文字列に依存しない) - [ ] click が flake するなら
dispatchEvent('click')に切替 - [ ] 主要ステップを
step(page, label, fn)で wrap(demo モードで目視できるように) - [ ] smoke に含めるかは費用対効果で判断(PR 実行時間 5 分以内が目安)— 含めるなら
playwright.config.tsのsmokeproject のtestMatchを更新 - [ ]
yarn test:e2e:demo <scenario名>で目視 OK を確認してから push