mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-17 22:33:06 +08:00
fix(ja-JP): address review feedback and add 5 missing skills
- Fix Chinese term '提炼' → '蒸留' in commands/rules-distill.md - Fix '重大な所見' (Critical→重大) in agents/opensource-sanitizer.md - Fix non-transactional persistence in swift-actor-persistence/SKILL.md: add rollback logic so cache stays consistent if disk write fails - Clarify anti-pattern wording: 'configurable file URL' → 'externally mutable after init' to remove internal inconsistency (P2) - Fix broken relative link in videodb/reference/api-reference.md: ../../../../../skills/... → ./editor.md - Add 5 previously missing SKILL.md translations: skill-scout, tinystruct-patterns, ui-to-vue, vite-patterns, windows-desktop-e2e
This commit is contained in:
@@ -158,7 +158,7 @@ git log -p | grep -iE '(password|secret|api.?key|token)' | head -20
|
||||
| 設定の完全性 | PASS/WARN | {count}件の所見 |
|
||||
| git履歴 | PASS/FAIL | {count}件の所見 |
|
||||
|
||||
## Critical所見(リリース前に修正必須)
|
||||
## 重大な所見(リリース前に修正必須)
|
||||
|
||||
1. **[SECRETS]** `src/config.py:42` — ハードコードされたデータベースパスワード: `DB_P...`(切り捨て)
|
||||
2. **[INTERNAL]** `docker-compose.yml:15` — 内部ドメインを参照
|
||||
@@ -174,9 +174,9 @@ git log -p | grep -iE '(password|secret|api.?key|token)' | head -20
|
||||
|
||||
## 推奨事項
|
||||
|
||||
{FAILの場合: "{N}件のCritical所見を修正してサニタイザーを再実行してください。"}
|
||||
{FAILの場合: "{N}件の重大な所見を修正してサニタイザーを再実行してください。"}
|
||||
{PASSの場合: "プロジェクトはオープンソースリリースの準備完了。パッケージャーに進んでください。"}
|
||||
{WARNINGSの場合: "プロジェクトはCriticalチェックに合格。リリース前に{N}件の警告をレビューしてください。"}
|
||||
{WARNINGSの場合: "プロジェクトは重大チェックに合格。リリース前に{N}件の警告をレビューしてください。"}
|
||||
```
|
||||
|
||||
## 例
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
description: "スキルをスキャンして横断的な原則を抽出し、ルールとして提炼する"
|
||||
description: "スキルをスキャンして横断的な原則を抽出し、ルールとして蒸留する"
|
||||
---
|
||||
|
||||
# /rules-distill — スキルから原則をルールとして提炼する
|
||||
# /rules-distill — スキルから原則をルールとして蒸留する
|
||||
|
||||
インストール済みのスキルをスキャンし、横断的な原則を抽出して、ルールとして提炼します。
|
||||
インストール済みのスキルをスキャンし、横断的な原則を抽出して、ルールとして蒸留します。
|
||||
|
||||
## フロー
|
||||
|
||||
|
||||
131
docs/ja-JP/skills/skill-scout/SKILL.md
Normal file
131
docs/ja-JP/skills/skill-scout/SKILL.md
Normal file
@@ -0,0 +1,131 @@
|
||||
---
|
||||
name: skill-scout
|
||||
description: Search existing local, marketplace, GitHub, and web skill sources before creating a new skill. Use when the user wants to create, build, fork, or find a skill for a workflow.
|
||||
origin: community
|
||||
---
|
||||
|
||||
# スキルスカウト
|
||||
|
||||
新しいスキルを作成する前にこのスキルを使用してください。目的は、既存のコミュニティやマーケットプレイスの成果を重複して作成することを避けながら、外部のものを採用する前にきちんと審査することです。
|
||||
|
||||
出典: `redminwang` によるコミュニティの古いPR #1232 から再利用。
|
||||
|
||||
## 使用するタイミング
|
||||
|
||||
- ユーザーが「スキルを作成する」「スキルをビルドする」「スキルを作る」「新しいスキル」と言ったとき。
|
||||
- ユーザーが「Xのスキルはありますか?」または「Yを実行するスキルは存在しますか?」と尋ねたとき。
|
||||
- ユーザーがワークフローを説明し、新しいスキルの作成を提案しようとしているとき。
|
||||
- ユーザーが既存のスキルをフォークまたは拡張したいとき。
|
||||
|
||||
ユーザーが検索をスキップして最初から作成するよう明示的に指示した場合は、それを確認してリクエストされた作成ワークフローを進めてください。
|
||||
|
||||
## 動作の仕組み
|
||||
|
||||
### ステップ1 - 意図の把握
|
||||
|
||||
以下を抽出します:
|
||||
|
||||
- スキルが実行すべきタスク。
|
||||
- スキルを使用するためのトリガー条件。
|
||||
- 関連するドメイン、ツール、フレームワーク、またはデータソース。
|
||||
- 3〜5個の検索キーワードと有用な同義語。
|
||||
|
||||
### ステップ2 - ローカルソースを検索する
|
||||
|
||||
まずインストール済みおよびマーケットプレイスのスキル名を検索します。ローカルソースはすでにユーザーの環境に含まれているため優先されます。
|
||||
|
||||
```bash
|
||||
find ~/.claude/skills -maxdepth 2 -name SKILL.md 2>/dev/null | grep -iE "keyword|synonym"
|
||||
find ~/.claude/plugins/marketplaces -path '*/skills/*/SKILL.md' 2>/dev/null | grep -iE "keyword|synonym"
|
||||
```
|
||||
|
||||
次にフロントマターの説明を検索します:
|
||||
|
||||
```bash
|
||||
grep -RilE "keyword|synonym" ~/.claude/skills ~/.claude/plugins/marketplaces 2>/dev/null
|
||||
```
|
||||
|
||||
### ステップ3 - リモートソースを検索する
|
||||
|
||||
利用可能なGitHubおよびWebの検索ツールを使用します。簡潔なクエリを優先します:
|
||||
|
||||
```bash
|
||||
gh search repos "claude code skill keyword" --limit 10 --sort stars
|
||||
gh search code "name: keyword" --filename SKILL.md --limit 10
|
||||
```
|
||||
|
||||
Web検索では、最大3つのターゲットクエリを使用します(例):
|
||||
|
||||
```text
|
||||
"claude code skill" keyword
|
||||
"SKILL.md" keyword
|
||||
"everything-claude-code" keyword
|
||||
```
|
||||
|
||||
### ステップ4 - 外部マッチを審査する
|
||||
|
||||
採用またはフォークのために外部スキルを推奨する前に:
|
||||
|
||||
- `SKILL.md` のフロントマターと手順を読む。
|
||||
- 予期しないシェルコマンド、ファイル書き込み、ネットワーク呼び出し、クレデンシャル処理、またはパッケージインストールがないか確認する。
|
||||
- リポジトリがメンテナンスされているかどうかを確認する。
|
||||
- マーケットプレイスのオリジナルを直接編集するのではなく、新しいローカルブランチにコピーしてdiffを確認することを優先する。
|
||||
|
||||
### ステップ5 - 結果をランク付けする
|
||||
|
||||
候補を以下の順でランク付けします:
|
||||
|
||||
1. スキル名での完全なキーワードマッチ。
|
||||
2. 説明でのキーワードまたは同義語マッチ。
|
||||
3. ローカルにインストール済みまたはマーケットプレイスのソース。
|
||||
4. 最近のアクティビティがあるメンテナンス済みのGitHubソース。
|
||||
5. Web上の言及のみ。
|
||||
|
||||
最終リストは10件に制限します。
|
||||
|
||||
### ステップ6 - 判断オプションを提示する
|
||||
|
||||
ユーザーに短いテーブルを提示します:
|
||||
|
||||
| オプション | 意味 |
|
||||
| --- | --- |
|
||||
| 既存を使用 | マッチするスキルをそのまま呼び出すかインストールする。 |
|
||||
| フォークまたは拡張 | 最も近いスキルをコピーして修正する。 |
|
||||
| 新規作成 | 近いマッチが存在しないことを確認した後、新しいスキルをビルドする。 |
|
||||
|
||||
ユーザーがそのパスを選択した後、または検索で近いマッチが見つからなかった場合にのみ、新しいスキルを作成します。
|
||||
|
||||
## 例
|
||||
|
||||
### 結果テーブル
|
||||
|
||||
```markdown
|
||||
| # | スキル | ソース | マッチする理由 | ギャップ |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | article-writing | ローカル ECC | 記事とガイドの草稿作成 | リリースノートに特化していない |
|
||||
| 2 | content-engine | ローカル ECC | マルチフォーマットコンテンツワークフロー | 必要以上に重い |
|
||||
| 3 | blog-writer | GitHub | 最近のコミットがあるブログ執筆スキル | セキュリティレビューが必要 |
|
||||
```
|
||||
|
||||
### ユーザー向けサマリー
|
||||
|
||||
```markdown
|
||||
2つの近いローカルマッチと1つの外部候補が見つかりました。最も近いのは
|
||||
`article-writing` です。草稿作成と修正をカバーしていますが、
|
||||
お求めのリリースノートチェックリストは含まれていません。そのまま使用するか、
|
||||
リリースノートバリアントにフォークするか、新しいスキルを作成するかを選択できます。
|
||||
```
|
||||
|
||||
## アンチパターン
|
||||
|
||||
- 検索が適切な場合に、新しいスキルの作成に直接飛びつかないこと。
|
||||
- 読まずに外部スキルをインストールしないこと。
|
||||
- 弱いマッチの長いランク付けされていないリストを提示しないこと。
|
||||
- Web上の言及のみを信頼できるソースとして扱わないこと。
|
||||
- インストール済みのマーケットプレイスオリジナルをその場で編集しないこと。
|
||||
|
||||
## 関連
|
||||
|
||||
- `search-first` - ビルドする前に検索する一般的なワークフロー。
|
||||
- `skill-stocktake` - インストール済みスキルの健全性、重複、ギャップの監査。
|
||||
- `agent-sort` - 既存のエージェントとスキルの分類と整理。
|
||||
@@ -35,13 +35,27 @@ public actor LocalRepository<T: Codable & Identifiable> where T.ID == String {
|
||||
// MARK: - Public API
|
||||
|
||||
public func save(_ item: T) throws {
|
||||
let previous = cache[item.id]
|
||||
cache[item.id] = item
|
||||
try persistToFile()
|
||||
do {
|
||||
try persistToFile()
|
||||
} catch {
|
||||
// ディスク書き込み失敗時はキャッシュをロールバックして整合性を維持
|
||||
cache[item.id] = previous
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public func delete(_ id: String) throws {
|
||||
let previous = cache[id]
|
||||
cache[id] = nil
|
||||
try persistToFile()
|
||||
do {
|
||||
try persistToFile()
|
||||
} catch {
|
||||
// ディスク書き込み失敗時はキャッシュをロールバックして整合性を維持
|
||||
cache[id] = previous
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public func find(by id: String) -> T? {
|
||||
@@ -131,7 +145,7 @@ final class QuestionListViewModel {
|
||||
|
||||
* Swiftの新しい並行処理コードでActorの代わりに `DispatchQueue` または `NSLock` を使用する
|
||||
* 内部のキャッシュ辞書を外部の呼び出し元に公開する
|
||||
* 検証なしでファイルURLを設定可能にする
|
||||
* 初期化後にファイルURLを外部から変更可能にする(初期化時のみ設定を許可すること)
|
||||
* すべてのActor メソッド呼び出しが `await` であることを忘れる——呼び出し元は非同期コンテキストを処理する必要がある
|
||||
* Actor の分離をバイパスするために `nonisolated` を使用する(本末転倒)
|
||||
|
||||
|
||||
104
docs/ja-JP/skills/tinystruct-patterns/SKILL.md
Normal file
104
docs/ja-JP/skills/tinystruct-patterns/SKILL.md
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
name: tinystruct-patterns
|
||||
description: Use when developing application modules or microservices with the tinystruct Java framework. Covers routing, context management, JSON handling with Builder, and CLI/HTTP dual-mode patterns.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# tinystruct 開発パターン
|
||||
|
||||
**tinystruct** Java フレームワークを使用してモジュールをビルドするためのアーキテクチャと実装パターン。CLIとHTTPが等しく扱われる軽量なシステムです。
|
||||
|
||||
## 使用するタイミング
|
||||
|
||||
- `AbstractApplication` を拡張して新しい `Application` モジュールを作成するとき。
|
||||
- `@Action` を使用してルートとコマンドラインアクションを定義するとき。
|
||||
- `Context` を通じてリクエストごとの状態を処理するとき。
|
||||
- ネイティブの `Builder` コンポーネントを使用してJSONシリアライゼーションを行うとき。
|
||||
- `application.properties` でデータベース接続またはシステム設定を構成するとき。
|
||||
- `ApplicationManager.init()` を通じて標準的な `bin/dispatcher` エントリポイントを生成または再生成するとき。
|
||||
- ルーティング競合(Action)またはCLI引数解析のデバッグを行うとき。
|
||||
|
||||
## 動作の仕組み
|
||||
|
||||
tinystruct フレームワークは、`@Action` でアノテーションされたメソッドをターミナルとWeb環境の両方でルーティング可能なエンドポイントとして扱います。アプリケーションは `AbstractApplication` を拡張することで作成され、`init()` などのコアライフサイクルフックとリクエスト `Context` へのアクセスが提供されます。
|
||||
|
||||
ルーティングは `ActionRegistry` によって処理され、パスセグメントをメソッド引数に自動的にマッピングして依存関係を注入します。データのみのサービスでは、ゼロ依存のフットプリントを維持するために、JSONシリアライゼーションにネイティブの `Builder` コンポーネントを使用すべきです。フレームワークには `ApplicationManager` のユーティリティも含まれており、`bin/dispatcher` スクリプトを生成することでプロジェクトの実行環境をブートストラップします。
|
||||
|
||||
## 例
|
||||
|
||||
### 基本アプリケーション(MyService)
|
||||
```java
|
||||
public class MyService extends AbstractApplication {
|
||||
@Override
|
||||
public void init() {
|
||||
this.setTemplateRequired(false); // データ/APIアプリの .view 参照を無効化
|
||||
}
|
||||
|
||||
@Override public String version() { return "1.0.0"; }
|
||||
|
||||
@Action("greet")
|
||||
public String greet() {
|
||||
return "Hello from tinystruct!";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### パラメータ付きルーティング(getUser)
|
||||
```java
|
||||
// Web: /api/user/123 または CLI: "bin/dispatcher api/user/123" を処理
|
||||
@Action("api/user/(\\d+)")
|
||||
public String getUser(int userId) {
|
||||
return "User ID: " + userId;
|
||||
}
|
||||
```
|
||||
|
||||
### HTTPモード分岐(login)
|
||||
```java
|
||||
@Action(value = "login", mode = Mode.HTTP_POST)
|
||||
public boolean doLogin() {
|
||||
// ログイン処理
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### ネイティブJSONデータ処理(getData)
|
||||
```java
|
||||
@Action("api/data")
|
||||
public Builder getData() throws ApplicationException {
|
||||
Builder builder = new Builder();
|
||||
builder.put("status", "success");
|
||||
Builder nested = new Builder();
|
||||
nested.put("id", 1);
|
||||
nested.put("name", "James");
|
||||
builder.put("data", nested);
|
||||
return builder;
|
||||
}
|
||||
```
|
||||
|
||||
## 設定
|
||||
|
||||
設定は `src/main/resources/application.properties` で管理されます。
|
||||
|
||||
## テストパターン
|
||||
|
||||
JUnit 5 を使用して、アクションが `ActionRegistry` に登録されていることを検証することでアクションをテストします。
|
||||
|
||||
## レッドフラグとアンチパターン
|
||||
|
||||
| 症状 | 正しいパターン |
|
||||
|---|---|
|
||||
| `com.google.gson` または `com.fasterxml.jackson` のインポート | `org.tinystruct.data.component.Builder` を使用する。 |
|
||||
| `.view` ファイルの `FileNotFoundException` | APIのみのアプリでは `init()` 内で `setTemplateRequired(false)` を呼び出す。 |
|
||||
| `private` メソッドへの `@Action` アノテーション | アクションはフレームワークに登録されるために `public` である必要がある。 |
|
||||
| アプリ内での `main(String[] args)` のハードコーディング | すべてのモジュールのエントリポイントとして `bin/dispatcher` を使用する。 |
|
||||
| 手動での `ActionRegistry` 登録 | 自動検出のために `@Action` アノテーションを優先する。 |
|
||||
|
||||
## テクニカルリファレンス
|
||||
|
||||
詳細なガイドは `references/` ディレクトリにあります:
|
||||
|
||||
- [アーキテクチャと設定](references/architecture.md) — 抽象化、パッケージマップ、プロパティ
|
||||
- [ルーティングと@Action](references/routing.md) — アノテーションの詳細、モード、パラメータ
|
||||
- [データ処理](references/data-handling.md) — JSONのためのネイティブ `Builder` の使用
|
||||
- [システムと使用方法](references/system-usage.md) — Context、セッション、イベント、CLI使用方法
|
||||
- [テストパターン](references/testing.md) — JUnit 5 統合と ActionRegistry テスト
|
||||
86
docs/ja-JP/skills/ui-to-vue/SKILL.md
Normal file
86
docs/ja-JP/skills/ui-to-vue/SKILL.md
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
name: ui-to-vue
|
||||
description: Use when the user has UI screenshots or design exports that need batch conversion into Vue 3 components, especially with Vant, Element Plus, or Ant Design Vue.
|
||||
origin: community
|
||||
---
|
||||
|
||||
# UI To Vue
|
||||
|
||||
UIデザインのスクリーンショットをVue 3 Composition APIコンポーネントコードに一括変換します。
|
||||
|
||||
## 使用するタイミング
|
||||
|
||||
- ユーザーがデザインスクリーンショットまたはデザインエクスポート画像のディレクトリを提供するとき。
|
||||
- ターゲットアプリケーションがVue 3のとき。
|
||||
- ユーザーがページコンポーネント、共有コンポーネント、ルーター配線の最初の変換を希望するとき。
|
||||
- ユーザーがVant、Element Plus、またはAnt Design Vueをコンポーネントライブラリとして指定するとき。
|
||||
|
||||
## 使用しないタイミング
|
||||
|
||||
- ユーザーがスクリーンショット1枚のみで、特定のコンポーネントを希望するとき。
|
||||
- ターゲットプロジェクトがVueでないとき。
|
||||
- デザインが詳細なインタラクションロジック、データフロー、またはアクセシビリティレビューを必要とするとき。
|
||||
- スクリーンショットに外部モデルAPIに送信できないプライベートな顧客データが含まれるとき。
|
||||
|
||||
## 入力
|
||||
|
||||
モジュールとページ状態でスクリーンショットをグループ化したディレクトリを入力として使用します。
|
||||
|
||||
サポートされている切り出し画像ディレクトリ名:`assets`、`icons`、`sprites`、`cut`、`images`、`cut-images`。
|
||||
|
||||
## 変換モデル
|
||||
|
||||
- ページグループ化:リスト、詳細、フォーム、ローディング、または空の状態を表す関連スクリーンショットを1つのページコンポーネントにまとめる。
|
||||
- UIライブラリマッピング:可能な限りネイティブのビジュアル要素をVant、Element Plus、またはAnt Design Vueコンポーネントにマッピングする。
|
||||
- 切り出し画像の優先順位:ページレベルのアセットを優先し、次にモジュールレベル、最後にグローバル共有アセット。
|
||||
- コンポーネント抽出:繰り返し使われるUIリージョンが2回以上現れる場合は共有コンポーネントに抽出する。
|
||||
|
||||
## CLI使用方法
|
||||
|
||||
グローバルバイナリに依存せず、ドキュメントに記載されたコマンドが機能するように `npx` でコンバーターを実行します:
|
||||
|
||||
```bash
|
||||
export DASHSCOPE_API_KEY=your_key
|
||||
npx ui-to-vue-converter@1.0.2 --input ./screenshots --ui vant --output ./src
|
||||
```
|
||||
|
||||
## オプション
|
||||
|
||||
| オプション | 説明 | デフォルト |
|
||||
| --- | --- | --- |
|
||||
| `--input` | デザイン画像ディレクトリ | `./screenshots` |
|
||||
| `--ui` | UIライブラリ:`vant`、`element-plus`、または `antd-vue` | `vant` |
|
||||
| `--output` | 出力ディレクトリ | `./src` |
|
||||
| `--config` | 設定ファイルのパス | `./.ui-to-vue.config.json` |
|
||||
|
||||
## セキュリティとプライバシー
|
||||
|
||||
- デザインスクリーンショットを外部モデルAPIに送信される可能性があるソース素材として扱う。
|
||||
- 許可なくプライベートな顧客デザインでこのフローを実行しないこと。
|
||||
- 再現可能なワークフローでは `@latest` の代わりにコンバーターのバージョンを固定すること。
|
||||
- コミット前に生成されたVueコードをレビューすること。
|
||||
- `.ui-to-vue.config.json`、APIキー、生成されたシークレット、または顧客スクリーンショットをコミットしないこと。
|
||||
|
||||
## 出力レビューチェックリスト
|
||||
|
||||
- [ ] ページコンポーネントが `views/` または選択した出力ディレクトリの下に生成された。
|
||||
- [ ] 繰り返しのUIリージョンが再利用が明確な場合のみ `components/` に抽出された。
|
||||
- [ ] ルーター出力がターゲットプロジェクトのルータースタイルと互換性がある。
|
||||
- [ ] 生成されたコンポーネントが要求したUIライブラリを一貫して使用している。
|
||||
- [ ] 生成されたCSSのユニットがデザインのベースラインと一致している。
|
||||
- [ ] コードがプロジェクトのフォーマッター、リンター、型チェッカー、ビルドをパスする。
|
||||
- [ ] プレースホルダーのコピー、モックデータ、生成されたアセットをコミット前にレビューした。
|
||||
|
||||
## トラブルシューティング
|
||||
|
||||
| 問題 | 確認事項 |
|
||||
| --- | --- |
|
||||
| `401` または認証エラー | コマンドを実行するシェルで `DASHSCOPE_API_KEY` が設定されていることを確認する。 |
|
||||
| `command not found: ui-to-vue` | `npx ui-to-vue-converter@1.0.2` の形式を使用するか、パッケージをグローバルインストールする。 |
|
||||
| 切り出し画像が無視される | アセットディレクトリ名がサポートされており、対応するページまたはモジュールの下にネストされていることを確認する。 |
|
||||
| コンポーネントが要求されたUIライブラリを無視する | 明示的な `--ui` 値で再実行して、生成されたインポートを確認する。 |
|
||||
| 生成されたレイアウトの寸法がおかしい | スクリーンショットのエクスポート幅がターゲットライブラリのベースラインと一致していることを確認する。 |
|
||||
|
||||
## リファレンス
|
||||
|
||||
- npmパッケージ:`ui-to-vue-converter`
|
||||
@@ -362,7 +362,7 @@ asset = CaptionAsset(
|
||||
)
|
||||
```
|
||||
|
||||
完全なCaptionAssetの使用方法については、[editor.md](../../../../../skills/videodb/reference/editor.md#caption-overlays) のエディターAPIを参照。
|
||||
完全なCaptionAssetの使用方法については、[editor.md](./editor.md#caption-overlays) のエディターAPIを参照。
|
||||
|
||||
## ビデオ検索パラメータ
|
||||
|
||||
|
||||
449
docs/ja-JP/skills/vite-patterns/SKILL.md
Normal file
449
docs/ja-JP/skills/vite-patterns/SKILL.md
Normal file
@@ -0,0 +1,449 @@
|
||||
---
|
||||
name: vite-patterns
|
||||
description: Vite build tool patterns including config, plugins, HMR, env variables, proxy setup, SSR, library mode, dependency pre-bundling, and build optimization. Activate when working with vite.config.ts, Vite plugins, or Vite-based projects.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Vite パターン
|
||||
|
||||
Vite 8+ プロジェクトのビルドツールおよびデベロップメントサーバーのパターン。設定、環境変数、プロキシ設定、ライブラリモード、依存関係の事前バンドル、一般的な本番環境の落とし穴をカバー。
|
||||
|
||||
## 使用するタイミング
|
||||
|
||||
- `vite.config.ts` または `vite.config.js` を設定するとき
|
||||
- 環境変数または `.env` ファイルを設定するとき
|
||||
- APIバックエンド用のデベロップメントサーバープロキシを設定するとき
|
||||
- ビルド出力(チャンク、ミニファイ、アセット)を最適化するとき
|
||||
- `build.lib` でライブラリを公開するとき
|
||||
- 依存関係の事前バンドルまたはCJS/ESM相互運用のトラブルシューティングをするとき
|
||||
- HMR、デベロップメントサーバー、またはビルドエラーをデバッグするとき
|
||||
- Viteプラグインの選択または順序付けをするとき
|
||||
|
||||
## 動作の仕組み
|
||||
|
||||
- **デベロップメントモード**はソースファイルをネイティブESMとして提供します(バンドルなし)。変換はモジュールリクエストごとにオンデマンドで行われるため、コールドスタートが速くHMRが精確です。
|
||||
- **ビルドモード**はRolldown(v7+)またはRollup(v5〜v6)を使用して、ツリーシェイキング、コード分割、Oxcベースのミニファイでアプリを本番用にバンドルします。
|
||||
- **依存関係の事前バンドル**はesbuildを通じてCJS/UMD依存関係をESMに一度変換し、結果を `node_modules/.vite` にキャッシュします。これにより後続の起動では処理をスキップできます。
|
||||
- **プラグイン**はデベロップメントとビルドにわたって統一されたインターフェースを共有します。同じプラグインオブジェクトが、デベロップメントサーバーのオンデマンド変換と本番パイプラインの両方で機能します。
|
||||
- **環境変数**はビルド時に静的にインライン化されます。`VITE_` プレフィックス付きの変数はバンドル内のパブリック定数になり、プレフィックスなしのものはクライアントコードから見えません。
|
||||
|
||||
## 例
|
||||
|
||||
### 設定の構造
|
||||
|
||||
#### 基本設定
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: { '@': new URL('./src', import.meta.url).pathname },
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
#### 条件付き設定
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig(({ command, mode }) => {
|
||||
const env = loadEnv(mode, process.cwd()) // VITE_ プレフィックスのみ(安全)
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
server: command === 'serve' ? { port: 3000 } : undefined,
|
||||
define: {
|
||||
__API_URL__: JSON.stringify(env.VITE_API_URL),
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### 主要な設定オプション
|
||||
|
||||
| キー | デフォルト | 説明 |
|
||||
|-----|---------|-------------|
|
||||
| `root` | `'.'` | プロジェクトルート(`index.html` の場所) |
|
||||
| `base` | `'/'` | デプロイされたアセットのパブリックベースパス |
|
||||
| `envPrefix` | `'VITE_'` | クライアントに公開する環境変数のプレフィックス |
|
||||
| `build.outDir` | `'dist'` | 出力ディレクトリ |
|
||||
| `build.minify` | `'oxc'` | ミニファイアー(`'oxc'`、`'terser'`、または `false`) |
|
||||
| `build.sourcemap` | `false` | `true`、`'inline'`、または `'hidden'` |
|
||||
|
||||
### プラグイン
|
||||
|
||||
#### 必須プラグイン
|
||||
|
||||
ほとんどのプラグインのニーズは、少数のよく管理されたパッケージでカバーできます。独自のプラグインを作成する前にこれらを検討してください。
|
||||
|
||||
| プラグイン | 目的 | 使用タイミング |
|
||||
|--------|---------|-------------|
|
||||
| `@vitejs/plugin-react-swc` | SWC経由のReact HMR + Fast Refresh | Reactアプリのデフォルト(Babelバリアントより高速) |
|
||||
| `@vitejs/plugin-react` | Babel経由のReact HMR + Fast Refresh | Babelプラグインが必要な場合のみ(emotion、MobXデコレーター) |
|
||||
| `@vitejs/plugin-vue` | Vue 3 SFCサポート | Vueアプリ |
|
||||
| `vite-plugin-checker` | ワーカースレッドでHMRオーバーレイ付きの `tsc` + ESLintを実行 | **TypeScriptアプリ全般** — Viteは `vite build` 中に型チェックを行いません |
|
||||
| `vite-tsconfig-paths` | `tsconfig.json` の `paths` エイリアスを尊重 | `tsconfig.json` にエイリアスが既にある場合 |
|
||||
| `vite-plugin-dts` | ライブラリモードで `.d.ts` ファイルを出力 | TypeScriptライブラリを公開するとき |
|
||||
| `vite-plugin-svgr` | SVGをReactコンポーネントとしてインポート | SVGをコンポーネントとして使用するReactアプリ |
|
||||
| `rollup-plugin-visualizer` | バンドルのツリーマップ/サンバーストレポート | 定期的なバンドルサイズの監査(`enforce: 'post'` を使用) |
|
||||
| `vite-plugin-pwa` | ゼロ設定のPWA + Workbox | オフライン対応アプリ |
|
||||
|
||||
**重要な注意:** `vite build` はトランスパイルしますが、型チェックは行いません。`vite-plugin-checker` を追加するか、CIで `tsc --noEmit` を実行しない限り、型エラーは本番環境にサイレントに出荷されます。
|
||||
|
||||
#### カスタムプラグインの作成
|
||||
|
||||
カスタムプラグインの作成は稀です。ほとんどのニーズは既存のプラグインでカバーできます。必要な場合は `vite.config.ts` にインラインで書き始め、再利用する場合にのみ抽出してください。
|
||||
|
||||
```typescript
|
||||
// vite.config.ts — 最小限のインラインプラグイン
|
||||
function myPlugin(): Plugin {
|
||||
return {
|
||||
name: 'my-plugin', // 必須、一意でなければならない
|
||||
enforce: 'pre', // 'pre' | 'post'(オプション)
|
||||
apply: 'build', // 'build' | 'serve'(オプション)
|
||||
transform(code, id) {
|
||||
if (!id.endsWith('.custom')) return
|
||||
return { code: transformCustom(code), map: null }
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**主要フック:** `transform`(ソースの変更)、`resolveId` + `load`(仮想モジュール)、`transformIndexHtml`(HTMLへの注入)、`configureServer`(デベロップメントミドルウェアの追加)、`hotUpdate`(カスタムHMR — v7+で非推奨の `handleHotUpdate` の代替)。
|
||||
|
||||
**仮想モジュール**は `\0` プレフィックス規約を使用します — `resolveId` は `'\0virtual:my-id'` を返すことで他のプラグインがスキップします。ユーザーコードは `'virtual:my-id'` をインポートします。
|
||||
|
||||
完全なプラグインAPIは [vite.dev/guide/api-plugin](https://vite.dev/guide/api-plugin) を参照してください。開発中の変換パイプラインのデバッグには `vite-plugin-inspect` を使用してください。
|
||||
|
||||
### HMR API
|
||||
|
||||
フレームワークプラグイン(`@vitejs/plugin-react`、`@vitejs/plugin-vue` など)はHMRを自動的に処理します。カスタム状態ストア、デベロップメントツール、または更新を跨いで状態を保持する必要があるフレームワーク非依存のユーティリティをビルドする場合のみ、`import.meta.hot` を直接使用してください。
|
||||
|
||||
```typescript
|
||||
// src/store.ts — バニラモジュールの手動HMR
|
||||
if (import.meta.hot) {
|
||||
// 更新を跨いで状態を保持する(.dataを再代入せず、必ず変更すること)
|
||||
import.meta.hot.data.count = import.meta.hot.data.count ?? 0
|
||||
|
||||
// モジュールが置き換えられる前にサイドエフェクトをクリーンアップ
|
||||
import.meta.hot.dispose((data) => clearInterval(data.intervalId))
|
||||
|
||||
// このモジュール自身の更新を受け入れる
|
||||
import.meta.hot.accept()
|
||||
}
|
||||
```
|
||||
|
||||
すべての `import.meta.hot` コードは本番ビルドからツリーシェイクされます — ガードを削除する必要はありません。
|
||||
|
||||
### 環境変数
|
||||
|
||||
Viteは `.env`、`.env.local`、`.env.[mode]`、`.env.[mode].local` をその順序で読み込みます(後のものが前のものを上書き)。`*.local` ファイルはgitignoreされており、ローカルのシークレット用です。
|
||||
|
||||
#### クライアントサイドアクセス
|
||||
|
||||
`VITE_` プレフィックス付きの変数のみがクライアントコードに公開されます:
|
||||
|
||||
```typescript
|
||||
import.meta.env.VITE_API_URL // string
|
||||
import.meta.env.MODE // 'development' | 'production' | カスタム
|
||||
import.meta.env.BASE_URL // base設定値
|
||||
import.meta.env.DEV // boolean
|
||||
import.meta.env.PROD // boolean
|
||||
import.meta.env.SSR // boolean
|
||||
```
|
||||
|
||||
#### 設定での環境変数使用
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd()) // VITE_ プレフィックスのみ(安全)
|
||||
return {
|
||||
define: {
|
||||
__API_URL__: JSON.stringify(env.VITE_API_URL),
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### セキュリティ
|
||||
|
||||
#### `VITE_` プレフィックスはセキュリティ境界ではない
|
||||
|
||||
`VITE_` でプレフィックスされた変数は**ビルド時にクライアントバンドルに静的にインライン化されます**。ミニファイ、base64エンコード、ソースマップの無効化では隠せません。悪意のある攻撃者は出荷されたJavaScriptから任意の `VITE_` 変数を抽出できます。
|
||||
|
||||
**ルール:** パブリックな値(APIのURL、フィーチャーフラグ、パブリックキー)のみを `VITE_` 変数に入れてください。シークレット(APIトークン、データベースのURL、プライベートキー)はAPIまたはサーバーレス関数の背後にあるサーバーサイドに置かなければなりません。
|
||||
|
||||
#### `loadEnv('')` の落とし穴
|
||||
|
||||
```typescript
|
||||
// BAD: 第3引数として '' を渡すと、サーバーのシークレットを含む全ての環境変数が読み込まれ、
|
||||
// `define` でクライアントコードにインライン化できてしまう。
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
|
||||
// GOOD: 明示的なプレフィックスリスト
|
||||
const env = loadEnv(mode, process.cwd(), ['VITE_', 'APP_'])
|
||||
```
|
||||
|
||||
#### 本番環境のソースマップ
|
||||
|
||||
本番環境のソースマップはオリジナルのソースコードを漏洩させます。エラートラッカー(Sentry、Bugsnag)にアップロードしてローカルで削除しない限り、無効にしてください:
|
||||
|
||||
```typescript
|
||||
build: {
|
||||
sourcemap: false, // デフォルト — このままにする
|
||||
}
|
||||
```
|
||||
|
||||
#### `.gitignore` チェックリスト
|
||||
|
||||
- `.env.local`、`.env.*.local` — ローカルのシークレットオーバーライド
|
||||
- `dist/` — ビルド出力
|
||||
- `node_modules/.vite` — 事前バンドルキャッシュ(古いエントリはゴーストエラーを引き起こす)
|
||||
|
||||
### サーバープロキシ
|
||||
|
||||
```typescript
|
||||
// vite.config.ts — server.proxy
|
||||
server: {
|
||||
proxy: {
|
||||
'/foo': 'http://localhost:4567', // 文字列の短縮形
|
||||
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true, // 仮想ホストバックエンドに必要
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
WebSocketプロキシには、ルート設定に `ws: true` を追加してください。
|
||||
|
||||
### ビルド最適化
|
||||
|
||||
#### 手動チャンク
|
||||
|
||||
```typescript
|
||||
// vite.config.ts — build.rolldownOptions
|
||||
build: {
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
// オブジェクト形式:特定のパッケージをグループ化
|
||||
manualChunks: {
|
||||
'react-vendor': ['react', 'react-dom'],
|
||||
'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-popover'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// 関数形式:ヒューリスティックで分割
|
||||
manualChunks(id) {
|
||||
if (id.includes('node_modules/react')) return 'react-vendor'
|
||||
if (id.includes('node_modules')) return 'vendor'
|
||||
}
|
||||
```
|
||||
|
||||
### パフォーマンス
|
||||
|
||||
#### バレルファイルを避ける
|
||||
|
||||
バレルファイル(ディレクトリからすべてを再エクスポートする `index.ts`)は、1つのシンボルをインポートする場合でも再エクスポートされたファイルをすべて読み込むことを強制します。これは公式ドキュメントで指摘されているデベロップメントサーバーの速度低下の主な原因です。
|
||||
|
||||
```typescript
|
||||
// BAD — 1つのユーティリティのインポートがViteにバレル全体を読み込ませる
|
||||
import { slash } from '@/utils'
|
||||
|
||||
// GOOD — 直接インポート、そのファイルだけが読み込まれる
|
||||
import { slash } from '@/utils/slash'
|
||||
```
|
||||
|
||||
#### インポート拡張子を明示的にする
|
||||
|
||||
暗黙の拡張子はそれぞれ `resolve.extensions` を通じて最大6回のファイルシステムチェックを強制します。大規模なコードベースでは積み重なります。
|
||||
|
||||
```typescript
|
||||
// BAD
|
||||
import Component from './Component'
|
||||
|
||||
// GOOD
|
||||
import Component from './Component.tsx'
|
||||
```
|
||||
|
||||
`tsconfig.json` の `allowImportingTsExtensions` と `resolve.extensions` を実際に使用する拡張子だけに絞ってください。
|
||||
|
||||
#### ホットパスルートのウォームアップ
|
||||
|
||||
`server.warmup.clientFiles` は、ブラウザがリクエストする前に既知のホットエントリを事前変換します。これにより大規模アプリでのコールドロードリクエストのウォーターフォールが解消されます。
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
server: {
|
||||
warmup: {
|
||||
clientFiles: ['./src/main.tsx', './src/routes/**/*.tsx'],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### 遅いデベロップメントサーバーのプロファイリング
|
||||
|
||||
`vite dev` が遅いと感じたら、`vite --profile` から始めてアプリを操作し、`p+enter` を押して `.cpuprofile` を保存します。[Speedscope](https://www.speedscope.app) で読み込み、どのプラグインが時間を消費しているかを確認します(通常はコミュニティプラグインの `buildStart`、`config`、または `configResolved` フック)。
|
||||
|
||||
### ライブラリモード
|
||||
|
||||
npmパッケージを公開する場合は `build.lib` を使用します。設定の詳細よりも重要な2つの落とし穴があります:
|
||||
|
||||
1. **型は出力されません** — `vite-plugin-dts` を追加するか、別途 `tsc --emitDeclarationOnly` を実行してください。
|
||||
2. **ピア依存関係は必ず外部化しなければなりません** — リストされていないピアがライブラリにバンドルされると、コンシューマーで重複ランタイムエラーが発生します。
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
build: {
|
||||
lib: {
|
||||
entry: 'src/index.ts',
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format) => `my-lib.${format}.js`,
|
||||
},
|
||||
rolldownOptions: {
|
||||
external: ['react', 'react-dom', 'react/jsx-runtime'], // すべてのピア依存関係
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### SSR外部化
|
||||
|
||||
ベアの `createServer({ middlewareMode: true })` のセットアップはフレームワーク作者向けです。ほとんどのアプリはNuxt、Remix、SvelteKit、Astro、またはTanStack Startを使用すべきです。フレームワークユーザーとして調整するのは、依存関係がSSRで壊れた場合の外部化設定です:
|
||||
|
||||
```typescript
|
||||
// vite.config.ts — SSRオプション
|
||||
ssr: {
|
||||
external: ['node-native-package'], // SSRバンドルで require() として保持
|
||||
noExternal: ['esm-only-package'], // SSR出力に強制バンドル(ほとんどのSSRエラーを修正)
|
||||
target: 'node', // 'node' または 'webworker'
|
||||
}
|
||||
```
|
||||
|
||||
### 依存関係の事前バンドル
|
||||
|
||||
Viteは依存関係を事前バンドルして、CJS/UMDをESMに変換し、リクエスト数を削減します。
|
||||
|
||||
```typescript
|
||||
// vite.config.ts — optimizeDeps
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'lodash-es', // 重い依存関係を強制的に事前バンドル
|
||||
'cjs-package', // 相互運用問題を引き起こすCJS依存関係
|
||||
'deep-lib/components/**', // 深いインポートのグロブ
|
||||
],
|
||||
exclude: ['local-esm-package'], // 除外する場合は有効なESMでなければならない
|
||||
force: true, // キャッシュを無視して再最適化(一時的なデバッグ)
|
||||
}
|
||||
```
|
||||
|
||||
### 一般的な落とし穴
|
||||
|
||||
#### デベロップメントとビルドが一致しない
|
||||
|
||||
デベロップメントは変換にesbuild/Rolldownを使用し、ビルドはバンドルにRolldownを使用します。CJSライブラリは両者で異なる動作をする場合があります。デプロイ前に必ず `vite build && vite preview` で確認してください。
|
||||
|
||||
#### デプロイ後の古いチャンク
|
||||
|
||||
新しいビルドは新しいチャンクハッシュを生成します。アクティブなセッションを持つユーザーは、もはや存在しない古いファイル名をリクエストします。Viteには組み込みの解決策がありません。緩和策:
|
||||
|
||||
- デプロイメントウィンドウ中は古い `dist/assets/` ファイルを保持する
|
||||
- ルーターでダイナミックインポートエラーをキャッチしてページをリロードする
|
||||
|
||||
#### Dockerとコンテナ
|
||||
|
||||
Viteはデフォルトで `localhost` にバインドし、コンテナの外からはアクセスできません:
|
||||
|
||||
```typescript
|
||||
// vite.config.ts — Docker/コンテナ設定
|
||||
server: {
|
||||
host: true, // 0.0.0.0 にバインド
|
||||
hmr: { clientPort: 3000 }, // リバースプロキシ経由の場合
|
||||
}
|
||||
```
|
||||
|
||||
#### モノレポのファイルアクセス
|
||||
|
||||
Viteはプロジェクトルートへのファイル提供を制限します。ルート外のパッケージはブロックされます:
|
||||
|
||||
```typescript
|
||||
// vite.config.ts — モノレポのファイルアクセス
|
||||
server: {
|
||||
fs: {
|
||||
allow: ['..'], // 親ディレクトリ(ワークスペースルート)を許可
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### アンチパターン
|
||||
|
||||
```typescript
|
||||
// BAD: envPrefix を '' にすると全ての環境変数(シークレットを含む)がクライアントに公開される
|
||||
envPrefix: ''
|
||||
|
||||
// BAD: アプリケーションソースコードで require() が動くと思い込む — ViteはESMファースト
|
||||
const lib = require('some-lib') // 代わりに import を使用
|
||||
|
||||
// BAD: 全てのnode_moduleを個別のチャンクに分割する — 何百もの小さなファイルを生成
|
||||
manualChunks(id) {
|
||||
if (id.includes('node_modules')) {
|
||||
return id.split('node_modules/')[1].split('/')[0] // パッケージごとに1チャンク
|
||||
}
|
||||
}
|
||||
|
||||
// BAD: ライブラリモードでピア依存関係を外部化しない — 重複ランタイムエラーを引き起こす
|
||||
// rolldownOptions.external なしの build.lib
|
||||
|
||||
// BAD: 非推奨のesbuildミニファイアーを使用する
|
||||
build: { minify: 'esbuild' } // 'oxc'(デフォルト)または 'terser' を使用
|
||||
|
||||
// BAD: import.meta.hot.data を再代入で変更する
|
||||
import.meta.hot.data = { count: 0 } // 誤り:プロパティを変更すべきで再代入しない
|
||||
import.meta.hot.data.count = 0 // 正しい
|
||||
```
|
||||
|
||||
**プロセスのアンチパターン:**
|
||||
|
||||
- **`vite preview` は本番サーバーではありません** — ビルドされたバンドルのスモークテストです。`dist/` を実際の静的ホスト(NGINX、Cloudflare Pages、Vercel静的)にデプロイするか、マルチステージDockerfileを使用してください。
|
||||
- **`vite build` が型チェックを行うと期待する** — トランスパイルのみです。型エラーは本番環境にサイレントに出荷されます。`vite-plugin-checker` を追加するか、CIで `tsc --noEmit` を実行してください。
|
||||
- **デフォルトで `@vitejs/plugin-legacy` を導入する** — バンドルサイズが約40%膨らみ、ソースマップのバンドルアナライザーが壊れ、95%以上のモダンブラウザユーザーには不要です。仮定ではなく実際のアナリティクスに基づいて適用してください。
|
||||
- **`tsconfig.json` パスを重複した30以上の `resolve.alias` エントリで手動管理する** — 代わりに `vite-tsconfig-paths` を使用してください。ExcalidrawやPostHogで観察されているため、新しいプロジェクトでは避けてください。
|
||||
- **依存関係の変更後に古い `node_modules/.vite` を放置する** — 事前バンドルキャッシュがゴーストエラーを引き起こします。ブランチを切り替えたときや依存関係をパッチした後にクリアしてください。
|
||||
|
||||
## クイックリファレンス
|
||||
|
||||
| パターン | 使用タイミング |
|
||||
|---------|-------------|
|
||||
| `defineConfig` | 常に — 型推論を提供する |
|
||||
| `loadEnv(mode, root, ['VITE_'])` | 設定での環境変数アクセス(明示的なプレフィックス) |
|
||||
| `vite-plugin-checker` | TypeScriptアプリ(型チェックのギャップを埋める) |
|
||||
| `vite-tsconfig-paths` | 手動の `resolve.alias` の代わりに |
|
||||
| `optimizeDeps.include` | 相互運用問題を引き起こすCJS依存関係 |
|
||||
| `server.proxy` | デベロップメント中にAPIリクエストをバックエンドにルーティング |
|
||||
| `server.host: true` | Docker、コンテナ、リモートアクセス |
|
||||
| `server.warmup.clientFiles` | ホットパスルートの事前変換 |
|
||||
| `build.lib` + `external` | npmパッケージの公開 |
|
||||
| `manualChunks`(オブジェクト形式) | ベンダーバンドルの分割 |
|
||||
| `vite --profile` | 遅いデベロップメントサーバーのデバッグ |
|
||||
| `vite build && vite preview` | 本番バンドルのローカルスモークテスト(本番サーバーではない) |
|
||||
|
||||
## 関連スキル
|
||||
|
||||
- `frontend-patterns` — Reactコンポーネントパターン
|
||||
- `docker-patterns` — Viteを使用したコンテナ化されたデベロップメント
|
||||
- `nextjs-turbopack` — Next.jsの代替バンドラー
|
||||
784
docs/ja-JP/skills/windows-desktop-e2e/SKILL.md
Normal file
784
docs/ja-JP/skills/windows-desktop-e2e/SKILL.md
Normal file
@@ -0,0 +1,784 @@
|
||||
---
|
||||
name: windows-desktop-e2e
|
||||
description: E2E testing for Windows native desktop apps (WPF, WinForms, Win32/MFC, Qt) using pywinauto and Windows UI Automation.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Windows デスクトップ E2E テスト
|
||||
|
||||
**pywinauto** と Windows UI Automation(UIA)を使用したWindowsネイティブデスクトップアプリケーションのエンドツーエンドテスト。WPF、WinForms、Win32/MFC、Qt(5.x / 6.x)をカバーし、Qt固有のガイダンスは専用セクションとして提供します。
|
||||
|
||||
## アクティベートするタイミング
|
||||
|
||||
- Windowsネイティブデスクトップアプリケーションのエンドツーエンドテストを書くまたは実行するとき
|
||||
- デスクトップGUIテストスイートをゼロから設定するとき
|
||||
- 不安定または失敗するデスクトップオートメーションテストを診断するとき
|
||||
- 既存のアプリにテスタビリティ(AutomationId、アクセシブルな名前)を追加するとき
|
||||
- デスクトップエンドツーエンドをCI/CDパイプライン(GitHub Actions `windows-latest`)に統合するとき
|
||||
|
||||
### 使用しないタイミング
|
||||
|
||||
- Webアプリケーション → `e2e-testing` スキル(Playwright)を使用する
|
||||
- Electron / CEF / WebView2 アプリ → HTMLレイヤーにはUIAではなくブラウザオートメーションが必要
|
||||
- モバイルアプリ → プラットフォーム固有のツールを使用する(UIAutomator、XCUITest)
|
||||
- 実行中のGUIを必要としない純粋なユニットまたは統合テスト
|
||||
|
||||
## コアコンセプト
|
||||
|
||||
すべてのWindowsデスクトップオートメーションは**UI Automation(UIA)**に依存します。これはWindowsに組み込まれたアクセシビリティAPIです。サポートされているすべてのフレームワークは、読み取りおよび操作可能なプロパティを持つUIA要素のツリーを公開します:
|
||||
|
||||
```
|
||||
テスト(Python)
|
||||
└── pywinauto(UIAバックエンド)
|
||||
└── Windows UI Automation API ← Windowsに組み込み、フレームワーク非依存
|
||||
└── アプリのUIAプロバイダー ← 各フレームワークが独自に実装
|
||||
└── 実行中の .exe
|
||||
```
|
||||
|
||||
**フレームワーク別UIA品質:**
|
||||
|
||||
| フレームワーク | AutomationId | 信頼性 | 注記 |
|
||||
|-----------|-------------|-------------|-------|
|
||||
| WPF | ★★★★★ | 優秀 | `x:Name` が直接AutomationIdにマッピング |
|
||||
| WinForms | ★★★★☆ | 良好 | `AccessibleName` = AutomationId |
|
||||
| UWP / WinUI 3 | ★★★★★ | 優秀 | Microsoftの完全サポート |
|
||||
| Qt 6.x | ★★★★★ | 優秀 | アクセシビリティがデフォルトで有効;クラス名が `Qt6*` に変更 |
|
||||
| Qt 5.15+ | ★★★★☆ | 良好 | Accessibilityモジュールが改善 |
|
||||
| Qt 5.7–5.14 | ★★★☆☆ | 普通 | `QT_ACCESSIBILITY=1` が必要;objectNameは手動設定 |
|
||||
| Win32 / MFC | ★★★☆☆ | 普通 | コントロールIDにアクセス可能;テキストマッチングが一般的 |
|
||||
|
||||
## セットアップと前提条件
|
||||
|
||||
```bash
|
||||
# Python 3.8+、Windowsのみ
|
||||
pip install pywinauto pytest pytest-html Pillow pytest-timeout
|
||||
# オプション:画面録画
|
||||
# ffmpegをインストールしてPATHに追加:https://ffmpeg.org/download.html
|
||||
```
|
||||
|
||||
UIAが到達可能か確認:
|
||||
|
||||
```python
|
||||
from pywinauto import Desktop
|
||||
Desktop(backend="uia").windows() # すべてのトップレベルウィンドウを一覧表示
|
||||
```
|
||||
|
||||
**Accessibility Insights for Windows**をインストールしてください(Microsoft提供、無料)— テストを書く前にUIA要素ツリーを検査するためのDevTools相当のツールです。
|
||||
|
||||
## テスタビリティのセットアップ(フレームワーク別)
|
||||
|
||||
テストを書く前に**全てのインタラクティブなコントロールに安定したAutomationIdを設定すること**が最も効果的です。
|
||||
|
||||
### WPF
|
||||
|
||||
```xml
|
||||
<!-- XAML: x:Name が自動的にAutomationIdになる -->
|
||||
<TextBox x:Name="usernameInput" />
|
||||
<PasswordBox x:Name="passwordInput" />
|
||||
<Button x:Name="btnLogin" Content="Login" />
|
||||
<TextBlock x:Name="lblError" />
|
||||
```
|
||||
|
||||
### WinForms
|
||||
|
||||
```csharp
|
||||
// デザイナーまたはコードで設定
|
||||
usernameInput.AccessibleName = "usernameInput";
|
||||
passwordInput.AccessibleName = "passwordInput";
|
||||
btnLogin.AccessibleName = "btnLogin";
|
||||
lblError.AccessibleName = "lblError";
|
||||
```
|
||||
|
||||
### Win32 / MFC
|
||||
|
||||
```cpp
|
||||
// .rcファイルのコントロールリソースIDがAutomationId文字列として公開される
|
||||
// IDC_EDIT_USERNAME -> AutomationId "1001"
|
||||
// 名前にはSetWindowTextを優先;より豊かなサポートにはIAccessibleを追加する
|
||||
```
|
||||
|
||||
### Qt — 以下の専用セクションを参照
|
||||
|
||||
---
|
||||
|
||||
## ページオブジェクトモデル
|
||||
|
||||
```
|
||||
tests/
|
||||
├── conftest.py # アプリ起動フィクスチャ、失敗時スクリーンショット
|
||||
├── pytest.ini
|
||||
├── config.py
|
||||
├── pages/
|
||||
│ ├── __init__.py # インポートに必須
|
||||
│ ├── base_page.py # ロケーター、ウェイト、スクリーンショットヘルパー
|
||||
│ ├── login_page.py
|
||||
│ └── main_page.py
|
||||
├── tests/
|
||||
│ ├── __init__.py
|
||||
│ ├── test_login.py
|
||||
│ └── test_main_flow.py
|
||||
└── artifacts/ # スクリーンショット、動画、ログ
|
||||
```
|
||||
|
||||
### base_page.py
|
||||
|
||||
```python
|
||||
import os, time
|
||||
from pywinauto import Desktop
|
||||
from config import ACTION_TIMEOUT, ARTIFACT_DIR
|
||||
|
||||
class BasePage:
|
||||
def __init__(self, window):
|
||||
self.window = window
|
||||
|
||||
# --- ロケーター(優先順位順)---
|
||||
|
||||
def by_id(self, auto_id, **kw):
|
||||
"""AutomationId — 最も安定。第一選択として使用する。"""
|
||||
return self.window.child_window(auto_id=auto_id, **kw)
|
||||
|
||||
def by_name(self, name, **kw):
|
||||
"""表示テキスト / アクセシブルな名前。"""
|
||||
return self.window.child_window(title=name, **kw)
|
||||
|
||||
def by_class(self, cls, index=0, **kw):
|
||||
"""コントロールクラス + インデックス — 脆弱、可能なら避ける。"""
|
||||
return self.window.child_window(class_name=cls, found_index=index, **kw)
|
||||
|
||||
# --- ウェイト ---
|
||||
|
||||
def wait_visible(self, spec, timeout=ACTION_TIMEOUT):
|
||||
spec.wait("visible", timeout=timeout)
|
||||
return spec
|
||||
|
||||
def wait_gone(self, spec, timeout=ACTION_TIMEOUT):
|
||||
spec.wait_not("visible", timeout=timeout)
|
||||
return spec
|
||||
|
||||
def wait_window(self, title, timeout=ACTION_TIMEOUT):
|
||||
"""新しいトップレベルウィンドウ(ダイアログ、子ウィンドウ)を待つ。"""
|
||||
dlg = Desktop(backend="uia").window(title=title)
|
||||
dlg.wait("visible", timeout=timeout)
|
||||
return dlg
|
||||
|
||||
def wait_until(self, fn, timeout=ACTION_TIMEOUT, interval=0.3):
|
||||
"""任意の条件をポーリング — UIAイベントが信頼できない場合に使用する。"""
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
if fn():
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(interval)
|
||||
raise TimeoutError(f"条件が{timeout}秒以内に満たされなかった")
|
||||
|
||||
# --- アクション ---
|
||||
|
||||
def click(self, spec):
|
||||
self.wait_visible(spec)
|
||||
spec.click_input()
|
||||
|
||||
def type_text(self, spec, text):
|
||||
self.wait_visible(spec)
|
||||
ctrl = spec.wrapper_object()
|
||||
try:
|
||||
ctrl.set_edit_text(text)
|
||||
except Exception as e:
|
||||
# Qt 5.x フォールバック:UIA Value Pattern が不完全な場合がある
|
||||
import sys, pywinauto.keyboard as kb
|
||||
print(f"[windows-desktop-e2e] set_edit_text 失敗 ({e})、キーボードフォールバックを使用", file=sys.stderr)
|
||||
ctrl.click_input()
|
||||
kb.send_keys("^a")
|
||||
kb.send_keys(text, with_spaces=True)
|
||||
|
||||
def get_text(self, spec):
|
||||
ctrl = spec.wrapper_object()
|
||||
for attr in ("window_text", "get_value"):
|
||||
try:
|
||||
v = getattr(ctrl, attr)()
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
# --- アーティファクト ---
|
||||
|
||||
def screenshot(self, name):
|
||||
os.makedirs(ARTIFACT_DIR, exist_ok=True)
|
||||
path = os.path.join(ARTIFACT_DIR, f"{name}.png")
|
||||
self.window.capture_as_image().save(path)
|
||||
return path
|
||||
```
|
||||
|
||||
### login_page.py
|
||||
|
||||
```python
|
||||
from pages.base_page import BasePage
|
||||
|
||||
class LoginPage(BasePage):
|
||||
@property
|
||||
def username(self): return self.by_id("usernameInput")
|
||||
|
||||
@property
|
||||
def password(self): return self.by_id("passwordInput")
|
||||
|
||||
@property
|
||||
def btn_login(self): return self.by_id("btnLogin")
|
||||
|
||||
@property
|
||||
def error_label(self): return self.by_id("lblError")
|
||||
|
||||
def login(self, user, pwd):
|
||||
self.type_text(self.username, user)
|
||||
self.type_text(self.password, pwd)
|
||||
self.click(self.btn_login)
|
||||
|
||||
def login_ok(self, user, pwd, main_title="Main Window"):
|
||||
self.login(user, pwd)
|
||||
return self.wait_window(main_title)
|
||||
|
||||
def login_fail(self, user, pwd):
|
||||
self.login(user, pwd)
|
||||
self.wait_visible(self.error_label)
|
||||
return self.get_text(self.error_label)
|
||||
```
|
||||
|
||||
### conftest.py
|
||||
|
||||
> 新しいプロジェクトでは**Tier 1サンドボックスフィクスチャ**(以下参照)を優先してください — 追加コストゼロでファイルシステムの分離が追加されます。この基本フィクスチャは最小限/レガシーセットアップ専用です。
|
||||
|
||||
```python
|
||||
import os, pytest
|
||||
os.environ["QT_ACCESSIBILITY"] = "1" # Qt 5.x UIAサポートに必要
|
||||
|
||||
from pywinauto import Application
|
||||
from config import APP_PATH, MAIN_WINDOW_TITLE, LAUNCH_TIMEOUT, ARTIFACT_DIR
|
||||
|
||||
@pytest.fixture
|
||||
def app(request):
|
||||
if not APP_PATH:
|
||||
pytest.exit("APP_PATH 環境変数が設定されていない", returncode=1)
|
||||
proc = Application(backend="uia").start(APP_PATH, timeout=LAUNCH_TIMEOUT)
|
||||
win = proc.window(title=MAIN_WINDOW_TITLE)
|
||||
win.wait("visible", timeout=LAUNCH_TIMEOUT)
|
||||
yield win
|
||||
# 失敗時のスクリーンショット
|
||||
if getattr(getattr(request.node, "rep_call", None), "failed", False):
|
||||
os.makedirs(ARTIFACT_DIR, exist_ok=True)
|
||||
try:
|
||||
win.capture_as_image().save(
|
||||
os.path.join(ARTIFACT_DIR, f"FAIL_{request.node.name}.png")
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# グレースフルな終了を試み、フォールバックとして強制終了
|
||||
# proc は pywinauto Application — wait_for_process() ではなく wait_for_process_exit() を使用
|
||||
try:
|
||||
win.close()
|
||||
proc.wait_for_process_exit(timeout=5)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
outcome = yield
|
||||
setattr(item, f"rep_{outcome.get_result().when}", outcome.get_result())
|
||||
```
|
||||
|
||||
### config.py
|
||||
|
||||
```python
|
||||
import os
|
||||
APP_PATH = os.environ.get("APP_PATH", "") # 環境変数で設定 — デフォルトパスなし
|
||||
MAIN_WINDOW_TITLE = os.environ.get("APP_TITLE", "")
|
||||
LAUNCH_TIMEOUT = int(os.environ.get("LAUNCH_TIMEOUT", "15"))
|
||||
ACTION_TIMEOUT = int(os.environ.get("ACTION_TIMEOUT", "10"))
|
||||
ARTIFACT_DIR = os.path.join(os.path.dirname(__file__), "artifacts")
|
||||
```
|
||||
|
||||
### pytest.ini
|
||||
|
||||
```ini
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
markers =
|
||||
smoke: 重要なパスの高速スモークテスト
|
||||
flaky: 既知の不安定なテスト
|
||||
addopts = -v --tb=short --html=artifacts/report.html --self-contained-html
|
||||
```
|
||||
|
||||
## ロケーター戦略
|
||||
|
||||
```
|
||||
AutomationId > Name(テキスト) > ClassName + インデックス > XPath
|
||||
(安定) (可読) (脆弱) (最後の手段)
|
||||
```
|
||||
|
||||
Accessibility Insights → **Properties** ペインで検査 → まず `AutomationId` を確認。
|
||||
|
||||
```python
|
||||
# 実行時の検査 — REPLに貼り付けてツリーを探索
|
||||
win.print_control_identifiers()
|
||||
# またはスコープを絞る:
|
||||
win.child_window(auto_id="groupBox1").print_control_identifiers()
|
||||
```
|
||||
|
||||
## ウェイトパターン
|
||||
|
||||
```python
|
||||
# コントロールが表示されるのを待つ
|
||||
page.wait_visible(page.by_id("statusLabel"))
|
||||
|
||||
# コントロールが消えるのを待つ(ローディングスピナーなど)
|
||||
page.wait_gone(page.by_id("spinnerOverlay"))
|
||||
|
||||
# ダイアログが表示されるのを待つ
|
||||
dlg = page.wait_window("Confirm Delete")
|
||||
|
||||
# カスタム条件(テキストの変化など)
|
||||
page.wait_until(lambda: page.get_text(page.by_id("lblStatus")) == "Ready")
|
||||
```
|
||||
|
||||
**`time.sleep()` を主要な同期手段として使用しないこと** — `wait()` または `wait_until()` を使用してください。
|
||||
|
||||
## アーティファクト管理
|
||||
|
||||
```python
|
||||
# オンデマンドスクリーンショット
|
||||
page.screenshot("after_login")
|
||||
|
||||
# フルスクリーンキャプチャ(ウィンドウが画面外または最小化されている場合)
|
||||
import pyautogui
|
||||
pyautogui.screenshot("artifacts/fullscreen.png")
|
||||
|
||||
# ffmpegによる画面録画(テスト前に開始し、テスト後に停止)
|
||||
import subprocess
|
||||
|
||||
def start_recording(name):
|
||||
return subprocess.Popen([
|
||||
"ffmpeg", "-f", "gdigrab", "-framerate", "10",
|
||||
"-i", "desktop", "-y", f"artifacts/videos/{name}.mp4"
|
||||
], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
def stop_recording(proc):
|
||||
proc.stdin.write(b"q"); proc.stdin.flush(); proc.wait(timeout=10)
|
||||
```
|
||||
|
||||
## 不安定なテストの対処
|
||||
|
||||
```python
|
||||
# 隔離 — PlaywrightのtestのFixmeと同等
|
||||
@pytest.mark.skip(reason="不安定:遅いCIでのアニメーションレース。Issue #42")
|
||||
def test_animated_transition(self, app): ...
|
||||
|
||||
# CIのみでスキップ
|
||||
@pytest.mark.skipif(os.environ.get("CI") == "true", reason="CIで不安定 #43")
|
||||
def test_heavy_load(self, app): ...
|
||||
```
|
||||
|
||||
一般的な原因と修正:
|
||||
|
||||
| 原因 | 修正 |
|
||||
|-------|-----|
|
||||
| コントロールが準備できていない | `time.sleep` を `wait_visible` に置き換える |
|
||||
| ウィンドウがフォーカスされていない | インタラクション前に `win.set_focus()` を追加する |
|
||||
| アニメーション進行中 | `wait_until(lambda: not loading_indicator.exists())` |
|
||||
| ダイアログのタイミング | `wait_window(title, timeout=15)` |
|
||||
| CI環境のディスプレイが準備できていない | `DISPLAY` を設定するかCIで仮想デスクトップを使用する |
|
||||
|
||||
## テスト分離とサンドボックス
|
||||
|
||||
分離の3つの階層 — ニーズを満たす最も軽い階層を使用してください。
|
||||
|
||||
### Tier 1 — ファイルシステム分離(デフォルト、常に使用)
|
||||
|
||||
各テストは `subprocess.Popen` と `Application.connect()` を通じて独自の `APPDATA` / `LOCALAPPDATA` / `TEMP` を取得します。pytestの `tmp_path` フィクスチャがクリーンアップを自動的に処理します。
|
||||
|
||||
```python
|
||||
# conftest.py — 基本的な `app` フィクスチャをこれに置き換える
|
||||
import os, subprocess, pytest
|
||||
from pywinauto import Application
|
||||
from config import APP_PATH, APP_ARGS, APP_TITLE, LAUNCH_TIMEOUT, ACTION_TIMEOUT, ARTIFACT_DIR
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def app(request, tmp_path):
|
||||
"""テストごとに新しいプロセス + 分離されたユーザーデータディレクトリ。"""
|
||||
if not APP_PATH:
|
||||
pytest.exit("APP_PATH が設定されていない", returncode=1)
|
||||
|
||||
# 全てのユーザーストレージを分離されたtmpディレクトリにリダイレクト
|
||||
sandbox_env = os.environ.copy()
|
||||
sandbox_env["QT_ACCESSIBILITY"] = "1"
|
||||
sandbox_env["APPDATA"] = str(tmp_path / "AppData" / "Roaming")
|
||||
sandbox_env["LOCALAPPDATA"] = str(tmp_path / "AppData" / "Local")
|
||||
sandbox_env["TEMP"] = sandbox_env["TMP"] = str(tmp_path / "Temp")
|
||||
for p in (sandbox_env["APPDATA"], sandbox_env["LOCALAPPDATA"], sandbox_env["TEMP"]):
|
||||
os.makedirs(p, exist_ok=True)
|
||||
|
||||
if not APP_TITLE:
|
||||
pytest.exit("APP_TITLE 環境変数が設定されていない", returncode=1)
|
||||
|
||||
# shlex.splitはスペースを含む引用符付き引数を処理;plain split()は壊れる
|
||||
import shlex
|
||||
# subprocessで起動して環境変数を渡し;PIDでpywinautoを接続
|
||||
proc = subprocess.Popen(
|
||||
[APP_PATH] + shlex.split(APP_ARGS),
|
||||
env=sandbox_env,
|
||||
)
|
||||
pw_app = Application(backend="uia").connect(process=proc.pid, timeout=LAUNCH_TIMEOUT)
|
||||
win = pw_app.window(title=APP_TITLE)
|
||||
win.wait("visible", timeout=LAUNCH_TIMEOUT)
|
||||
yield win
|
||||
|
||||
if getattr(getattr(request.node, "rep_call", None), "failed", False):
|
||||
os.makedirs(ARTIFACT_DIR, exist_ok=True)
|
||||
try:
|
||||
win.capture_as_image().save(
|
||||
os.path.join(ARTIFACT_DIR, f"FAIL_{request.node.name}.png")
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
win.close()
|
||||
proc.wait(timeout=5)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
# tmp_pathはpytestによって自動的にクリーンアップされる
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
outcome = yield
|
||||
setattr(item, f"rep_{outcome.get_result().when}", outcome.get_result())
|
||||
```
|
||||
|
||||
### Tier 2 — Windowsジョブオブジェクト(オプション:プロセスライフタイムの封じ込め)
|
||||
|
||||
プロセスをジョブオブジェクトにアタッチして、テストフィクスチャのジョブハンドルがGCされたときに**自動的に終了**させます。また、フィクスチャのクリーンアップから逃れる子プロセスのスポーンも防止します。
|
||||
|
||||
> **分離のスコープ:** ジョブオブジェクトはファイルシステムアクセスの仮想化や
|
||||
> ネットワークトラフィックのブロックを行いません。ファイル書き込みとネットワーク分離には
|
||||
> AppContainer、Windowsファイアウォールルール、またはTier 3(Windowsサンドボックス)が必要です。
|
||||
> Tier 2はプロセスライフタイムと子プロセスの封じ込めにのみ使用してください。
|
||||
|
||||
追加の依存関係は不要です。
|
||||
|
||||
```python
|
||||
import ctypes, ctypes.wintypes as wt
|
||||
|
||||
def restrict_process(pid: int):
|
||||
"""
|
||||
プロセスをジョブオブジェクトにアタッチして以下を防止:
|
||||
- ジョブ外でのプロセスのスポーン(LIMIT_KILL_ON_JOB_CLOSE)
|
||||
ネットワークはブロックしません — Windowsファイアウォールルールを使用してください。
|
||||
"""
|
||||
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
|
||||
# 最小限の権限:SET_QUOTA (0x0100) | TERMINATE (0x0001)
|
||||
PROCESS_SET_QUOTA_AND_TERMINATE = 0x0101
|
||||
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
job = kernel32.CreateJobObjectW(None, None)
|
||||
hproc = kernel32.OpenProcess(PROCESS_SET_QUOTA_AND_TERMINATE, False, pid)
|
||||
|
||||
# 正しい構造体レイアウト — LimitFlagsはオフセット+16(+44ではない)
|
||||
class JOBOBJECT_BASIC_LIMIT_INFORMATION(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("PerProcessUserTimeLimit", wt.LARGE_INTEGER),
|
||||
("PerJobUserTimeLimit", wt.LARGE_INTEGER),
|
||||
("LimitFlags", wt.DWORD),
|
||||
("MinimumWorkingSetSize", ctypes.c_size_t),
|
||||
("MaximumWorkingSetSize", ctypes.c_size_t),
|
||||
("ActiveProcessLimit", wt.DWORD),
|
||||
("Affinity", ctypes.c_size_t),
|
||||
("PriorityClass", wt.DWORD),
|
||||
("SchedulingClass", wt.DWORD),
|
||||
]
|
||||
|
||||
info = JOBOBJECT_BASIC_LIMIT_INFORMATION()
|
||||
info.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
||||
ok = kernel32.SetInformationJobObject(job, 2, ctypes.byref(info), ctypes.sizeof(info))
|
||||
if not ok:
|
||||
raise ctypes.WinError()
|
||||
kernel32.AssignProcessToJobObject(job, hproc)
|
||||
kernel32.CloseHandle(hproc)
|
||||
return job # 生存を維持 — ジョブが閉じると(GC時)プロセスが終了する
|
||||
|
||||
# proc = subprocess.Popen(...) の後: job = restrict_process(proc.pid)
|
||||
```
|
||||
|
||||
### Tier 3 — Windowsサンドボックス(CI完全OS分離)
|
||||
|
||||
実行ごとにクリーンなWindowsイメージが必要な場合(残留レジストリキーなし、共有GPUステートなし、真の分離)、[Windowsサンドボックス](https://learn.microsoft.com/windows/security/application-security/application-isolation/windows-sandbox/windows-sandbox-overview)内で**テストスイート全体**を実行します。
|
||||
|
||||
**要件:** Windows 10/11 Pro またはエンタープライズ、仮想化が有効。
|
||||
|
||||
プロジェクトルートに `e2e-sandbox.wsb` を作成:
|
||||
|
||||
```xml
|
||||
<Configuration>
|
||||
<MappedFolders>
|
||||
<!-- アプリバイナリ(読み取り専用) -->
|
||||
<MappedFolder>
|
||||
<HostFolder>C:\path\to\your\build\Release</HostFolder>
|
||||
<SandboxFolder>C:\app</SandboxFolder>
|
||||
<ReadOnly>true</ReadOnly>
|
||||
</MappedFolder>
|
||||
<!-- テストスイート(アーティファクト用に読み書き可能) -->
|
||||
<MappedFolder>
|
||||
<HostFolder>C:\path\to\your\e2e_test</HostFolder>
|
||||
<SandboxFolder>C:\e2e_test</SandboxFolder>
|
||||
<ReadOnly>false</ReadOnly>
|
||||
</MappedFolder>
|
||||
</MappedFolders>
|
||||
<LogonCommand>
|
||||
<!--
|
||||
WindowsサンドボックスはデフォルトでPythonがない。まずサイレントインストール、
|
||||
次に依存関係をインストールしてテストを実行する。アーティファクトは
|
||||
上記のMappedFolderを通じてホストに書き戻される。
|
||||
-->
|
||||
<Command>powershell -Command "
|
||||
winget install --id Python.Python.3.11 --silent --accept-package-agreements;
|
||||
$env:PATH += ';' + $env:LOCALAPPDATA + '\Programs\Python\Python311\Scripts';
|
||||
cd C:\e2e_test;
|
||||
pip install -r requirements.txt;
|
||||
pytest tests\ -v
|
||||
"</Command>
|
||||
</LogonCommand>
|
||||
</Configuration>
|
||||
```
|
||||
|
||||
起動:`WindowsSandbox.exe e2e-sandbox.wsb`
|
||||
|
||||
> pywinautoとアプリは両方ともサンドボックス**内**で実行されます(同じセッションが必要)。
|
||||
> アーティファクトはマップされたフォルダーを通じてホストに書き戻されます。
|
||||
|
||||
### 階層の比較
|
||||
|
||||
| 階層 | 分離 | セットアップコスト | CIで動作 | 使用タイミング |
|
||||
|------|-----------|-----------|-------------|----------|
|
||||
| 1 — `tmp_path` 環境リダイレクト | ファイルシステム | ゼロ | 常に | 全テストのデフォルト |
|
||||
| 2 — ジョブオブジェクト | プロセスツリー | 低 | 常に | 子プロセスの逃走を防止 |
|
||||
| 3 — Windowsサンドボックス | 完全OS | 中 | Pro/Enterpriseイメージが必要 | 定期的なクリーンルーム実行 |
|
||||
|
||||
### テストのハングを防止する
|
||||
|
||||
`pytest-timeout` を追加して単一テストに上限を設けます。`pytest.ini` で `timeout = 60` と `timeout_method = thread` を設定します。注意:`thread` メソッドはWindows上でQtアプリのサブプロセスを終了できません — `conftest.py` に `atexit.register(lambda: [p.kill() for p in psutil.Process().children(recursive=True)])` を追加してオーファンを刈り取ってください。
|
||||
|
||||
## CI/CDインテグレーション
|
||||
|
||||
```yaml
|
||||
# .github/workflows/e2e-desktop.yml
|
||||
name: Desktop E2E
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: windows-latest # 実際のGUI環境、Xvfb不要
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with: { python-version: "3.11" }
|
||||
|
||||
- name: 依存関係をインストール
|
||||
run: pip install pywinauto pytest pytest-html Pillow
|
||||
|
||||
- name: アプリをビルド
|
||||
run: cmake --build build --config Release # ビルドシステムに合わせて調整
|
||||
|
||||
- name: E2Eを実行
|
||||
env:
|
||||
APP_PATH: ${{ github.workspace }}\build\Release\MyApp.exe
|
||||
APP_TITLE: "My Application"
|
||||
CI: "true"
|
||||
run: pytest tests/ --html=artifacts/report.html --self-contained-html --junitxml=artifacts/results.xml -v
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: e2e-artifacts
|
||||
path: artifacts/
|
||||
retention-days: 14
|
||||
```
|
||||
|
||||
## Qt固有
|
||||
|
||||
### Qt 5.xでのUIA有効化
|
||||
|
||||
Qt 5.xのアクセシビリティは一部のビルド(特に5.7〜5.14)でデフォルトが無効です。起動前に環境変数を設定してください。Qt 6.xはデフォルトでアクセシビリティが有効です — Qt 6ではこのステップをスキップしてください。
|
||||
|
||||
```python
|
||||
# conftest.py — モジュールの先頭に追加
|
||||
import os
|
||||
os.environ["QT_ACCESSIBILITY"] = "1"
|
||||
```
|
||||
|
||||
またはCIでエクスポート:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
QT_ACCESSIBILITY: "1"
|
||||
```
|
||||
|
||||
### Qtウィジェットへの安定した識別子の追加
|
||||
|
||||
```cpp
|
||||
// 優先:objectNameとaccessibleNameの両方
|
||||
void setTestId(QWidget* w, const char* id) {
|
||||
w->setObjectName(id);
|
||||
w->setAccessibleName(id); // UIA Nameプロパティになる
|
||||
}
|
||||
|
||||
// ダイアログコンストラクタ内:
|
||||
setTestId(ui->usernameEdit, "usernameInput");
|
||||
setTestId(ui->passwordEdit, "passwordInput");
|
||||
setTestId(ui->loginButton, "btnLogin");
|
||||
setTestId(ui->errorLabel, "lblError");
|
||||
```
|
||||
|
||||
タイポを避けるためにすべてのIDをヘッダーに集約:
|
||||
|
||||
```cpp
|
||||
// test_ids.h
|
||||
#define TID_USERNAME "usernameInput"
|
||||
#define TID_PASSWORD "passwordInput"
|
||||
#define TID_BTN_LOGIN "btnLogin"
|
||||
#define TID_LBL_ERROR "lblError"
|
||||
```
|
||||
|
||||
### Qt固有の注意点
|
||||
|
||||
**QComboBox** — ドロップダウンは別のトップレベルウィンドウです:
|
||||
|
||||
```python
|
||||
from pywinauto import Desktop
|
||||
|
||||
def select_combo_item(page, combo_spec, item_text):
|
||||
page.click(combo_spec)
|
||||
# ドロップダウンは新しいルートレベルウィンドウとして表示される
|
||||
# class_nameはQtバージョンによって異なる — Accessibility Insightsで確認
|
||||
# Qt 5.x: "Qt5QWindowIcon" | Qt 6.x: "Qt6QWindowIcon" — Accessibility Insightsで確認
|
||||
popup = Desktop(backend="uia").window(class_name_re="Qt[56]QWindowIcon")
|
||||
popup.wait("visible", timeout=5)
|
||||
popup.child_window(title=item_text).click_input()
|
||||
```
|
||||
|
||||
**QMessageBox / QDialog** — これも別のトップレベルウィンドウです:
|
||||
|
||||
```python
|
||||
dlg = page.wait_window("Confirm") # ダイアログタイトルを待つ
|
||||
dlg.child_window(title="OK").click_input() # 内部のボタンをクリック
|
||||
```
|
||||
|
||||
**QTableWidget / QTableView** — 行/セルアクセス:
|
||||
|
||||
```python
|
||||
table = page.by_id("tblUsers").wrapper_object()
|
||||
cell = table.cell(row=0, column=1)
|
||||
print(cell.window_text())
|
||||
```
|
||||
|
||||
**自己描画コントロール**(`paintEvent`のみ、`QGraphicsView`、`QOpenGLWidget`)— UIAは内部を見ることができません。以下のフォールバックセクションを使用してください。
|
||||
|
||||
## フォールバック:スクリーンショットモード
|
||||
|
||||
コントロールがUIAで到達できない場合(自己描画、サードパーティ、ゲームエンジン):
|
||||
|
||||
```bash
|
||||
pip install pyautogui Pillow opencv-python
|
||||
```
|
||||
|
||||
```python
|
||||
import pyautogui, cv2, numpy as np
|
||||
from PIL import Image
|
||||
|
||||
def find_image_on_screen(template_path, confidence=0.85):
|
||||
"""画面上のテンプレート画像を探す。(x, y) の中心またはNoneを返す。"""
|
||||
screen = np.array(pyautogui.screenshot())
|
||||
template = np.array(Image.open(template_path))
|
||||
result = cv2.matchTemplate(
|
||||
cv2.cvtColor(screen, cv2.COLOR_RGB2BGR),
|
||||
cv2.cvtColor(template, cv2.COLOR_RGB2BGR),
|
||||
cv2.TM_CCOEFF_NORMED,
|
||||
)
|
||||
_, max_val, _, max_loc = cv2.minMaxLoc(result)
|
||||
if max_val >= confidence:
|
||||
h, w = template.shape[:2]
|
||||
return max_loc[0] + w // 2, max_loc[1] + h // 2
|
||||
return None
|
||||
|
||||
def click_image(template_path, confidence=0.85):
|
||||
pos = find_image_on_screen(template_path, confidence)
|
||||
if pos is None:
|
||||
raise RuntimeError(f"画面上で画像が見つからない:{template_path}")
|
||||
pyautogui.click(*pos)
|
||||
```
|
||||
|
||||
**控えめに使用すること** — 画像マッチングはDPI変更、テーマ切り替え、部分的な遮蔽で壊れます。
|
||||
常にUIAを最初に試し、本当に到達できないコントロールにのみスクリーンショットにフォールバックしてください。
|
||||
|
||||
## アンチパターン
|
||||
|
||||
```python
|
||||
# BAD: 固定スリープ
|
||||
time.sleep(3)
|
||||
page.click(page.by_id("btnSubmit"))
|
||||
|
||||
# GOOD: 条件ウェイト
|
||||
page.wait_visible(page.by_id("btnSubmit"))
|
||||
page.click(page.by_id("btnSubmit"))
|
||||
```
|
||||
|
||||
```python
|
||||
# BAD: 主要戦略として脆弱なクラス+インデックスロケーター
|
||||
page.by_class("Edit", index=2).type_keys("hello")
|
||||
|
||||
# GOOD: AutomationId
|
||||
page.by_id("usernameInput").set_edit_text("hello")
|
||||
```
|
||||
|
||||
```python
|
||||
# BAD: ピクセル座標でのアサート
|
||||
assert btn.rectangle().left == 120
|
||||
|
||||
# GOOD: コンテンツ/状態でのアサート
|
||||
assert page.get_text(page.by_id("lblStatus")) == "Logged in"
|
||||
assert page.by_id("btnLogout").is_enabled()
|
||||
```
|
||||
|
||||
```python
|
||||
# BAD: 全テストにわたってアプリインスタンスを共有(状態の漏洩)
|
||||
@pytest.fixture(scope="session")
|
||||
def app(): ...
|
||||
|
||||
# GOOD: テストごとに新しいプロセス(または最大でもクラスごと)
|
||||
@pytest.fixture(scope="function")
|
||||
def app(): ...
|
||||
```
|
||||
|
||||
## テストの実行
|
||||
|
||||
```bash
|
||||
# 全テスト
|
||||
pytest tests/ -v
|
||||
|
||||
# スモークのみ
|
||||
pytest tests/ -m smoke -v
|
||||
|
||||
# 特定ファイル
|
||||
pytest tests/test_login.py -v
|
||||
|
||||
# カスタムアプリパスで実行
|
||||
APP_PATH="C:\build\Release\MyApp.exe" APP_TITLE="MyApp" pytest tests/ -v
|
||||
|
||||
# 不安定なテストを検出(各テストを5回繰り返す)
|
||||
pip install pytest-repeat
|
||||
pytest tests/test_login.py --count=5 -v
|
||||
```
|
||||
|
||||
## 関連スキル
|
||||
|
||||
- `e2e-testing` — WebアプリケーションのPlaywright E2Eテスト
|
||||
- `cpp-testing` — GoogleTestを使用したC++ユニット/統合テスト
|
||||
- `cpp-coding-standards` — C++コードスタイルとパターン
|
||||
Reference in New Issue
Block a user