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:
Claude
2026-05-17 09:08:06 +09:00
committed by Affaan Mustafa
parent d66b5fa480
commit fabb4d0c11
9 changed files with 1578 additions and 10 deletions

View File

@@ -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}件の警告をレビューしてください。"}
```
## 例

View File

@@ -1,10 +1,10 @@
---
description: "スキルをスキャンして横断的な原則を抽出し、ルールとして提炼する"
description: "スキルをスキャンして横断的な原則を抽出し、ルールとして蒸留する"
---
# /rules-distill — スキルから原則をルールとして提炼する
# /rules-distill — スキルから原則をルールとして蒸留する
インストール済みのスキルをスキャンし、横断的な原則を抽出して、ルールとして提炼します。
インストール済みのスキルをスキャンし、横断的な原則を抽出して、ルールとして蒸留します。
## フロー

View 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` - 既存のエージェントとスキルの分類と整理。

View File

@@ -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` を使用する(本末転倒)

View 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 テスト

View 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`

View File

@@ -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を参照。
## ビデオ検索パラメータ

View 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が精確です。
- **ビルドモード**はRolldownv7+またはRollupv5〜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の代替バンドラー

View 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 AutomationUIAを使用したWindowsネイティブデスクトップアプリケーションのエンドツーエンドテスト。WPF、WinForms、Win32/MFC、Qt5.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 AutomationUIA**に依存します。これはWindowsに組み込まれたアクセシビリティAPIです。サポートされているすべてのフレームワークは、読み取りおよび操作可能なプロパティを持つUIA要素のツリーを公開します
```
テストPython
└── pywinautoUIAバックエンド
└── 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.75.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 3Windowsサンドボックスが必要です。
> 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++コードスタイルとパターン