E2E テスト分割戦略
Tauri アプリ向けに Playwright テストを CI セーフと @interactive カテゴリに分割する方法
E2E テスト分割戦略
Tauri アプリは固有のテスト課題に直面する。WebView は OS の WebKit エンジンで動作し、アプリのキーボードショートカットエンジンは CI のヘッドレスブラウザでキーボードイベントを確実に受信しない。この記事では、ほとんどのテストを CI で実行しつつ、キーボード依存のテストをローカル実行に予約する実用的な分割戦略を文書化する。
問題
設定ダイアログを開くテストを考える。
test("opens settings dialog", async ({ page }) => {
await page.keyboard.press("Control+,");
await expect(page.getByTestId("settings-dialog")).toBeVisible();
});このテストは開発者のマシンでは完璧に動作するが、CI では断続的に(または常に)失敗する。
ヘッドレスブラウザはキーボードショートカットの処理が異なる -- ショートカットエンジンは
keydownイベントをリッスンするが、ヘッドレス Chromium は実際のブラウザと同一にディスパッチしない場合があるアプリのショートカットエンジンはフォーカスが必要 -- CI ではページがショートカット発火に適切なフォーカスコンテキストを持たない場合がある
タイミングの問題 -- ショートカットエンジンは非同期に初期化されるため、エンジンの準備前にテストがキーイベントを送信する可能性がある
一方、DOM API を直接使用するテスト(ボタンクリック、テキスト内容読み取り、表示状態のアサーション)は、ローカルと CI の両環境で確実に動作する。
2つのテストカテゴリ
CI セーフテスト
DOM を直接操作するテスト -- クリック、page.evaluate()、要素の表示/コンテンツのアサーション。キーボードショートカットに依存しない。
test.describe("Core Navigation", () => {
test("navigates to archives via sidebar click", async ({ page }) => {
await page.click('[data-testid="nav-archives"]');
await expect(page.getByTestId("archives-page")).toBeVisible();
});
});@interactive テスト
アプリのショートカットエンジンがキーボードイベントを処理する必要があるテスト。test.describe の名前に @interactive タグを付ける。
test.describe("Settings Dialog @interactive", () => {
test("opens with Ctrl+,", async ({ page }) => {
await page.keyboard.press("Control+,");
await expect(page.getByTestId("settings-dialog")).toBeVisible();
});
test("opens command palette with Ctrl+K", async ({ page }) => {
await page.keyboard.press("Control+k");
await expect(page.getByTestId("command-palette")).toBeVisible();
});
});CI の設定
CI は --grep-invert を使って @interactive テストを除外する。
# CI が実行するもの(@interactive を除外)
pnpm test:e2e --project=chromium --grep-invert="@interactive"ローカル開発ではすべてのテストを実行する。
# ローカルで全テスト実行(@interactive を含む)
pnpm test:e2e
# @interactive テストのみ実行
pnpm test:e2e --grep="@interactive"テストファイルの構成
テストはカテゴリではなく機能ごとに整理される。各 spec ファイルに CI セーフと @interactive の両方のテストブロックを含めることができる。
e2e/
command-palette.spec.ts # @interactive(キーボードショートカット)
core-navigation.spec.ts # CI セーフ(クリック)
display-scale.spec.ts # CI セーフ(DOM アサーション)
draft-shortcut-guard.spec.ts # @interactive(キーボードショートカット)
indent-guides.spec.ts # CI セーフ(ビジュアルアサーション)
list-indent-wrap.spec.ts # CI セーフ(エディタ動作)
panel-divider-drag.spec.ts # CI セーフ(マウスドラッグ)
settings-dialog.spec.ts # @interactive(Ctrl+, で開く)
split-pane.spec.ts # CI セーフ(DOM アサーション)
write-and-archive.spec.ts # CI セーフ(クリックとテキスト入力)
zoom-fix-verification.spec.ts # CI セーフ(CSS アサーション)
helpers.ts # 共有テストユーティリティ Tip
新しいテストファイルを作成する際、「このテストはキーボードショートカットでアプリコマンドをトリガーするか?」と問う。はいなら @interactive タグを付ける。クリック、page.evaluate()、DOM アサーションのみ使用するなら CI セーフであり、タグは不要。
WebKit 要件
Tauri アプリは OS の WebKit エンジンを使用する(Chromium ではない)ため、すべての Playwright テストはローカル検証で WebKit ブラウザを使用すべきである。
// playwright.config.ts
export default defineConfig({
projects: [
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
],
});Warning
Chromium ベースのテスト結果は、ユーザーが実際に見るものと異なる場合がある。Chromium で通過するテストが、WebKit が CSS、イベント、フォーカスを異なる方法で処理するため、実際のアプリでは失敗する可能性がある。常にローカルで WebKit を使って検証すること。
CI は実用的な理由で Chromium を使用する(ヘッドレスモードでより高速かつ安定)が、CI テストは WebKit 固有の動作を避けるよう明示的に設計されている。より深いブラウザ統合をテストする @interactive テストは、ローカルでのみ WebKit を使って実行する。
実際のウィンドウ動作にはネイティブWebDriverレーンを追加する
Playwright WebKitが操作するのはあくまでブラウザプロセスであり、パッケージ化されたTauriアプリケーションではありません。Tauri IPC、複数のネイティブウィンドウ、AppKitのフォーカス、OSのクローズライフサイクルに依存する動作までは証明できません。ただし、これらをすべて手動テストに残す必要はありません。macOSでは専用テストバンドルにtauri-plugin-wdio-webdriverを登録し、WebdriverIOから実際のTauri WebViewを操作できます。
このレーンは独立させ、直列で実行してください。
ドライバー依存は
native-e2eのようなオプトインCargo featureの背後に置き、通常ビルドでは登録しない。専用のTauri設定、bundle identifier、アプリケーション名、状態ディレクトリ、incognito WebView storeを使い、テストが普段使いのインストールやサインイン済みアカウントに触れないようにする。
テスト対象ウィンドウにはembedded driverのcapabilityだけを許可する。本番capability setにはドライバー権限を含めない。
操作にはW3C pointer/keyboard actionsを使う。JavaScriptでDOM eventをdispatchすると、テスト対象であるエディター、フォーカス、クリップボード、ネイティブウィンドウの経路を迂回しかねない。
embedded driverが固定portを所有する場合や、journeyが実際のアプリケーションウィンドウを作成する場合は
maxInstances: 1にする。失敗時には、検出できる全ウィンドウのスクリーンショットに加え、秘匿化済みのtitle、URL、realm identifier、frontend error marker、protocol traceを保存する。選択中のWebDriver contextが1つだけでは、マルチウィンドウの不具合を再構成しにくい。
専用fixture stateを必ず消去し、driver portが閉じるまで待つ。runnerが中断された場合も、完全一致するテストバンドルの実行ファイルだけを終了する。
# src-tauri/Cargo.toml
[features]
native-e2e = ["dep:tauri-plugin-wdio-webdriver"]
[target.'cfg(target_os = "macos")'.dependencies]
tauri-plugin-wdio-webdriver = { version = "1.3.0", optional = true }#[cfg(all(target_os = "macos", feature = "native-e2e"))]
let builder = builder.plugin(tauri_plugin_wdio_webdriver::init());// wdio.conf.ts (excerpt)
export const config = {
maxInstances: 1,
hostname: "127.0.0.1",
port: 45678,
services: [["@wdio/tauri-service", { driverProvider: "embedded" }]],
capabilities: [
{
browserName: "tauri",
"tauri:options": { application: TEST_APP_BINARY },
},
],
};このレーンには本番除外チェックも明示的に加えてください。native-e2e なしの通常Cargo dependency graphを調べ、通常の本番bundleをビルドし、そのcapabilityと生成物にdriver/plugin/test bootstrapのmarkerがないことをscanします。ネイティブテストがgreenでも、自動化surfaceを誤って出荷していれば十分ではありません。
Tip
このレーンは、実際のウィンドウ作成・再利用、ウィンドウ間event transport、native close/focus、installed WebViewに意味論が依存するinput pathという薄いnative boundaryだけに使ってください。通常のrenderingやDOM journeyは、より高速なPlaywrightレーンに残します。
新しいテストの書き方
以下の判断ツリーに従う。
テストはキーボードショートカット(Ctrl+K、Ctrl+, など)を使用するか?
はい ->
@interactiveタグを付けるいいえ -> 次へ
テストはフォーカス依存の動作(ショートカットエンジン、vim モード)に依存するか?
はい ->
@interactiveタグを付けるいいえ -> CI セーフ、タグ不要
共有ユーティリティは
helpers.tsに置く -- ページオブジェクトパターン、共通セットアップ、待機ヘルパー。
両カテゴリを含む適切に構造化されたテストファイルの例:
import { test, expect } from "@playwright/test";
// CI-safe: uses DOM clicks
test.describe("Settings Display", () => {
test("shows display scale options", async ({ page }) => {
// Open settings via DOM click (not keyboard shortcut)
await page.click('[data-testid="settings-button"]');
await expect(page.getByTestId("display-scale-options")).toBeVisible();
});
});
// Interactive: uses keyboard shortcut
test.describe("Settings Shortcut @interactive", () => {
test("Ctrl+, opens settings", async ({ page }) => {
await page.keyboard.press("Control+,");
await expect(page.getByTestId("settings-dialog")).toBeVisible();
});
});まとめ
この分割戦略は実用的である。CI で実行できるものは CI で、できないものはローカルで実行する。 @interactive タグは、特別な Playwright プラグインや設定を必要としないシンプルな grep ベースのフィルターである。CI をグリーンに保ちつつ、キーボード駆動機能の徹底的なローカルテストを可能にする。