Day 3: セレクタとDOM操作
今日学ぶこと
- CSSセレクタの基本(ID, class, tag, attribute)
- cy.get() の詳細な使い方
- data-cy / data-testid 属性によるベストプラクティス
- DOM走査コマンド(find, parent, children, siblings)
- 要素の絞り込み(first, last, eq)
- cy.within() によるスコープ限定
- クリック・テキスト入力などのDOM操作
CSSセレクタの基本
Cypressでは、CSSセレクタを使って要素を取得します。まずはCSSセレクタの基本を確認しましょう。
flowchart TB
subgraph Selectors["CSSセレクタの種類"]
ID["#id\nID セレクタ"]
CLASS[".class\nクラスセレクタ"]
TAG["tag\nタグセレクタ"]
ATTR["[attr=val]\n属性セレクタ"]
end
style Selectors fill:#3b82f6,color:#fff
| セレクタ | 書き方 | 例 | 説明 |
|---|---|---|---|
| ID | #id |
#login-btn |
一意のIDで要素を取得 |
| クラス | .class |
.btn-primary |
クラス名で要素を取得 |
| タグ | tag |
button |
HTMLタグ名で要素を取得 |
| 属性 | [attr=value] |
[type="submit"] |
属性値で要素を取得 |
| 子孫 | parent child |
form input |
親の中にある子孫要素 |
| 直下の子 | parent > child |
ul > li |
直下の子要素のみ |
| 複合 | .class[attr] |
.btn[disabled] |
複数条件を組み合わせ |
// ID セレクタ
cy.get('#username')
// クラスセレクタ
cy.get('.submit-button')
// タグセレクタ
cy.get('h1')
// 属性セレクタ
cy.get('[type="email"]')
cy.get('[name="password"]')
// 複合セレクタ
cy.get('input[type="text"]')
cy.get('button.primary[type="submit"]')
// 子孫セレクタ
cy.get('.form-group input')
cy.get('nav > ul > li')
cy.get() の詳細な使い方
cy.get() はCypressで最も使用頻度の高いコマンドです。CSSセレクタを受け取り、マッチする要素を返します。
// 基本的な使い方
cy.get('button') // 全ての<button>要素
cy.get('.error-message') // class="error-message" の要素
cy.get('#submit') // id="submit" の要素
// タイムアウトの指定
cy.get('.loading', { timeout: 10000 }) // 最大10秒待機
// 複数要素が返る場合
cy.get('li') // 全ての<li>要素を取得
cy.get('li').should('have.length', 5) // 5つの<li>があることを確認
cy.get() と cy.contains() の使い分け
// cy.get() - CSSセレクタで要素を取得
cy.get('.nav-link')
// cy.contains() - テキスト内容で要素を取得
cy.contains('ログイン')
cy.contains('button', '送信') // <button>の中から「送信」を含むものを取得
// 正規表現も使える
cy.contains(/^合計: \d+円$/)
| コマンド | 用途 | 特徴 |
|---|---|---|
| cy.get() | CSSセレクタで取得 | 構造ベースの取得 |
| cy.contains() | テキスト内容で取得 | ユーザー視点の取得 |
data-cy / data-testid によるベストプラクティス
CSSクラスやIDは、デザイン変更やリファクタリングで変わる可能性があります。テスト専用の属性を使うことで、テストの安定性が大幅に向上します。
flowchart TB
subgraph Bad["避けるべきセレクタ"]
B1["タグ名\n(button)"]
B2["CSSクラス\n(.btn-primary)"]
B3["ID\n(#main-btn)"]
end
subgraph Good["推奨セレクタ"]
G1["data-cy\n[data-cy=submit]"]
G2["data-testid\n[data-testid=submit]"]
end
Bad -->|"変更に弱い"| FRAGILE["テストが壊れやすい"]
Good -->|"変更に強い"| STABLE["テストが安定する"]
style Bad fill:#ef4444,color:#fff
style Good fill:#22c55e,color:#fff
style FRAGILE fill:#ef4444,color:#fff
style STABLE fill:#22c55e,color:#fff
HTMLにdata-cy属性を追加
<!-- 推奨: テスト専用属性を使う -->
<button data-cy="submit-btn" class="btn btn-primary">送信</button>
<input data-cy="email-input" type="email" class="form-control" />
<div data-cy="error-message" class="alert alert-danger">エラーです</div>
Cypressでの使い方
// data-cy属性で要素を取得
cy.get('[data-cy="submit-btn"]').click()
cy.get('[data-cy="email-input"]').type('user@example.com')
cy.get('[data-cy="error-message"]').should('be.visible')
セレクタ戦略の優先順位
| 優先度 | セレクタ | 理由 |
|---|---|---|
| 1(最推奨) | [data-cy="..."] |
テスト専用。他の変更の影響を受けない |
| 2 | [data-testid="..."] |
Testing Libraryとの互換性あり |
| 3 | #id |
一意だが、JSやCSSで使われることがある |
| 4 | .class |
デザイン変更で壊れる可能性あり |
| 5(非推奨) | tag |
対象が広すぎて不安定 |
Cypress公式は
data-cy属性の使用を推奨しています。テストコードと本番コードの関心を明確に分離できます。
DOM走査コマンド
cy.get() で取得した要素を起点に、DOMツリーを走査するコマンドを見ていきましょう。
flowchart TB
subgraph DOM["DOM ツリー"]
PARENT["parent()\n親要素"]
CURRENT["現在の要素"]
CHILD1["children()\n子要素1"]
CHILD2["children()\n子要素2"]
SIBLING["siblings()\n兄弟要素"]
FIND["find()\n子孫要素"]
end
PARENT --> CURRENT
CURRENT --> CHILD1
CURRENT --> CHILD2
CURRENT --- SIBLING
CHILD1 --> FIND
style CURRENT fill:#3b82f6,color:#fff
style PARENT fill:#8b5cf6,color:#fff
style CHILD1 fill:#22c55e,color:#fff
style CHILD2 fill:#22c55e,color:#fff
style SIBLING fill:#f59e0b,color:#fff
style FIND fill:#22c55e,color:#fff
cy.find() - 子孫要素を検索
// cy.get() との違い: find() は現在の要素内から検索する
cy.get('.user-card').find('.username') // .user-card 内の .username
cy.get('form').find('input[type="text"]') // form 内の text input
// cy.get() はドキュメント全体から検索する
cy.get('.username') // ページ全体から .username を検索
cy.parent() と cy.parents() - 親要素へ移動
// 直接の親要素
cy.get('.error-text').parent()
// 条件に合う祖先要素
cy.get('.error-text').parents('.form-group')
// closest() のように最も近い祖先を取得
cy.get('.error-text').closest('.card')
cy.children() - 子要素を取得
// 全ての子要素
cy.get('ul.menu').children()
// 条件に合う子要素
cy.get('ul.menu').children('.active')
cy.siblings() - 兄弟要素を取得
// 全ての兄弟要素
cy.get('.active-tab').siblings()
// 条件に合う兄弟要素
cy.get('.active-tab').siblings('.disabled')
要素の絞り込み
複数の要素が返される場合に、特定の要素を選択するコマンドです。
// 最初の要素
cy.get('li').first()
// 最後の要素
cy.get('li').last()
// N番目の要素(0始まり)
cy.get('li').eq(0) // 1番目
cy.get('li').eq(2) // 3番目
cy.get('li').eq(-1) // 最後の要素
// フィルタリング
cy.get('li').filter('.active') // .active クラスを持つ li のみ
cy.get('li').not('.disabled') // .disabled クラスを持たない li のみ
| コマンド | 説明 | 例 |
|---|---|---|
| first() | 最初の要素 | cy.get('li').first() |
| last() | 最後の要素 | cy.get('li').last() |
| eq(index) | N番目の要素 | cy.get('li').eq(2) |
| filter(selector) | 条件に合う要素のみ | cy.get('li').filter('.done') |
| not(selector) | 条件に合わない要素 | cy.get('li').not('.skip') |
cy.within() でスコープを限定
cy.within() を使うと、特定の要素をスコープとしてその中だけでコマンドを実行できます。同じ構造が繰り返されるページで特に便利です。
<div data-cy="login-form">
<input name="email" />
<input name="password" />
<button>ログイン</button>
</div>
<div data-cy="register-form">
<input name="email" />
<input name="password" />
<button>登録</button>
</div>
// within() を使わない場合 - どちらの email かわからない
cy.get('[name="email"]') // 2つマッチしてしまう
// within() でスコープを限定
cy.get('[data-cy="login-form"]').within(() => {
cy.get('[name="email"]').type('user@example.com')
cy.get('[name="password"]').type('secret123')
cy.get('button').click()
})
// 別のフォームに対する操作
cy.get('[data-cy="register-form"]').within(() => {
cy.get('[name="email"]').type('newuser@example.com')
cy.get('[name="password"]').type('newpassword')
cy.get('button').click()
})
flowchart TB
subgraph Page["ページ全体"]
subgraph Login["login-form スコープ"]
LE["email input"]
LP["password input"]
LB["ログインボタン"]
end
subgraph Register["register-form スコープ"]
RE["email input"]
RP["password input"]
RB["登録ボタン"]
end
end
style Login fill:#3b82f6,color:#fff
style Register fill:#8b5cf6,color:#fff
要素のクリック操作
Cypressでは3種類のクリック操作が用意されています。
// 通常のクリック
cy.get('[data-cy="submit-btn"]').click()
// ダブルクリック
cy.get('[data-cy="item"]').dblclick()
// 右クリック(コンテキストメニュー)
cy.get('[data-cy="file"]').rightclick()
click() のオプション
// 要素の特定の位置をクリック
cy.get('.map').click('topLeft')
cy.get('.map').click('center') // デフォルト
cy.get('.map').click('bottomRight')
// 座標を指定してクリック
cy.get('.canvas').click(100, 200)
// 複数要素を順番にクリック
cy.get('.checkbox').click({ multiple: true })
// 要素が覆われていてもクリック(注意して使用)
cy.get('.hidden-btn').click({ force: true })
| オプション | 説明 | 使用例 |
|---|---|---|
| position | クリック位置 | click('topLeft') |
| x, y | 座標指定 | click(100, 200) |
| multiple | 複数要素を全てクリック | click({ multiple: true }) |
| force | 強制クリック | click({ force: true }) |
force: trueはテストが通らないときの安易な解決策として使わないでください。本当にUIが隠れている場合のみ使用しましょう。
テキスト入力操作
cy.type() - テキストを入力
// 基本的な入力
cy.get('[data-cy="email"]').type('user@example.com')
// 特殊キーの入力
cy.get('[data-cy="search"]').type('Cypress{enter}') // Enter キー
cy.get('[data-cy="name"]').type('{selectall}{backspace}') // 全選択して削除
cy.get('[data-cy="input"]').type('{ctrl+a}') // Ctrl+A
// 入力速度の調整(デフォルトは10ms)
cy.get('[data-cy="input"]').type('slow typing', { delay: 100 })
特殊キー一覧
| キー | 記法 | 説明 |
|---|---|---|
| Enter | {enter} |
Enterキー |
| Tab | {tab} |
Tabキー(プラグイン必要な場合あり) |
| Escape | {esc} |
Escapeキー |
| Backspace | {backspace} |
1文字削除 |
| Delete | {del} |
Deleteキー |
| 全選択 | {selectall} |
テキスト全選択 |
| 上矢印 | {uparrow} |
上矢印キー |
| 下矢印 | {downarrow} |
下矢印キー |
cy.clear() - 入力をクリア
// 入力フィールドをクリア
cy.get('[data-cy="email"]').clear()
// クリアしてから新しい値を入力
cy.get('[data-cy="email"]').clear().type('new@example.com')
その他の入力操作
// セレクトボックス
cy.get('[data-cy="country"]').select('Japan')
cy.get('[data-cy="country"]').select('jp') // value で選択
// チェックボックス
cy.get('[data-cy="agree"]').check()
cy.get('[data-cy="agree"]').uncheck()
// ラジオボタン
cy.get('[data-cy="plan"]').check('premium') // value を指定
実践: ユーザー登録フォームのテスト
ここまで学んだ内容を組み合わせて、実践的なテストを書いてみましょう。
describe('ユーザー登録フォーム', () => {
beforeEach(() => {
cy.visit('/register')
})
it('全てのフィールドを入力して登録できる', () => {
cy.get('[data-cy="register-form"]').within(() => {
// テキスト入力
cy.get('[data-cy="username"]').type('testuser')
cy.get('[data-cy="email"]').type('test@example.com')
cy.get('[data-cy="password"]').type('SecurePass123!')
cy.get('[data-cy="password-confirm"]').type('SecurePass123!')
// セレクトボックス
cy.get('[data-cy="country"]').select('Japan')
// チェックボックス
cy.get('[data-cy="terms"]').check()
// 送信ボタンをクリック
cy.get('[data-cy="submit"]').click()
})
// 登録成功メッセージの確認
cy.get('[data-cy="success-message"]')
.should('be.visible')
.and('contain', '登録が完了しました')
})
it('必須フィールドが空の場合エラーが表示される', () => {
cy.get('[data-cy="register-form"]').within(() => {
// 何も入力せずに送信
cy.get('[data-cy="submit"]').click()
// エラーメッセージの確認
cy.get('[data-cy="error-username"]')
.should('be.visible')
.and('have.text', 'ユーザー名は必須です')
cy.get('[data-cy="error-email"]')
.should('be.visible')
})
})
it('メールアドレスの形式が正しくない場合エラー', () => {
cy.get('[data-cy="register-form"]').within(() => {
cy.get('[data-cy="email"]').type('invalid-email')
cy.get('[data-cy="submit"]').click()
cy.get('[data-cy="error-email"]')
.should('contain', '有効なメールアドレスを入力してください')
})
})
})
まとめ
| 概念 | 説明 |
|---|---|
| CSSセレクタ | ID, クラス, タグ, 属性を組み合わせて要素を特定する |
| data-cy属性 | テスト専用属性で安定したセレクタを実現する |
| cy.get() | CSSセレクタでDOM要素を取得する基本コマンド |
| DOM走査 | find, parent, children, siblings で要素間を移動する |
| 要素の絞り込み | first, last, eq, filter, not で対象を限定する |
| cy.within() | 特定の要素をスコープとして操作を限定する |
| クリック操作 | click, dblclick, rightclick の3種類 |
| テキスト入力 | type, clear, select, check/uncheck で入力操作を行う |
重要ポイント
- data-cy属性を使おう - CSSクラスやIDに依存するとデザイン変更でテストが壊れる。テスト専用属性を使うことで保守性が大幅に向上する
- cy.within() で明確なスコープを - 同じ構造が繰り返されるページでは、within()でスコープを限定して意図を明確にする
- force: true は最終手段 - テストが通らない場合、まずUIの問題を疑う。force オプションは本当に必要な場合だけ使う
練習問題
問題1: 基本
以下のHTMLに対して、Cypressでログインフォームのテストを書いてください。
<form id="login-form">
<input data-cy="login-email" type="email" />
<input data-cy="login-password" type="password" />
<button data-cy="login-submit" type="submit">ログイン</button>
</form>
問題2: 応用
商品一覧ページで、3番目の商品カードの「カートに追加」ボタンをクリックするテストを書いてください。各商品カードは .product-card クラスを持ち、内部に [data-cy="add-to-cart"] ボタンがあります。
チャレンジ問題
以下の要件を満たすテストスイートを作成してください。
- 検索フォームにキーワードを入力してEnterキーで検索
- 検索結果が5件表示されることを確認
- 最初の結果をクリックして詳細ページに遷移
- 詳細ページの「お気に入り」ボタンをクリック
- 「お気に入りに追加しました」メッセージが表示されることを確認
参考リンク
- Cypress Best Practices - Selecting Elements
- cy.get() - Cypress公式ドキュメント
- cy.type() - Cypress公式ドキュメント
- cy.within() - Cypress公式ドキュメント
次回予告: Day 4では「アサーションをマスターする」について学びます。should() と expect() の使い分け、チェーンアサーション、リトライメカニズムを理解して、信頼性の高いテストを書けるようになりましょう。