zudo-tauri-wisdom
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

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 では断続的に(または常に)失敗する。

  1. ヘッドレスブラウザはキーボードショートカットの処理が異なる -- ショートカットエンジンは keydown イベントをリッスンするが、ヘッドレス Chromium は実際のブラウザと同一にディスパッチしない場合がある

  2. アプリのショートカットエンジンはフォーカスが必要 -- CI ではページがショートカット発火に適切なフォーカスコンテキストを持たない場合がある

  3. タイミングの問題 -- ショートカットエンジンは非同期に初期化されるため、エンジンの準備前にテストがキーイベントを送信する可能性がある

一方、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レーンに残します。

新しいテストの書き方

以下の判断ツリーに従う。

  1. テストはキーボードショートカット(Ctrl+K、Ctrl+, など)を使用するか?

  • はい -> @interactive タグを付ける

  • いいえ -> 次へ

  1. テストはフォーカス依存の動作(ショートカットエンジン、vim モード)に依存するか?

  • はい -> @interactive タグを付ける

  • いいえ -> CI セーフ、タグ不要

  1. 共有ユーティリティは 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 をグリーンに保ちつつ、キーボード駆動機能の徹底的なローカルテストを可能にする。

Revision History

作成更新