hyva-playwright-test について
hyva-playwright-test は、Alpine.js を使う Hyvä ストアフロントの Playwright テスト作成向けスキルです。Alpine.js の DOM の動作を考慮したロケーター、操作、アサーションの方法をまとめています。
主な機能
- 01信頼性の高いスコープ付きロケーター戦略
- 02Alpine.js の操作パターン
- 03自動再試行する Web ファーストアサーション
- 04Hyvä と Luma のセレクター比較
活用シーン
- 01非表示要素との一致を避けてストアフロントのメッセージをテスト
- 02フィルター、メニュー、フォーム、カート更新のテスト作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ hyva-themes/ hyva-ai-tools --skill hyva-playwright-testインストール後、Claude Code で/hyva-playwright-testを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/account-patterns.mdcart-patterns.mdcategory-patterns.mdpage-object-patterns.mdproduct-patterns.mdselector-patterns.md詳細
Hyvä + Alpine.js の Playwright テストの作成
概要
Hyvä は、Luma の KnockoutJS/RequireJS/jQuery を Alpine.js + Tailwind CSS に置き換えます。Playwright の strict mode(複数の要素に一致するロケーターを拒否します)は、非表示の要素がページ全体に存在する Alpine.js の DOM パターンと競合します。このスキルでは、Hyvä のストアフロント向けに Playwright テストを作成する際に見つかった落とし穴と解決策について説明します。
最重要ルール:非表示の Alpine 要素
Hyvä のテンプレートでは、<div x-show="displayErrorMessage" class="message error"> のような要素が DOM のあちこちに配置されています。これらは非表示ですが DOM 上には存在するため、.message.error のような単独のセレクターは、非表示と表示中の両方の要素に一致し、Playwright の strict mode 違反を引き起こします。
ページレベルのメッセージは、必ず #messages コンテナーにスコープしてください:
// WRONG — matches hidden Alpine x-show elements throughout DOM
await expect(page.locator('.message.success')).toContainText('Added to cart');
await expect(page.locator('.message-error')).toContainText('Error');
// RIGHT — scoped to the visible messages container
await expect(page.locator('#messages .message.success')).toContainText('Added to cart');
await expect(page.locator('#messages .message-error, #messages .message.error')).toContainText('Error');
使用しないでください: セレクターとしての単独の .message、.message.error、.message.success、div.message。
例外 — ページ内メッセージ: すべての .message 要素がフラッシュメッセージというわけではありません。検索結果の「結果なし」通知(.message.notice)は、#messages コンテナー内ではなく、#maincontent 内に静的なページ内コンテンツとして描画されます。これらのページ内メッセージには、単独のクラスセレクターが適切です。
セレクターの方針
Playwright が推奨するロケーターの優先順位に従ってください:
getByRole()— 常に優先してください — ユーザーのページの認識方法に最も近い方法です。同じテキストが見出し、リンク、パンくずリスト、sr-onlyのスパンに表示される場合の、テキストの曖昧さを避けられます。getByLabel()— フォームコントロール(チェックボックス、関連付けられたラベルを持つ入力欄)に使用します。getByText()— 対話型でない要素に使用し、コンテナーにスコープします(例:page.locator('#maincontent').getByText(...))。getByPlaceholder()、getByAltText()— それぞれ入力欄と画像に使用します。getByTestId()— Hyvä がdata-testid属性を提供する場合、またはカスタムテスト ID を追加する場合に使用します。- CSS セレクター — 最後の手段として、ユーザー向けのロケーターが利用できない場合にのみ使用します。クラスベースのセレクターよりも、
aria-*属性セレクター(例:[aria-label="pagination"]、[aria-current="page"])を優先してください。CSS が必要な場合は、一意のコンテナーにスコープしてください(例:#messages .message.success)。
避けてください: :visible 疑似セレクター — Playwright のドキュメントにあるとおり、「通常は、要素を一意に識別できる、より信頼性の高い方法を見つけるほうが適切です」。代わりに、コンテナーにスコープするか、ロール/属性セレクターを使用してください。DOM に要素を区別する他の方法がない場合の、絶対的な最終手段としてのみ :visible を使用してください。
Alpine.js の操作パターン
| パターン | 問題 | 解決策 |
|---|---|---|
x-show で非表示になった要素 | strict mode:複数の要素に一致 | 一意のコンテナー(#messages)にスコープするか、ロール/属性セレクターを使用する |
x-defer="intersect" | 要素が表示されるまで初期化されない | 操作の前に scrollIntoViewIfNeeded() を実行する |
x-if(テンプレート) | 条件が true になるまで要素が DOM に存在しない | 先にトリガーをクリックしてから、子要素を検索する |
入力欄の x-model | フォーム送信後に Alpine が値をクリアする | 送信後に入力値をアサートしない。成功メッセージで確認する |
x-text/x-html の非同期更新 | カートのバッジが非同期で更新される | タイムアウト付きのウェブファーストアサーションを使用する:not.toHaveText('0', { timeout: 15_000 }) |
x-show のサブメニュー | ホバーするまで非表示 | 子要素をクリックする前に、親要素に hover() を実行する |
| Alpine のフォーム表示 | チェックボックスがオンになるまでフィールドが非表示 | チェックボックスをオンにした後、waitFor({ state: 'visible' }) を実行する |
入力欄での press('Enter') | Alpine にバインドされたフォームが予期せず送信される可能性がある | 送信ボタンの明示的な .click() を優先する |
アサーション
自動的に待機して再試行するウェブファーストアサーションを必ず使用してください:
// DO — auto-retries // DON'T — no retry
await expect(loc).toBeVisible(); // expect(await loc.isVisible()).toBe(true);
await expect(loc).toContainText('X'); // expect(await loc.textContent()).toContain('X');
非同期の Alpine.js 更新(カートの数量、価格)には、アサーションに長めのタイムアウトを指定してください — waitForTimeout() は使用しないでください:
// Cart count updates asynchronously via Alpine x-text
await expect(page.locator('#menu-cart-icon span[x-text="summaryCount"]'))
.not.toHaveText('0', { timeout: 15_000 });
Hyvä と Luma のセレクターの違い
| 要素 | Hyvä のセレクター | Luma のセレクター |
|---|---|---|
| ページネーションのナビゲーション | getByRole('navigation', { name: 'pagination' }) | ul.pages-items |
| ページリンク | getByRole('link', { name: 'Page 2' }) | .pages-items li a |
| アクティブなページ | [aria-current="page"] | <strong> 要素 |
| フィルターボタン | getByRole('button', { name: 'Color filter' }) | .filter-options-title |
| カートアイコンのバッジ | #menu-cart-icon > span[x-text="summaryCount"] | .counter-number |
| アカウントメニュー | #customer-menu + nav | .customer-menu |
| 成功メッセージ | #messages .message.success | .message-success |
| エラーメッセージ | #messages .message-error, #messages .message.error | .message-error |
| メインメニュー | getByRole('navigation', { name: 'Main menu' }) | nav.navigation |
| フッターナビゲーション | getByRole('navigation', { name: 'Company Menu' }).getByRole('link', { name }) | nav ul li:nth-child(N) a |
| 商品画像 | #gallery img[itemprop="image"] | #gallery img:visible |
| カートに追加(カード) | getByRole('button', { name: /Add to Cart/ }).first() | button.btn-primary:visible |
参考資料
コード例については references/ を参照してください。現在のタスクに関連するファイルを読み込んでください。
常に役立つ資料:
- page-object-patterns.md — ページオブジェクトの構成、ナビゲーション、フォーム送信、リダイレクト
- selector-patterns.md — セレクター修正の前後(メッセージ、テキストの曖昧さ、フォーム)
ページ別(そのページをテストする場合に読み込みます):
- cart-patterns.md — カートのスピナー待機、数量の変更、ミニカート
- product-patterns.md — バンドルの数量、ギャラリー画像
- account-patterns.md — パスワード変更(Alpine のチェックボックス表示)
- category-patterns.md — フィルター(x-defer のスクロール)、ページネーション(ARIA)